Coverage Report

Created: 2025-07-23 06:21

/rust/registry/src/index.crates.io-6f17d22bba15001f/time-0.3.41/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
    hours: Hours,
54
    minutes: Minutes,
55
    seconds: Seconds,
56
}
57
58
impl 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
0
    pub const unsafe fn __from_hms_unchecked(hours: i8, minutes: i8, seconds: i8) -> Self {
82
0
        // Safety: The caller must uphold the safety invariants.
83
0
        unsafe {
84
0
            Self::from_hms_ranged_unchecked(
85
0
                Hours::new_unchecked(hours),
86
0
                Minutes::new_unchecked(minutes),
87
0
                Seconds::new_unchecked(seconds),
88
0
            )
89
0
        }
90
0
    }
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
0
    pub const fn from_hms(
105
0
        hours: i8,
106
0
        minutes: i8,
107
0
        seconds: i8,
108
0
    ) -> Result<Self, error::ComponentRange> {
109
0
        Ok(Self::from_hms_ranged(
110
0
            ensure_ranged!(Hours: hours),
111
0
            ensure_ranged!(Minutes: minutes),
112
0
            ensure_ranged!(Seconds: seconds),
113
        ))
114
0
    }
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
0
    pub(crate) const fn from_hms_ranged_unchecked(
121
0
        hours: Hours,
122
0
        minutes: Minutes,
123
0
        seconds: Seconds,
124
0
    ) -> Self {
125
0
        if hours.get() < 0 {
126
0
            debug_assert!(minutes.get() <= 0);
127
0
            debug_assert!(seconds.get() <= 0);
128
0
        } else if hours.get() > 0 {
129
0
            debug_assert!(minutes.get() >= 0);
130
0
            debug_assert!(seconds.get() >= 0);
131
0
        }
132
0
        if minutes.get() < 0 {
133
0
            debug_assert!(seconds.get() <= 0);
134
0
        } else if minutes.get() > 0 {
135
0
            debug_assert!(seconds.get() >= 0);
136
0
        }
137
138
0
        Self {
139
0
            hours,
140
0
            minutes,
141
0
            seconds,
142
0
        }
143
0
    }
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
0
    pub(crate) const fn from_hms_ranged(
151
0
        hours: Hours,
152
0
        mut minutes: Minutes,
153
0
        mut seconds: Seconds,
154
0
    ) -> Self {
155
0
        if (hours.get() > 0 && minutes.get() < 0) || (hours.get() < 0 && minutes.get() > 0) {
156
0
            minutes = minutes.neg();
157
0
        }
158
0
        if (hours.get() > 0 && seconds.get() < 0)
159
0
            || (hours.get() < 0 && seconds.get() > 0)
160
0
            || (minutes.get() > 0 && seconds.get() < 0)
161
0
            || (minutes.get() < 0 && seconds.get() > 0)
162
0
        {
163
0
            seconds = seconds.neg();
164
0
        }
165
166
0
        Self {
167
0
            hours,
168
0
            minutes,
169
0
            seconds,
170
0
        }
171
0
    }
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
0
    pub const fn from_whole_seconds(seconds: i32) -> Result<Self, error::ComponentRange> {
181
0
        Ok(Self::from_whole_seconds_ranged(
182
0
            ensure_ranged!(WholeSeconds: seconds),
183
        ))
184
0
    }
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
0
    pub(crate) const fn from_whole_seconds_ranged(seconds: WholeSeconds) -> Self {
198
0
        // Safety: The type of `seconds` guarantees that all values are in range.
199
0
        unsafe {
200
0
            Self::__from_hms_unchecked(
201
0
                (seconds.get() / Second::per(Hour) as i32) as i8,
202
0
                ((seconds.get() % Second::per(Hour) as i32) / Minute::per(Hour) as i32) as i8,
203
0
                (seconds.get() % Second::per(Minute) as i32) as i8,
204
0
            )
205
0
        }
206
0
    }
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
0
    pub const fn as_hms(self) -> (i8, i8, i8) {
217
0
        (self.hours.get(), self.minutes.get(), self.seconds.get())
218
0
    }
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
0
    pub const fn whole_hours(self) -> i8 {
236
0
        self.hours.get()
237
0
    }
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
0
    pub const fn whole_minutes(self) -> i16 {
248
0
        self.hours.get() as i16 * Minute::per(Hour) as i16 + self.minutes.get() as i16
249
0
    }
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
0
    pub const fn minutes_past_hour(self) -> i8 {
260
0
        self.minutes.get()
261
0
    }
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
0
    pub const fn whole_seconds(self) -> i32 {
274
0
        self.hours.get() as i32 * Second::per(Hour) as i32
275
0
            + self.minutes.get() as i32 * Second::per(Minute) as i32
276
0
            + self.seconds.get() as i32
277
0
    }
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
0
    pub const fn seconds_past_minute(self) -> i8 {
288
0
        self.seconds.get()
289
0
    }
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
0
    pub const fn is_utc(self) -> bool {
301
0
        self.hours.get() == 0 && self.minutes.get() == 0 && self.seconds.get() == 0
302
0
    }
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
0
    pub const fn is_positive(self) -> bool {
313
0
        self.hours.get() > 0 || self.minutes.get() > 0 || self.seconds.get() > 0
314
0
    }
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
0
    pub const fn is_negative(self) -> bool {
325
0
        self.hours.get() < 0 || self.minutes.get() < 0 || self.seconds.get() < 0
326
0
    }
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
0
    pub fn local_offset_at(datetime: OffsetDateTime) -> Result<Self, error::IndeterminateOffset> {
340
0
        local_offset_at(datetime).ok_or(error::IndeterminateOffset)
341
0
    }
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
0
    pub fn current_local_offset() -> Result<Self, error::IndeterminateOffset> {
355
0
        let now = OffsetDateTime::now_utc();
356
0
        local_offset_at(now).ok_or(error::IndeterminateOffset)
357
0
    }
358
}
359
360
#[cfg(feature = "formatting")]
361
impl UtcOffset {
362
    /// Format the `UtcOffset` using the provided [format description](crate::format_description).
363
0
    pub fn format_into(
364
0
        self,
365
0
        output: &mut (impl io::Write + ?Sized),
366
0
        format: &(impl Formattable + ?Sized),
367
0
    ) -> Result<usize, error::Format> {
368
0
        format.format_into(output, None, None, Some(self))
369
0
    }
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
0
    pub fn format(self, format: &(impl Formattable + ?Sized)) -> Result<String, error::Format> {
381
0
        format.format(None, None, Some(self))
382
0
    }
383
}
384
385
#[cfg(feature = "parsing")]
386
impl 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
405
mod private {
406
    #[non_exhaustive]
407
    #[derive(Debug, Clone, Copy)]
408
    pub struct UtcOffsetMetadata;
409
}
410
use private::UtcOffsetMetadata;
411
412
impl SmartDisplay for UtcOffset {
413
    type Metadata = UtcOffsetMetadata;
414
415
0
    fn metadata(&self, _: FormatterOptions) -> Metadata<Self> {
416
0
        let sign = if self.is_negative() { '-' } else { '+' };
417
0
        let width = smart_display::padded_width_of!(
418
0
            sign,
419
0
            self.hours.abs() => width(2),
420
0
            ":",
421
0
            self.minutes.abs() => width(2),
422
0
            ":",
423
0
            self.seconds.abs() => width(2),
424
0
        );
425
0
        Metadata::new(width, self, UtcOffsetMetadata)
426
0
    }
427
428
0
    fn fmt_with_metadata(
429
0
        &self,
430
0
        f: &mut fmt::Formatter<'_>,
431
0
        metadata: Metadata<Self>,
432
0
    ) -> fmt::Result {
433
0
        f.pad_with_width(
434
0
            metadata.unpadded_width(),
435
0
            format_args!(
436
0
                "{}{:02}:{:02}:{:02}",
437
0
                if self.is_negative() { '-' } else { '+' },
438
0
                self.hours.abs(),
439
0
                self.minutes.abs(),
440
0
                self.seconds.abs(),
441
0
            ),
442
0
        )
443
0
    }
444
}
445
446
impl fmt::Display for UtcOffset {
447
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
448
0
        SmartDisplay::fmt(self, f)
449
0
    }
450
}
451
452
impl fmt::Debug for UtcOffset {
453
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
454
0
        fmt::Display::fmt(self, f)
455
0
    }
456
}
457
458
impl Neg for UtcOffset {
459
    type Output = Self;
460
461
0
    fn neg(self) -> Self::Output {
462
0
        Self::from_hms_ranged(self.hours.neg(), self.minutes.neg(), self.seconds.neg())
463
0
    }
464
}