Skip to main content

time/
utc_offset.rs

1//! The [`UtcOffset`] struct and its associated `impl`s.
2
3#[cfg(feature = "formatting")]
4use alloc::string::String;
5use core::cmp::Ordering;
6use core::fmt;
7use core::hash::{Hash, Hasher};
8use core::ops::Neg;
9#[cfg(feature = "formatting")]
10use std::io;
11
12use deranged::{RangedI8, RangedI32};
13use powerfmt::smart_display::{FormatterOptions, Metadata, SmartDisplay};
14
15#[cfg(feature = "local-offset")]
16use crate::OffsetDateTime;
17use crate::error;
18#[cfg(feature = "formatting")]
19use crate::formatting::Formattable;
20use crate::internal_macros::ensure_ranged;
21#[cfg(feature = "parsing")]
22use crate::parsing::Parsable;
23#[cfg(feature = "local-offset")]
24use crate::sys::local_offset_at;
25use crate::unit::*;
26
27/// The type of the `hours` field of `UtcOffset`.
28type Hours = RangedI8<-25, 25>;
29/// The type of the `minutes` field of `UtcOffset`.
30type Minutes = RangedI8<{ -(Minute::per_t::<i8>(Hour) - 1) }, { Minute::per_t::<i8>(Hour) - 1 }>;
31/// The type of the `seconds` field of `UtcOffset`.
32type Seconds =
33    RangedI8<{ -(Second::per_t::<i8>(Minute) - 1) }, { Second::per_t::<i8>(Minute) - 1 }>;
34/// The type capable of storing the range of whole seconds that a `UtcOffset` can encompass.
35type WholeSeconds = RangedI32<
36    {
37        Hours::MIN.get() as i32 * Second::per_t::<i32>(Hour)
38            + Minutes::MIN.get() as i32 * Second::per_t::<i32>(Minute)
39            + Seconds::MIN.get() as i32
40    },
41    {
42        Hours::MAX.get() as i32 * Second::per_t::<i32>(Hour)
43            + Minutes::MAX.get() as i32 * Second::per_t::<i32>(Minute)
44            + Seconds::MAX.get() as i32
45    },
46>;
47
48/// An offset from UTC.
49///
50/// This struct can store values up to ±25:59:59. If you need support outside this range, please
51/// file an issue with your use case.
52// All three components _must_ have the same sign.
53#[derive(Clone, Copy, Eq)]
54#[cfg_attr(not(docsrs), repr(C))]
55pub struct UtcOffset {
56    // The order of this struct's fields matter. Do not reorder them.
57
58    // Little endian version
59    #[cfg(target_endian = "little")]
60    seconds: Seconds,
61    #[cfg(target_endian = "little")]
62    minutes: Minutes,
63    #[cfg(target_endian = "little")]
64    hours: Hours,
65
66    // Big endian version
67    #[cfg(target_endian = "big")]
68    hours: Hours,
69    #[cfg(target_endian = "big")]
70    minutes: Minutes,
71    #[cfg(target_endian = "big")]
72    seconds: Seconds,
73}
74
75impl Hash for UtcOffset {
76    #[inline]
77    fn hash<H>(&self, state: &mut H)
78    where
79        H: Hasher,
80    {
81        state.write_u32(self.as_u32_for_equality());
82    }
83}
84
85impl PartialEq for UtcOffset {
86    #[inline]
87    fn eq(&self, other: &Self) -> bool {
88        self.as_u32_for_equality().eq(&other.as_u32_for_equality())
89    }
90}
91
92impl PartialOrd for UtcOffset {
93    #[inline]
94    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
95        Some(self.cmp(other))
96    }
97}
98
99impl Ord for UtcOffset {
100    #[inline]
101    fn cmp(&self, other: &Self) -> Ordering {
102        self.as_i32_for_comparison()
103            .cmp(&other.as_i32_for_comparison())
104    }
105}
106
107impl UtcOffset {
108    /// Provide a representation of the `UtcOffset` as a `i32`. This value can be used for equality,
109    /// and hashing. This value is not suitable for ordering; use `as_i32_for_comparison` instead.
110    #[inline]
111    pub(crate) const fn as_u32_for_equality(self) -> u32 {
112        // Safety: Size and alignment are handled by the compiler. Both the source and destination
113        // types are plain old data (POD) types.
114        unsafe {
115            if const { cfg!(target_endian = "little") } {
116                core::mem::transmute::<[i8; 4], u32>([
117                    self.seconds.get(),
118                    self.minutes.get(),
119                    self.hours.get(),
120                    0,
121                ])
122            } else {
123                core::mem::transmute::<[i8; 4], u32>([
124                    self.hours.get(),
125                    self.minutes.get(),
126                    self.seconds.get(),
127                    0,
128                ])
129            }
130        }
131    }
132
133    /// Provide a representation of the `UtcOffset` as a `i32`. This value can be used for ordering.
134    /// While it is suitable for equality, `as_u32_for_equality` is preferred for performance
135    /// reasons.
136    #[inline]
137    const fn as_i32_for_comparison(self) -> i32 {
138        (self.hours.get() as i32) << 16
139            | (self.minutes.get() as i32) << 8
140            | (self.seconds.get() as i32)
141    }
142
143    /// A `UtcOffset` that is UTC.
144    ///
145    /// ```rust
146    /// # use time::UtcOffset;
147    /// # use time_macros::offset;
148    /// assert_eq!(UtcOffset::UTC, offset!(UTC));
149    /// ```
150    pub const UTC: Self = Self::from_whole_seconds_ranged(WholeSeconds::new_static::<0>());
151
152    /// Create a `UtcOffset` representing an offset of the hours, minutes, and seconds provided, the
153    /// validity of which must be guaranteed by the caller. All three parameters must have the same
154    /// sign.
155    ///
156    /// # Safety
157    ///
158    /// - Hours must be in the range `-25..=25`.
159    /// - Minutes must be in the range `-59..=59`.
160    /// - Seconds must be in the range `-59..=59`.
161    ///
162    /// While the signs of the parameters are required to match to avoid bugs, this is not a safety
163    /// invariant.
164    #[doc(hidden)]
165    #[inline]
166    #[track_caller]
167    pub const unsafe fn __from_hms_unchecked(hours: i8, minutes: i8, seconds: i8) -> Self {
168        // Safety: The caller must uphold the safety invariants.
169        unsafe {
170            Self::from_hms_ranged_unchecked(
171                Hours::new_unchecked(hours),
172                Minutes::new_unchecked(minutes),
173                Seconds::new_unchecked(seconds),
174            )
175        }
176    }
177
178    /// Create a `UtcOffset` representing an offset by the number of hours, minutes, and seconds
179    /// provided.
180    ///
181    /// The sign of all three components should match. If they do not, all smaller components will
182    /// have their signs flipped.
183    ///
184    /// ```rust
185    /// # use time::UtcOffset;
186    /// assert_eq!(UtcOffset::from_hms(1, 2, 3)?.as_hms(), (1, 2, 3));
187    /// assert_eq!(UtcOffset::from_hms(1, -2, -3)?.as_hms(), (1, 2, 3));
188    /// # Ok::<_, time::Error>(())
189    /// ```
190    #[inline]
191    pub const fn from_hms(
192        hours: i8,
193        minutes: i8,
194        seconds: i8,
195    ) -> Result<Self, error::ComponentRange> {
196        Ok(Self::from_hms_ranged(
197            ensure_ranged!(Hours: hours("offset hour")),
198            ensure_ranged!(Minutes: minutes("offset minute")),
199            ensure_ranged!(Seconds: seconds("offset second")),
200        ))
201    }
202
203    /// Create a `UtcOffset` representing an offset of the hours, minutes, and seconds provided. All
204    /// three parameters must have the same sign.
205    ///
206    /// While the signs of the parameters are required to match, this is not a safety invariant.
207    #[inline]
208    #[track_caller]
209    pub(crate) const fn from_hms_ranged_unchecked(
210        hours: Hours,
211        minutes: Minutes,
212        seconds: Seconds,
213    ) -> Self {
214        if hours.get() < 0 {
215            debug_assert!(minutes.get() <= 0);
216            debug_assert!(seconds.get() <= 0);
217        } else if hours.get() > 0 {
218            debug_assert!(minutes.get() >= 0);
219            debug_assert!(seconds.get() >= 0);
220        }
221        if minutes.get() < 0 {
222            debug_assert!(seconds.get() <= 0);
223        } else if minutes.get() > 0 {
224            debug_assert!(seconds.get() >= 0);
225        }
226
227        Self {
228            hours,
229            minutes,
230            seconds,
231        }
232    }
233
234    /// Create a `UtcOffset` representing an offset by the number of hours, minutes, and seconds
235    /// provided.
236    ///
237    /// The sign of all three components should match. If they do not, all smaller components will
238    /// have their signs flipped.
239    #[inline]
240    pub(crate) const fn from_hms_ranged(
241        hours: Hours,
242        mut minutes: Minutes,
243        mut seconds: Seconds,
244    ) -> Self {
245        if (hours.get() > 0 && minutes.get() < 0) || (hours.get() < 0 && minutes.get() > 0) {
246            minutes = minutes.neg();
247        }
248        if (hours.get() > 0 && seconds.get() < 0)
249            || (hours.get() < 0 && seconds.get() > 0)
250            || (minutes.get() > 0 && seconds.get() < 0)
251            || (minutes.get() < 0 && seconds.get() > 0)
252        {
253            seconds = seconds.neg();
254        }
255
256        Self {
257            hours,
258            minutes,
259            seconds,
260        }
261    }
262
263    /// Create a `UtcOffset` representing an offset by the number of seconds provided.
264    ///
265    /// ```rust
266    /// # use time::UtcOffset;
267    /// assert_eq!(UtcOffset::from_whole_seconds(3_723)?.as_hms(), (1, 2, 3));
268    /// # Ok::<_, time::Error>(())
269    /// ```
270    #[inline]
271    pub const fn from_whole_seconds(seconds: i32) -> Result<Self, error::ComponentRange> {
272        Ok(Self::from_whole_seconds_ranged(
273            ensure_ranged!(WholeSeconds: seconds),
274        ))
275    }
276
277    /// Create a `UtcOffset` representing an offset by the number of seconds provided.
278    // ignore because the function is crate-private
279    /// ```rust,ignore
280    /// # use time::UtcOffset;
281    /// # use deranged::RangedI32;
282    /// assert_eq!(
283    ///     UtcOffset::from_whole_seconds_ranged(RangedI32::new_static::<3_723>()).as_hms(),
284    ///     (1, 2, 3)
285    /// );
286    /// # Ok::<_, time::Error>(())
287    /// ```
288    #[inline]
289    pub(crate) const fn from_whole_seconds_ranged(seconds: WholeSeconds) -> Self {
290        // Safety: The type of `seconds` guarantees that all values are in range.
291        unsafe {
292            Self::__from_hms_unchecked(
293                (seconds.get() / Second::per_t::<i32>(Hour)) as i8,
294                ((seconds.get() % Second::per_t::<i32>(Hour)) / Minute::per_t::<i32>(Hour)) as i8,
295                (seconds.get() % Second::per_t::<i32>(Minute)) as i8,
296            )
297        }
298    }
299
300    /// Obtain the UTC offset as its hours, minutes, and seconds. The sign of all three components
301    /// will always match. A positive value indicates an offset to the east; a negative to the west.
302    ///
303    /// ```rust
304    /// # use time_macros::offset;
305    /// assert_eq!(offset!(+1:02:03).as_hms(), (1, 2, 3));
306    /// assert_eq!(offset!(-1:02:03).as_hms(), (-1, -2, -3));
307    /// ```
308    #[inline]
309    pub const fn as_hms(self) -> (i8, i8, i8) {
310        (self.hours.get(), self.minutes.get(), self.seconds.get())
311    }
312
313    /// Obtain the UTC offset as its hours, minutes, and seconds. The sign of all three components
314    /// will always match. A positive value indicates an offset to the east; a negative to the west.
315    #[cfg(feature = "quickcheck")]
316    #[inline]
317    pub(crate) const fn as_hms_ranged(self) -> (Hours, Minutes, Seconds) {
318        (self.hours, self.minutes, self.seconds)
319    }
320
321    /// Obtain the number of whole hours the offset is from UTC. A positive value indicates an
322    /// offset to the east; a negative to the west.
323    ///
324    /// ```rust
325    /// # use time_macros::offset;
326    /// assert_eq!(offset!(+1:02:03).whole_hours(), 1);
327    /// assert_eq!(offset!(-1:02:03).whole_hours(), -1);
328    /// ```
329    #[inline]
330    pub const fn whole_hours(self) -> i8 {
331        self.hours.get()
332    }
333
334    /// Obtain the number of whole minutes the offset is from UTC. A positive value indicates an
335    /// offset to the east; a negative to the west.
336    ///
337    /// ```rust
338    /// # use time_macros::offset;
339    /// assert_eq!(offset!(+1:02:03).whole_minutes(), 62);
340    /// assert_eq!(offset!(-1:02:03).whole_minutes(), -62);
341    /// ```
342    #[inline]
343    pub const fn whole_minutes(self) -> i16 {
344        self.hours.get() as i16 * Minute::per_t::<i16>(Hour) + self.minutes.get() as i16
345    }
346
347    /// Obtain the number of minutes past the hour the offset is from UTC. A positive value
348    /// indicates an offset to the east; a negative to the west.
349    ///
350    /// ```rust
351    /// # use time_macros::offset;
352    /// assert_eq!(offset!(+1:02:03).minutes_past_hour(), 2);
353    /// assert_eq!(offset!(-1:02:03).minutes_past_hour(), -2);
354    /// ```
355    #[inline]
356    pub const fn minutes_past_hour(self) -> i8 {
357        self.minutes.get()
358    }
359
360    /// Obtain the number of whole seconds the offset is from UTC. A positive value indicates an
361    /// offset to the east; a negative to the west.
362    ///
363    /// ```rust
364    /// # use time_macros::offset;
365    /// assert_eq!(offset!(+1:02:03).whole_seconds(), 3723);
366    /// assert_eq!(offset!(-1:02:03).whole_seconds(), -3723);
367    /// ```
368    // This may be useful for anyone manually implementing arithmetic, as it
369    // would let them construct a `Duration` directly.
370    #[inline]
371    pub const fn whole_seconds(self) -> i32 {
372        self.hours.get() as i32 * Second::per_t::<i32>(Hour)
373            + self.minutes.get() as i32 * Second::per_t::<i32>(Minute)
374            + self.seconds.get() as i32
375    }
376
377    /// Obtain the number of seconds past the minute the offset is from UTC. A positive value
378    /// indicates an offset to the east; a negative to the west.
379    ///
380    /// ```rust
381    /// # use time_macros::offset;
382    /// assert_eq!(offset!(+1:02:03).seconds_past_minute(), 3);
383    /// assert_eq!(offset!(-1:02:03).seconds_past_minute(), -3);
384    /// ```
385    #[inline]
386    pub const fn seconds_past_minute(self) -> i8 {
387        self.seconds.get()
388    }
389
390    /// Check if the offset is exactly UTC.
391    ///
392    ///
393    /// ```rust
394    /// # use time_macros::offset;
395    /// assert!(!offset!(+1:02:03).is_utc());
396    /// assert!(!offset!(-1:02:03).is_utc());
397    /// assert!(offset!(UTC).is_utc());
398    /// ```
399    #[inline]
400    pub const fn is_utc(self) -> bool {
401        self.as_u32_for_equality() == Self::UTC.as_u32_for_equality()
402    }
403
404    /// Check if the offset is positive, or east of UTC.
405    ///
406    /// ```rust
407    /// # use time_macros::offset;
408    /// assert!(offset!(+1:02:03).is_positive());
409    /// assert!(!offset!(-1:02:03).is_positive());
410    /// assert!(!offset!(UTC).is_positive());
411    /// ```
412    #[inline]
413    pub const fn is_positive(self) -> bool {
414        self.as_i32_for_comparison() > Self::UTC.as_i32_for_comparison()
415    }
416
417    /// Check if the offset is negative, or west of UTC.
418    ///
419    /// ```rust
420    /// # use time_macros::offset;
421    /// assert!(!offset!(+1:02:03).is_negative());
422    /// assert!(offset!(-1:02:03).is_negative());
423    /// assert!(!offset!(UTC).is_negative());
424    /// ```
425    #[inline]
426    pub const fn is_negative(self) -> bool {
427        self.as_i32_for_comparison() < Self::UTC.as_i32_for_comparison()
428    }
429
430    /// Attempt to obtain the system's UTC offset at a known moment in time. If the offset cannot be
431    /// determined, an error is returned.
432    ///
433    /// ```rust
434    /// # use time::{UtcOffset, OffsetDateTime};
435    /// let local_offset = UtcOffset::local_offset_at(OffsetDateTime::UNIX_EPOCH);
436    /// # if false {
437    /// assert!(local_offset.is_ok());
438    /// # }
439    /// ```
440    #[cfg(feature = "local-offset")]
441    #[inline]
442    pub fn local_offset_at(datetime: OffsetDateTime) -> Result<Self, error::IndeterminateOffset> {
443        local_offset_at(datetime).ok_or(error::IndeterminateOffset)
444    }
445
446    /// Attempt to obtain the system's current UTC offset. If the offset cannot be determined, an
447    /// error is returned.
448    ///
449    /// ```rust
450    /// # use time::UtcOffset;
451    /// let local_offset = UtcOffset::current_local_offset();
452    /// # if false {
453    /// assert!(local_offset.is_ok());
454    /// # }
455    /// ```
456    #[cfg(feature = "local-offset")]
457    #[inline]
458    pub fn current_local_offset() -> Result<Self, error::IndeterminateOffset> {
459        let now = OffsetDateTime::now_utc();
460        local_offset_at(now).ok_or(error::IndeterminateOffset)
461    }
462}
463
464#[cfg(feature = "formatting")]
465impl UtcOffset {
466    /// Format the `UtcOffset` using the provided [format description](crate::format_description).
467    #[inline]
468    pub fn format_into(
469        self,
470        output: &mut (impl io::Write + ?Sized),
471        format: &(impl Formattable + ?Sized),
472    ) -> Result<usize, error::Format> {
473        format.format_into(output, &self, &mut Default::default())
474    }
475
476    /// Format the `UtcOffset` using the provided [format description](crate::format_description).
477    ///
478    /// ```rust
479    /// # use time::format_description;
480    /// # use time_macros::offset;
481    /// let format = format_description::parse("[offset_hour sign:mandatory]:[offset_minute]")?;
482    /// assert_eq!(offset!(+1).format(&format)?, "+01:00");
483    /// # Ok::<_, time::Error>(())
484    /// ```
485    #[inline]
486    pub fn format(self, format: &(impl Formattable + ?Sized)) -> Result<String, error::Format> {
487        format.format(&self, &mut Default::default())
488    }
489}
490
491#[cfg(feature = "parsing")]
492impl UtcOffset {
493    /// Parse a `UtcOffset` from the input using the provided [format
494    /// description](crate::format_description).
495    ///
496    /// ```rust
497    /// # use time::UtcOffset;
498    /// # use time_macros::{offset, format_description};
499    /// let format = format_description!("[offset_hour]:[offset_minute]");
500    /// assert_eq!(UtcOffset::parse("-03:42", &format)?, offset!(-3:42));
501    /// # Ok::<_, time::Error>(())
502    /// ```
503    #[inline]
504    pub fn parse(
505        input: &str,
506        description: &(impl Parsable + ?Sized),
507    ) -> Result<Self, error::Parse> {
508        description.parse_offset(input.as_bytes())
509    }
510}
511
512mod private {
513    /// Metadata for `UtcOffset`.
514    #[non_exhaustive]
515    #[derive(Debug, Clone, Copy)]
516    pub struct UtcOffsetMetadata;
517}
518use private::UtcOffsetMetadata;
519
520// This no longer needs special handling, as the format is fixed and doesn't require anything
521// advanced. Trait impls can't be deprecated and the info is still useful for other types
522// implementing `SmartDisplay`, so leave it as-is for now.
523impl SmartDisplay for UtcOffset {
524    type Metadata = UtcOffsetMetadata;
525
526    #[inline]
527    fn metadata(&self, _: FormatterOptions) -> Metadata<'_, Self> {
528        Metadata::new(9, self, UtcOffsetMetadata)
529    }
530
531    #[inline]
532    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
533        fmt::Display::fmt(self, f)
534    }
535}
536
537/// Obtain a pointer to the two ASCII digits representing `n`.
538///
539/// # Safety: `n` must be less than 100.
540#[inline]
541unsafe fn digits_ptr(n: u8) -> *const u8 {
542    const DIGIT_PAIRS: [u8; 200] = *b"0001020304050607080910111213141516171819\
543                                      2021222324252627282930313233343536373839\
544                                      4041424344454647484950515253545556575859\
545                                      6061626364656667686970717273747576777879\
546                                      8081828384858687888990919293949596979899";
547
548    debug_assert!(n < 100);
549    // Safety: We're staying within the bounds of the array.
550    unsafe { DIGIT_PAIRS.as_ptr().add((n as usize) * 2) }
551}
552
553impl fmt::Display for UtcOffset {
554    #[inline]
555    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
556        let hours = self.hours.get().unsigned_abs();
557        let minutes = self.minutes.get().unsigned_abs();
558        let seconds = self.seconds.get().unsigned_abs();
559
560        let sign = if self.is_negative() { b'-' } else { b'+' };
561        let mut buf = [sign, 0, 0, b':', 0, 0, b':', 0, 0];
562
563        // Safety: `hours`, `minutes` and `seconds` are all less than 100. Both the source and
564        // destination are valid for two bytes, aligned, and do not overlap.
565        unsafe {
566            digits_ptr(hours).copy_to_nonoverlapping(buf.as_mut_ptr().add(1), 2);
567            digits_ptr(minutes).copy_to_nonoverlapping(buf.as_mut_ptr().add(4), 2);
568            digits_ptr(seconds).copy_to_nonoverlapping(buf.as_mut_ptr().add(7), 2);
569        }
570
571        // Safety: All bytes are ASCII, which is a subset of UTF-8.
572        f.pad(unsafe { str::from_utf8_unchecked(&buf) })
573    }
574}
575
576impl fmt::Debug for UtcOffset {
577    #[inline]
578    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
579        fmt::Display::fmt(self, f)
580    }
581}
582
583impl Neg for UtcOffset {
584    type Output = Self;
585
586    #[inline]
587    fn neg(self) -> Self::Output {
588        Self::from_hms_ranged(self.hours.neg(), self.minutes.neg(), self.seconds.neg())
589    }
590}