time/
utc_offset.rs

1//! The [`UtcOffset`] struct and its associated `impl`s.
2
3#[cfg(feature = "formatting")]
4use alloc::string::String;
5use core::fmt;
6use core::ops::Neg;
7#[cfg(feature = "formatting")]
8use std::io;
9
10use deranged::{RangedI32, RangedI8};
11use powerfmt::ext::FormatterExt;
12use powerfmt::smart_display::{self, FormatterOptions, Metadata, SmartDisplay};
13
14use crate::convert::*;
15use crate::error;
16#[cfg(feature = "formatting")]
17use crate::formatting::Formattable;
18use crate::internal_macros::ensure_ranged;
19#[cfg(feature = "parsing")]
20use crate::parsing::Parsable;
21#[cfg(feature = "local-offset")]
22use crate::sys::local_offset_at;
23#[cfg(feature = "local-offset")]
24use crate::OffsetDateTime;
25
26/// The type of the `hours` field of `UtcOffset`.
27type Hours = RangedI8<-25, 25>;
28/// The type of the `minutes` field of `UtcOffset`.
29type Minutes = RangedI8<{ -(Minute::per(Hour) as i8 - 1) }, { Minute::per(Hour) as i8 - 1 }>;
30/// The type of the `seconds` field of `UtcOffset`.
31type Seconds = RangedI8<{ -(Second::per(Minute) as i8 - 1) }, { Second::per(Minute) as i8 - 1 }>;
32/// The type capable of storing the range of whole seconds that a `UtcOffset` can encompass.
33type WholeSeconds = RangedI32<
34    {
35        Hours::MIN.get() as i32 * Second::per(Hour) as i32
36            + Minutes::MIN.get() as i32 * Second::per(Minute) as i32
37            + Seconds::MIN.get() as i32
38    },
39    {
40        Hours::MAX.get() as i32 * Second::per(Hour) as i32
41            + Minutes::MAX.get() as i32 * Second::per(Minute) as i32
42            + Seconds::MAX.get() as i32
43    },
44>;
45
46/// An offset from UTC.
47///
48/// This struct can store values up to ±25:59:59. If you need support outside this range, please
49/// file an issue with your use case.
50// All three components _must_ have the same sign.
51#[derive(Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
52pub struct UtcOffset {
53    hours: Hours,
54    minutes: Minutes,
55    seconds: Seconds,
56}
57
58impl UtcOffset {
59    /// A `UtcOffset` that is UTC.
60    ///
61    /// ```rust
62    /// # use time::UtcOffset;
63    /// # use time_macros::offset;
64    /// assert_eq!(UtcOffset::UTC, offset!(UTC));
65    /// ```
66    pub const UTC: Self = Self::from_whole_seconds_ranged(WholeSeconds::new_static::<0>());
67
68    // region: constructors
69    /// Create a `UtcOffset` representing an offset of the hours, minutes, and seconds provided, the
70    /// validity of which must be guaranteed by the caller. All three parameters must have the same
71    /// sign.
72    ///
73    /// # Safety
74    ///
75    /// - Hours must be in the range `-25..=25`.
76    /// - Minutes must be in the range `-59..=59`.
77    /// - Seconds must be in the range `-59..=59`.
78    ///
79    /// While the signs of the parameters are required to match to avoid bugs, this is not a safety
80    /// invariant.
81    #[doc(hidden)]
82    pub const unsafe fn __from_hms_unchecked(hours: i8, minutes: i8, seconds: i8) -> Self {
83        // Safety: The caller must uphold the safety invariants.
84        unsafe {
85            Self::from_hms_ranged_unchecked(
86                Hours::new_unchecked(hours),
87                Minutes::new_unchecked(minutes),
88                Seconds::new_unchecked(seconds),
89            )
90        }
91    }
92
93    /// Create a `UtcOffset` representing an offset by the number of hours, minutes, and seconds
94    /// provided.
95    ///
96    /// The sign of all three components should match. If they do not, all smaller components will
97    /// have their signs flipped.
98    ///
99    /// ```rust
100    /// # use time::UtcOffset;
101    /// assert_eq!(UtcOffset::from_hms(1, 2, 3)?.as_hms(), (1, 2, 3));
102    /// assert_eq!(UtcOffset::from_hms(1, -2, -3)?.as_hms(), (1, 2, 3));
103    /// # Ok::<_, time::Error>(())
104    /// ```
105    pub const fn from_hms(
106        hours: i8,
107        minutes: i8,
108        seconds: i8,
109    ) -> Result<Self, error::ComponentRange> {
110        Ok(Self::from_hms_ranged(
111            ensure_ranged!(Hours: hours),
112            ensure_ranged!(Minutes: minutes),
113            ensure_ranged!(Seconds: seconds),
114        ))
115    }
116
117    /// Create a `UtcOffset` representing an offset of the hours, minutes, and seconds provided. All
118    /// three parameters must have the same sign.
119    ///
120    /// While the signs of the parameters are required to match, this is not a safety invariant.
121    pub(crate) const fn from_hms_ranged_unchecked(
122        hours: Hours,
123        minutes: Minutes,
124        seconds: Seconds,
125    ) -> Self {
126        if hours.get() < 0 {
127            debug_assert!(minutes.get() <= 0);
128            debug_assert!(seconds.get() <= 0);
129        } else if hours.get() > 0 {
130            debug_assert!(minutes.get() >= 0);
131            debug_assert!(seconds.get() >= 0);
132        }
133        if minutes.get() < 0 {
134            debug_assert!(seconds.get() <= 0);
135        } else if minutes.get() > 0 {
136            debug_assert!(seconds.get() >= 0);
137        }
138
139        Self {
140            hours,
141            minutes,
142            seconds,
143        }
144    }
145
146    /// Create a `UtcOffset` representing an offset by the number of hours, minutes, and seconds
147    /// provided.
148    ///
149    /// The sign of all three components should match. If they do not, all smaller components will
150    /// have their signs flipped.
151    pub(crate) const fn from_hms_ranged(
152        hours: Hours,
153        mut minutes: Minutes,
154        mut seconds: Seconds,
155    ) -> Self {
156        if (hours.get() > 0 && minutes.get() < 0) || (hours.get() < 0 && minutes.get() > 0) {
157            minutes = minutes.neg();
158        }
159        if (hours.get() > 0 && seconds.get() < 0)
160            || (hours.get() < 0 && seconds.get() > 0)
161            || (minutes.get() > 0 && seconds.get() < 0)
162            || (minutes.get() < 0 && seconds.get() > 0)
163        {
164            seconds = seconds.neg();
165        }
166
167        Self {
168            hours,
169            minutes,
170            seconds,
171        }
172    }
173
174    /// Create a `UtcOffset` representing an offset by the number of seconds provided.
175    ///
176    /// ```rust
177    /// # use time::UtcOffset;
178    /// assert_eq!(UtcOffset::from_whole_seconds(3_723)?.as_hms(), (1, 2, 3));
179    /// # Ok::<_, time::Error>(())
180    /// ```
181    pub const fn from_whole_seconds(seconds: i32) -> Result<Self, error::ComponentRange> {
182        Ok(Self::from_whole_seconds_ranged(
183            ensure_ranged!(WholeSeconds: seconds),
184        ))
185    }
186
187    /// Create a `UtcOffset` representing an offset by the number of seconds provided.
188    // ignore because the function is crate-private
189    /// ```rust,ignore
190    /// # use time::UtcOffset;
191    /// # use deranged::RangedI32;
192    /// assert_eq!(
193    ///     UtcOffset::from_whole_seconds_ranged(RangedI32::new_static::<3_723>()).as_hms(),
194    ///     (1, 2, 3)
195    /// );
196    /// # Ok::<_, time::Error>(())
197    /// ```
198    pub(crate) const fn from_whole_seconds_ranged(seconds: WholeSeconds) -> Self {
199        // Safety: The type of `seconds` guarantees that all values are in range.
200        unsafe {
201            Self::__from_hms_unchecked(
202                (seconds.get() / Second::per(Hour) as i32) as _,
203                ((seconds.get() % Second::per(Hour) as i32) / Minute::per(Hour) as i32) as _,
204                (seconds.get() % Second::per(Minute) as i32) as _,
205            )
206        }
207    }
208    // endregion constructors
209
210    // region: getters
211    /// Obtain the UTC offset as its hours, minutes, and seconds. The sign of all three components
212    /// will always match. A positive value indicates an offset to the east; a negative to the west.
213    ///
214    /// ```rust
215    /// # use time_macros::offset;
216    /// assert_eq!(offset!(+1:02:03).as_hms(), (1, 2, 3));
217    /// assert_eq!(offset!(-1:02:03).as_hms(), (-1, -2, -3));
218    /// ```
219    pub const fn as_hms(self) -> (i8, i8, i8) {
220        (self.hours.get(), self.minutes.get(), self.seconds.get())
221    }
222
223    /// Obtain the UTC offset as its hours, minutes, and seconds. The sign of all three components
224    /// will always match. A positive value indicates an offset to the east; a negative to the west.
225    #[cfg(feature = "quickcheck")]
226    pub(crate) const fn as_hms_ranged(self) -> (Hours, Minutes, Seconds) {
227        (self.hours, self.minutes, self.seconds)
228    }
229
230    /// Obtain the number of whole hours the offset is from UTC. A positive value indicates an
231    /// offset to the east; a negative to the west.
232    ///
233    /// ```rust
234    /// # use time_macros::offset;
235    /// assert_eq!(offset!(+1:02:03).whole_hours(), 1);
236    /// assert_eq!(offset!(-1:02:03).whole_hours(), -1);
237    /// ```
238    pub const fn whole_hours(self) -> i8 {
239        self.hours.get()
240    }
241
242    /// Obtain the number of whole minutes the offset is from UTC. A positive value indicates an
243    /// offset to the east; a negative to the west.
244    ///
245    /// ```rust
246    /// # use time_macros::offset;
247    /// assert_eq!(offset!(+1:02:03).whole_minutes(), 62);
248    /// assert_eq!(offset!(-1:02:03).whole_minutes(), -62);
249    /// ```
250    pub const fn whole_minutes(self) -> i16 {
251        self.hours.get() as i16 * Minute::per(Hour) as i16 + self.minutes.get() as i16
252    }
253
254    /// Obtain the number of minutes past the hour the offset is from UTC. A positive value
255    /// indicates an offset to the east; a negative to the west.
256    ///
257    /// ```rust
258    /// # use time_macros::offset;
259    /// assert_eq!(offset!(+1:02:03).minutes_past_hour(), 2);
260    /// assert_eq!(offset!(-1:02:03).minutes_past_hour(), -2);
261    /// ```
262    pub const fn minutes_past_hour(self) -> i8 {
263        self.minutes.get()
264    }
265
266    /// Obtain the number of whole seconds the offset is from UTC. A positive value indicates an
267    /// offset to the east; a negative to the west.
268    ///
269    /// ```rust
270    /// # use time_macros::offset;
271    /// assert_eq!(offset!(+1:02:03).whole_seconds(), 3723);
272    /// assert_eq!(offset!(-1:02:03).whole_seconds(), -3723);
273    /// ```
274    // This may be useful for anyone manually implementing arithmetic, as it
275    // would let them construct a `Duration` directly.
276    pub const fn whole_seconds(self) -> i32 {
277        self.hours.get() as i32 * Second::per(Hour) as i32
278            + self.minutes.get() as i32 * Second::per(Minute) as i32
279            + self.seconds.get() as i32
280    }
281
282    /// Obtain the number of seconds past the minute the offset is from UTC. A positive value
283    /// indicates an offset to the east; a negative to the west.
284    ///
285    /// ```rust
286    /// # use time_macros::offset;
287    /// assert_eq!(offset!(+1:02:03).seconds_past_minute(), 3);
288    /// assert_eq!(offset!(-1:02:03).seconds_past_minute(), -3);
289    /// ```
290    pub const fn seconds_past_minute(self) -> i8 {
291        self.seconds.get()
292    }
293    // endregion getters
294
295    // region: is_{sign}
296    /// Check if the offset is exactly UTC.
297    ///
298    ///
299    /// ```rust
300    /// # use time_macros::offset;
301    /// assert!(!offset!(+1:02:03).is_utc());
302    /// assert!(!offset!(-1:02:03).is_utc());
303    /// assert!(offset!(UTC).is_utc());
304    /// ```
305    pub const fn is_utc(self) -> bool {
306        self.hours.get() == 0 && self.minutes.get() == 0 && self.seconds.get() == 0
307    }
308
309    /// Check if the offset is positive, or east of UTC.
310    ///
311    /// ```rust
312    /// # use time_macros::offset;
313    /// assert!(offset!(+1:02:03).is_positive());
314    /// assert!(!offset!(-1:02:03).is_positive());
315    /// assert!(!offset!(UTC).is_positive());
316    /// ```
317    pub const fn is_positive(self) -> bool {
318        self.hours.get() > 0 || self.minutes.get() > 0 || self.seconds.get() > 0
319    }
320
321    /// Check if the offset is negative, or west of UTC.
322    ///
323    /// ```rust
324    /// # use time_macros::offset;
325    /// assert!(!offset!(+1:02:03).is_negative());
326    /// assert!(offset!(-1:02:03).is_negative());
327    /// assert!(!offset!(UTC).is_negative());
328    /// ```
329    pub const fn is_negative(self) -> bool {
330        self.hours.get() < 0 || self.minutes.get() < 0 || self.seconds.get() < 0
331    }
332    // endregion is_{sign}
333
334    // region: local offset
335    /// Attempt to obtain the system's UTC offset at a known moment in time. If the offset cannot be
336    /// determined, an error is returned.
337    ///
338    /// ```rust
339    /// # use time::{UtcOffset, OffsetDateTime};
340    /// let local_offset = UtcOffset::local_offset_at(OffsetDateTime::UNIX_EPOCH);
341    /// # if false {
342    /// assert!(local_offset.is_ok());
343    /// # }
344    /// ```
345    #[cfg(feature = "local-offset")]
346    pub fn local_offset_at(datetime: OffsetDateTime) -> Result<Self, error::IndeterminateOffset> {
347        local_offset_at(datetime).ok_or(error::IndeterminateOffset)
348    }
349
350    /// Attempt to obtain the system's current UTC offset. If the offset cannot be determined, an
351    /// error is returned.
352    ///
353    /// ```rust
354    /// # use time::UtcOffset;
355    /// let local_offset = UtcOffset::current_local_offset();
356    /// # if false {
357    /// assert!(local_offset.is_ok());
358    /// # }
359    /// ```
360    #[cfg(feature = "local-offset")]
361    pub fn current_local_offset() -> Result<Self, error::IndeterminateOffset> {
362        let now = OffsetDateTime::now_utc();
363        local_offset_at(now).ok_or(error::IndeterminateOffset)
364    }
365    // endregion: local offset
366}
367
368// region: formatting & parsing
369#[cfg(feature = "formatting")]
370impl UtcOffset {
371    /// Format the `UtcOffset` using the provided [format description](crate::format_description).
372    pub fn format_into(
373        self,
374        output: &mut (impl io::Write + ?Sized),
375        format: &(impl Formattable + ?Sized),
376    ) -> Result<usize, error::Format> {
377        format.format_into(output, None, None, Some(self))
378    }
379
380    /// Format the `UtcOffset` using the provided [format description](crate::format_description).
381    ///
382    /// ```rust
383    /// # use time::format_description;
384    /// # use time_macros::offset;
385    /// let format = format_description::parse("[offset_hour sign:mandatory]:[offset_minute]")?;
386    /// assert_eq!(offset!(+1).format(&format)?, "+01:00");
387    /// # Ok::<_, time::Error>(())
388    /// ```
389    pub fn format(self, format: &(impl Formattable + ?Sized)) -> Result<String, error::Format> {
390        format.format(None, None, Some(self))
391    }
392}
393
394#[cfg(feature = "parsing")]
395impl UtcOffset {
396    /// Parse a `UtcOffset` from the input using the provided [format
397    /// description](crate::format_description).
398    ///
399    /// ```rust
400    /// # use time::UtcOffset;
401    /// # use time_macros::{offset, format_description};
402    /// let format = format_description!("[offset_hour]:[offset_minute]");
403    /// assert_eq!(UtcOffset::parse("-03:42", &format)?, offset!(-3:42));
404    /// # Ok::<_, time::Error>(())
405    /// ```
406    pub fn parse(
407        input: &str,
408        description: &(impl Parsable + ?Sized),
409    ) -> Result<Self, error::Parse> {
410        description.parse_offset(input.as_bytes())
411    }
412}
413
414mod private {
415    #[non_exhaustive]
416    #[derive(Debug, Clone, Copy)]
417    pub struct UtcOffsetMetadata;
418}
419use private::UtcOffsetMetadata;
420
421impl SmartDisplay for UtcOffset {
422    type Metadata = UtcOffsetMetadata;
423
424    fn metadata(&self, _: FormatterOptions) -> Metadata<Self> {
425        let sign = if self.is_negative() { '-' } else { '+' };
426        let width = smart_display::padded_width_of!(
427            sign,
428            self.hours.abs() => width(2),
429            ":",
430            self.minutes.abs() => width(2),
431            ":",
432            self.seconds.abs() => width(2),
433        );
434        Metadata::new(width, self, UtcOffsetMetadata)
435    }
436
437    fn fmt_with_metadata(
438        &self,
439        f: &mut fmt::Formatter<'_>,
440        metadata: Metadata<Self>,
441    ) -> fmt::Result {
442        f.pad_with_width(
443            metadata.unpadded_width(),
444            format_args!(
445                "{}{:02}:{:02}:{:02}",
446                if self.is_negative() { '-' } else { '+' },
447                self.hours.abs(),
448                self.minutes.abs(),
449                self.seconds.abs(),
450            ),
451        )
452    }
453}
454
455impl fmt::Display for UtcOffset {
456    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
457        SmartDisplay::fmt(self, f)
458    }
459}
460
461impl fmt::Debug for UtcOffset {
462    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
463        fmt::Display::fmt(self, f)
464    }
465}
466// endregion formatting & parsing
467
468impl Neg for UtcOffset {
469    type Output = Self;
470
471    fn neg(self) -> Self::Output {
472        Self::from_hms_ranged(self.hours.neg(), self.minutes.neg(), self.seconds.neg())
473    }
474}