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