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_t::<i8>(Hour) - 1) }, { Minute::per_t::<i8>(Hour) - 1 }>;
30/// The type of the `seconds` field of `UtcOffset`.
31type Seconds =
32    RangedI8<{ -(Second::per_t::<i8>(Minute) - 1) }, { Second::per_t::<i8>(Minute) - 1 }>;
33/// The type capable of storing the range of whole seconds that a `UtcOffset` can encompass.
34type WholeSeconds = RangedI32<
35    {
36        Hours::MIN.get() as i32 * Second::per_t::<i32>(Hour)
37            + Minutes::MIN.get() as i32 * Second::per_t::<i32>(Minute)
38            + Seconds::MIN.get() as i32
39    },
40    {
41        Hours::MAX.get() as i32 * Second::per_t::<i32>(Hour)
42            + Minutes::MAX.get() as i32 * Second::per_t::<i32>(Minute)
43            + Seconds::MAX.get() as i32
44    },
45>;
46
47/// An offset from UTC.
48///
49/// This struct can store values up to ±25:59:59. If you need support outside this range, please
50/// file an issue with your use case.
51// All three components _must_ have the same sign.
52#[derive(Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
53pub struct UtcOffset {
54    hours: Hours,
55    minutes: Minutes,
56    seconds: Seconds,
57}
58
59impl UtcOffset {
60    /// A `UtcOffset` that is UTC.
61    ///
62    /// ```rust
63    /// # use time::UtcOffset;
64    /// # use time_macros::offset;
65    /// assert_eq!(UtcOffset::UTC, offset!(UTC));
66    /// ```
67    pub const UTC: Self = Self::from_whole_seconds_ranged(WholeSeconds::new_static::<0>());
68
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_t::<i32>(Hour)) as i8,
203                ((seconds.get() % Second::per_t::<i32>(Hour)) / Minute::per_t::<i32>(Hour)) as i8,
204                (seconds.get() % Second::per_t::<i32>(Minute)) as i8,
205            )
206        }
207    }
208
209    /// Obtain the UTC offset as its hours, minutes, and seconds. The sign of all three components
210    /// will always match. A positive value indicates an offset to the east; a negative to the west.
211    ///
212    /// ```rust
213    /// # use time_macros::offset;
214    /// assert_eq!(offset!(+1:02:03).as_hms(), (1, 2, 3));
215    /// assert_eq!(offset!(-1:02:03).as_hms(), (-1, -2, -3));
216    /// ```
217    pub const fn as_hms(self) -> (i8, i8, i8) {
218        (self.hours.get(), self.minutes.get(), self.seconds.get())
219    }
220
221    /// Obtain the UTC offset as its hours, minutes, and seconds. The sign of all three components
222    /// will always match. A positive value indicates an offset to the east; a negative to the west.
223    #[cfg(feature = "quickcheck")]
224    pub(crate) const fn as_hms_ranged(self) -> (Hours, Minutes, Seconds) {
225        (self.hours, self.minutes, self.seconds)
226    }
227
228    /// Obtain the number of whole hours the offset is from UTC. A positive value indicates an
229    /// offset to the east; a negative to the west.
230    ///
231    /// ```rust
232    /// # use time_macros::offset;
233    /// assert_eq!(offset!(+1:02:03).whole_hours(), 1);
234    /// assert_eq!(offset!(-1:02:03).whole_hours(), -1);
235    /// ```
236    pub const fn whole_hours(self) -> i8 {
237        self.hours.get()
238    }
239
240    /// Obtain the number of whole minutes the offset is from UTC. A positive value indicates an
241    /// offset to the east; a negative to the west.
242    ///
243    /// ```rust
244    /// # use time_macros::offset;
245    /// assert_eq!(offset!(+1:02:03).whole_minutes(), 62);
246    /// assert_eq!(offset!(-1:02:03).whole_minutes(), -62);
247    /// ```
248    pub const fn whole_minutes(self) -> i16 {
249        self.hours.get() as i16 * Minute::per_t::<i16>(Hour) + self.minutes.get() as i16
250    }
251
252    /// Obtain the number of minutes past the hour the offset is from UTC. A positive value
253    /// indicates an offset to the east; a negative to the west.
254    ///
255    /// ```rust
256    /// # use time_macros::offset;
257    /// assert_eq!(offset!(+1:02:03).minutes_past_hour(), 2);
258    /// assert_eq!(offset!(-1:02:03).minutes_past_hour(), -2);
259    /// ```
260    pub const fn minutes_past_hour(self) -> i8 {
261        self.minutes.get()
262    }
263
264    /// Obtain the number of whole seconds the offset is from UTC. A positive value indicates an
265    /// offset to the east; a negative to the west.
266    ///
267    /// ```rust
268    /// # use time_macros::offset;
269    /// assert_eq!(offset!(+1:02:03).whole_seconds(), 3723);
270    /// assert_eq!(offset!(-1:02:03).whole_seconds(), -3723);
271    /// ```
272    // This may be useful for anyone manually implementing arithmetic, as it
273    // would let them construct a `Duration` directly.
274    pub const fn whole_seconds(self) -> i32 {
275        self.hours.get() as i32 * Second::per_t::<i32>(Hour)
276            + self.minutes.get() as i32 * Second::per_t::<i32>(Minute)
277            + self.seconds.get() as i32
278    }
279
280    /// Obtain the number of seconds past the minute the offset is from UTC. A positive value
281    /// indicates an offset to the east; a negative to the west.
282    ///
283    /// ```rust
284    /// # use time_macros::offset;
285    /// assert_eq!(offset!(+1:02:03).seconds_past_minute(), 3);
286    /// assert_eq!(offset!(-1:02:03).seconds_past_minute(), -3);
287    /// ```
288    pub const fn seconds_past_minute(self) -> i8 {
289        self.seconds.get()
290    }
291
292    /// Check if the offset is exactly UTC.
293    ///
294    ///
295    /// ```rust
296    /// # use time_macros::offset;
297    /// assert!(!offset!(+1:02:03).is_utc());
298    /// assert!(!offset!(-1:02:03).is_utc());
299    /// assert!(offset!(UTC).is_utc());
300    /// ```
301    pub const fn is_utc(self) -> bool {
302        self.hours.get() == 0 && self.minutes.get() == 0 && self.seconds.get() == 0
303    }
304
305    /// Check if the offset is positive, or east of UTC.
306    ///
307    /// ```rust
308    /// # use time_macros::offset;
309    /// assert!(offset!(+1:02:03).is_positive());
310    /// assert!(!offset!(-1:02:03).is_positive());
311    /// assert!(!offset!(UTC).is_positive());
312    /// ```
313    pub const fn is_positive(self) -> bool {
314        self.hours.get() > 0 || self.minutes.get() > 0 || self.seconds.get() > 0
315    }
316
317    /// Check if the offset is negative, or west of UTC.
318    ///
319    /// ```rust
320    /// # use time_macros::offset;
321    /// assert!(!offset!(+1:02:03).is_negative());
322    /// assert!(offset!(-1:02:03).is_negative());
323    /// assert!(!offset!(UTC).is_negative());
324    /// ```
325    pub const fn is_negative(self) -> bool {
326        self.hours.get() < 0 || self.minutes.get() < 0 || self.seconds.get() < 0
327    }
328
329    /// Attempt to obtain the system's UTC offset at a known moment in time. If the offset cannot be
330    /// determined, an error is returned.
331    ///
332    /// ```rust
333    /// # use time::{UtcOffset, OffsetDateTime};
334    /// let local_offset = UtcOffset::local_offset_at(OffsetDateTime::UNIX_EPOCH);
335    /// # if false {
336    /// assert!(local_offset.is_ok());
337    /// # }
338    /// ```
339    #[cfg(feature = "local-offset")]
340    pub fn local_offset_at(datetime: OffsetDateTime) -> Result<Self, error::IndeterminateOffset> {
341        local_offset_at(datetime).ok_or(error::IndeterminateOffset)
342    }
343
344    /// Attempt to obtain the system's current UTC offset. If the offset cannot be determined, an
345    /// error is returned.
346    ///
347    /// ```rust
348    /// # use time::UtcOffset;
349    /// let local_offset = UtcOffset::current_local_offset();
350    /// # if false {
351    /// assert!(local_offset.is_ok());
352    /// # }
353    /// ```
354    #[cfg(feature = "local-offset")]
355    pub fn current_local_offset() -> Result<Self, error::IndeterminateOffset> {
356        let now = OffsetDateTime::now_utc();
357        local_offset_at(now).ok_or(error::IndeterminateOffset)
358    }
359}
360
361#[cfg(feature = "formatting")]
362impl UtcOffset {
363    /// Format the `UtcOffset` using the provided [format description](crate::format_description).
364    pub fn format_into(
365        self,
366        output: &mut (impl io::Write + ?Sized),
367        format: &(impl Formattable + ?Sized),
368    ) -> Result<usize, error::Format> {
369        format.format_into(output, None, None, Some(self))
370    }
371
372    /// Format the `UtcOffset` using the provided [format description](crate::format_description).
373    ///
374    /// ```rust
375    /// # use time::format_description;
376    /// # use time_macros::offset;
377    /// let format = format_description::parse("[offset_hour sign:mandatory]:[offset_minute]")?;
378    /// assert_eq!(offset!(+1).format(&format)?, "+01:00");
379    /// # Ok::<_, time::Error>(())
380    /// ```
381    pub fn format(self, format: &(impl Formattable + ?Sized)) -> Result<String, error::Format> {
382        format.format(None, None, Some(self))
383    }
384}
385
386#[cfg(feature = "parsing")]
387impl UtcOffset {
388    /// Parse a `UtcOffset` from the input using the provided [format
389    /// description](crate::format_description).
390    ///
391    /// ```rust
392    /// # use time::UtcOffset;
393    /// # use time_macros::{offset, format_description};
394    /// let format = format_description!("[offset_hour]:[offset_minute]");
395    /// assert_eq!(UtcOffset::parse("-03:42", &format)?, offset!(-3:42));
396    /// # Ok::<_, time::Error>(())
397    /// ```
398    pub fn parse(
399        input: &str,
400        description: &(impl Parsable + ?Sized),
401    ) -> Result<Self, error::Parse> {
402        description.parse_offset(input.as_bytes())
403    }
404}
405
406mod private {
407    #[non_exhaustive]
408    #[derive(Debug, Clone, Copy)]
409    pub struct UtcOffsetMetadata;
410}
411use private::UtcOffsetMetadata;
412
413impl SmartDisplay for UtcOffset {
414    type Metadata = UtcOffsetMetadata;
415
416    fn metadata(&self, _: FormatterOptions) -> Metadata<Self> {
417        let sign = if self.is_negative() { '-' } else { '+' };
418        let width = smart_display::padded_width_of!(
419            sign,
420            self.hours.abs() => width(2),
421            ":",
422            self.minutes.abs() => width(2),
423            ":",
424            self.seconds.abs() => width(2),
425        );
426        Metadata::new(width, self, UtcOffsetMetadata)
427    }
428
429    fn fmt_with_metadata(
430        &self,
431        f: &mut fmt::Formatter<'_>,
432        metadata: Metadata<Self>,
433    ) -> fmt::Result {
434        f.pad_with_width(
435            metadata.unpadded_width(),
436            format_args!(
437                "{}{:02}:{:02}:{:02}",
438                if self.is_negative() { '-' } else { '+' },
439                self.hours.abs(),
440                self.minutes.abs(),
441                self.seconds.abs(),
442            ),
443        )
444    }
445}
446
447impl fmt::Display for UtcOffset {
448    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
449        SmartDisplay::fmt(self, f)
450    }
451}
452
453impl fmt::Debug for UtcOffset {
454    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
455        fmt::Display::fmt(self, f)
456    }
457}
458
459impl Neg for UtcOffset {
460    type Output = Self;
461
462    fn neg(self) -> Self::Output {
463        Self::from_hms_ranged(self.hours.neg(), self.minutes.neg(), self.seconds.neg())
464    }
465}