Coverage Report

Created: 2025-07-12 07:16

/rust/registry/src/index.crates.io-6f17d22bba15001f/time-0.3.37/src/utc_offset.rs
Line
Count
Source (jump to first uncovered line)
1
//! The [`UtcOffset`] struct and its associated `impl`s.
2
3
#[cfg(feature = "formatting")]
4
use alloc::string::String;
5
use core::fmt;
6
use core::ops::Neg;
7
#[cfg(feature = "formatting")]
8
use std::io;
9
10
use deranged::{RangedI32, RangedI8};
11
use powerfmt::ext::FormatterExt;
12
use powerfmt::smart_display::{self, FormatterOptions, Metadata, SmartDisplay};
13
14
use crate::convert::*;
15
use crate::error;
16
#[cfg(feature = "formatting")]
17
use crate::formatting::Formattable;
18
use crate::internal_macros::ensure_ranged;
19
#[cfg(feature = "parsing")]
20
use crate::parsing::Parsable;
21
#[cfg(feature = "local-offset")]
22
use crate::sys::local_offset_at;
23
#[cfg(feature = "local-offset")]
24
use crate::OffsetDateTime;
25
26
/// The type of the `hours` field of `UtcOffset`.
27
type Hours = RangedI8<-25, 25>;
28
/// The type of the `minutes` field of `UtcOffset`.
29
type Minutes = RangedI8<{ -(Minute::per(Hour) as i8 - 1) }, { Minute::per(Hour) as i8 - 1 }>;
30
/// The type of the `seconds` field of `UtcOffset`.
31
type 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.
33
type 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)]
52
pub 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
61
impl 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
0
    pub const unsafe fn __from_hms_unchecked(hours: i8, minutes: i8, seconds: i8) -> Self {
86
0
        // Safety: The caller must uphold the safety invariants.
87
0
        unsafe {
88
0
            Self::from_hms_ranged_unchecked(
89
0
                Hours::new_unchecked(hours),
90
0
                Minutes::new_unchecked(minutes),
91
0
                Seconds::new_unchecked(seconds),
92
0
            )
93
0
        }
94
0
    }
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
0
    pub const fn from_hms(
109
0
        hours: i8,
110
0
        minutes: i8,
111
0
        seconds: i8,
112
0
    ) -> Result<Self, error::ComponentRange> {
113
0
        Ok(Self::from_hms_ranged(
114
0
            ensure_ranged!(Hours: hours),
115
0
            ensure_ranged!(Minutes: minutes),
116
0
            ensure_ranged!(Seconds: seconds),
117
        ))
118
0
    }
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
0
    pub(crate) const fn from_hms_ranged_unchecked(
125
0
        hours: Hours,
126
0
        minutes: Minutes,
127
0
        seconds: Seconds,
128
0
    ) -> Self {
129
0
        if hours.get() < 0 {
130
0
            debug_assert!(minutes.get() <= 0);
131
0
            debug_assert!(seconds.get() <= 0);
132
0
        } else if hours.get() > 0 {
133
0
            debug_assert!(minutes.get() >= 0);
134
0
            debug_assert!(seconds.get() >= 0);
135
0
        }
136
0
        if minutes.get() < 0 {
137
0
            debug_assert!(seconds.get() <= 0);
138
0
        } else if minutes.get() > 0 {
139
0
            debug_assert!(seconds.get() >= 0);
140
0
        }
141
142
0
        Self {
143
0
            hours,
144
0
            minutes,
145
0
            seconds,
146
0
        }
147
0
    }
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
0
    pub(crate) const fn from_hms_ranged(
155
0
        hours: Hours,
156
0
        mut minutes: Minutes,
157
0
        mut seconds: Seconds,
158
0
    ) -> Self {
159
0
        if (hours.get() > 0 && minutes.get() < 0) || (hours.get() < 0 && minutes.get() > 0) {
160
0
            minutes = minutes.neg();
161
0
        }
162
0
        if (hours.get() > 0 && seconds.get() < 0)
163
0
            || (hours.get() < 0 && seconds.get() > 0)
164
0
            || (minutes.get() > 0 && seconds.get() < 0)
165
0
            || (minutes.get() < 0 && seconds.get() > 0)
166
0
        {
167
0
            seconds = seconds.neg();
168
0
        }
169
170
0
        Self {
171
0
            hours,
172
0
            minutes,
173
0
            seconds,
174
0
        }
175
0
    }
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
0
    pub const fn from_whole_seconds(seconds: i32) -> Result<Self, error::ComponentRange> {
185
0
        Ok(Self::from_whole_seconds_ranged(
186
0
            ensure_ranged!(WholeSeconds: seconds),
187
        ))
188
0
    }
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
0
    pub(crate) const fn from_whole_seconds_ranged(seconds: WholeSeconds) -> Self {
202
0
        // Safety: The type of `seconds` guarantees that all values are in range.
203
0
        unsafe {
204
0
            Self::__from_hms_unchecked(
205
0
                (seconds.get() / Second::per(Hour) as i32) as _,
206
0
                ((seconds.get() % Second::per(Hour) as i32) / Minute::per(Hour) as i32) as _,
207
0
                (seconds.get() % Second::per(Minute) as i32) as _,
208
0
            )
209
0
        }
210
0
    }
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
0
    pub const fn as_hms(self) -> (i8, i8, i8) {
223
0
        (self.hours.get(), self.minutes.get(), self.seconds.get())
224
0
    }
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
0
    pub const fn whole_hours(self) -> i8 {
242
0
        self.hours.get()
243
0
    }
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
0
    pub const fn whole_minutes(self) -> i16 {
254
0
        self.hours.get() as i16 * Minute::per(Hour) as i16 + self.minutes.get() as i16
255
0
    }
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
0
    pub const fn minutes_past_hour(self) -> i8 {
266
0
        self.minutes.get()
267
0
    }
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
0
    pub const fn whole_seconds(self) -> i32 {
280
0
        self.hours.get() as i32 * Second::per(Hour) as i32
281
0
            + self.minutes.get() as i32 * Second::per(Minute) as i32
282
0
            + self.seconds.get() as i32
283
0
    }
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
0
    pub const fn seconds_past_minute(self) -> i8 {
294
0
        self.seconds.get()
295
0
    }
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
0
    pub const fn is_utc(self) -> bool {
309
0
        self.hours.get() == 0 && self.minutes.get() == 0 && self.seconds.get() == 0
310
0
    }
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
0
    pub const fn is_positive(self) -> bool {
321
0
        self.hours.get() > 0 || self.minutes.get() > 0 || self.seconds.get() > 0
322
0
    }
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
0
    pub const fn is_negative(self) -> bool {
333
0
        self.hours.get() < 0 || self.minutes.get() < 0 || self.seconds.get() < 0
334
0
    }
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
0
    pub fn local_offset_at(datetime: OffsetDateTime) -> Result<Self, error::IndeterminateOffset> {
350
0
        local_offset_at(datetime).ok_or(error::IndeterminateOffset)
351
0
    }
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
0
    pub fn current_local_offset() -> Result<Self, error::IndeterminateOffset> {
365
0
        let now = OffsetDateTime::now_utc();
366
0
        local_offset_at(now).ok_or(error::IndeterminateOffset)
367
0
    }
368
    // endregion: local offset
369
}
370
371
// region: formatting & parsing
372
#[cfg(feature = "formatting")]
373
impl UtcOffset {
374
    /// Format the `UtcOffset` using the provided [format description](crate::format_description).
375
0
    pub fn format_into(
376
0
        self,
377
0
        output: &mut impl io::Write,
378
0
        format: &(impl Formattable + ?Sized),
379
0
    ) -> Result<usize, error::Format> {
380
0
        format.format_into(output, None, None, Some(self))
381
0
    }
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
0
    pub fn format(self, format: &(impl Formattable + ?Sized)) -> Result<String, error::Format> {
393
0
        format.format(None, None, Some(self))
394
0
    }
395
}
396
397
#[cfg(feature = "parsing")]
398
impl 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
417
mod private {
418
    #[non_exhaustive]
419
    #[derive(Debug, Clone, Copy)]
420
    pub struct UtcOffsetMetadata;
421
}
422
use private::UtcOffsetMetadata;
423
424
impl SmartDisplay for UtcOffset {
425
    type Metadata = UtcOffsetMetadata;
426
427
0
    fn metadata(&self, _: FormatterOptions) -> Metadata<Self> {
428
0
        let sign = if self.is_negative() { '-' } else { '+' };
429
0
        let width = smart_display::padded_width_of!(
430
0
            sign,
431
0
            self.hours.abs() => width(2),
432
0
            ":",
433
0
            self.minutes.abs() => width(2),
434
0
            ":",
435
0
            self.seconds.abs() => width(2),
436
0
        );
437
0
        Metadata::new(width, self, UtcOffsetMetadata)
438
0
    }
439
440
0
    fn fmt_with_metadata(
441
0
        &self,
442
0
        f: &mut fmt::Formatter<'_>,
443
0
        metadata: Metadata<Self>,
444
0
    ) -> fmt::Result {
445
0
        f.pad_with_width(
446
0
            metadata.unpadded_width(),
447
0
            format_args!(
448
0
                "{}{:02}:{:02}:{:02}",
449
0
                if self.is_negative() { '-' } else { '+' },
450
0
                self.hours.abs(),
451
0
                self.minutes.abs(),
452
0
                self.seconds.abs(),
453
0
            ),
454
0
        )
455
0
    }
456
}
457
458
impl fmt::Display for UtcOffset {
459
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
460
0
        SmartDisplay::fmt(self, f)
461
0
    }
462
}
463
464
impl fmt::Debug for UtcOffset {
465
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
466
0
        fmt::Display::fmt(self, f)
467
0
    }
468
}
469
// endregion formatting & parsing
470
471
impl Neg for UtcOffset {
472
    type Output = Self;
473
474
0
    fn neg(self) -> Self::Output {
475
0
        Self::from_hms_ranged(self.hours.neg(), self.minutes.neg(), self.seconds.neg())
476
0
    }
477
}