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