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