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