Coverage Report

Created: 2026-08-14 07:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/rust/registry/src/index.crates.io-1949cf8c6b5b557f/jiff-0.2.35/src/tz/offset.rs
Line
Count
Source
1
use core::{
2
    ops::{Add, AddAssign, Neg, Sub, SubAssign},
3
    time::Duration as UnsignedDuration,
4
};
5
6
use jcore::{constants as c, tz::Offset as JOffset};
7
8
use crate::{
9
    civil,
10
    duration::{Duration, SDuration},
11
    error::{tz::offset::Error as E, Error, ErrorContext},
12
    span::Span,
13
    timestamp::Timestamp,
14
    tz::{AmbiguousOffset, AmbiguousTimestamp, AmbiguousZoned, TimeZone},
15
    util::{b, constant, round::Increment},
16
    RoundMode, SignedDuration, Unit,
17
};
18
19
/// An enum indicating whether a particular datetime is in DST or not.
20
///
21
/// DST stands for "daylight saving time." It is a label used to apply to
22
/// points in time as a way to contrast it with "standard time." DST is
23
/// usually, but not always, one hour ahead of standard time. When DST takes
24
/// effect is usually determined by governments, and the rules can vary
25
/// depending on the location. DST is typically used as a means to maximize
26
/// "sunlight" time during typical working hours, and as a cost cutting measure
27
/// by reducing energy consumption. (The effectiveness of DST and whether it
28
/// is overall worth it is a separate question entirely.)
29
///
30
/// In general, most users should never need to deal with this type. But it can
31
/// be occasionally useful in circumstances where callers need to know whether
32
/// DST is active or not for a particular point in time.
33
///
34
/// This type has a `From<bool>` trait implementation, where the bool is
35
/// interpreted as being `true` when DST is active.
36
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
37
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
38
pub enum Dst {
39
    /// DST is not in effect. In other words, standard time is in effect.
40
    No,
41
    /// DST is in effect.
42
    Yes,
43
}
44
45
impl Dst {
46
    /// Returns true when this value is equal to `Dst::Yes`.
47
0
    pub fn is_dst(self) -> bool {
48
0
        matches!(self, Dst::Yes)
49
0
    }
50
51
    /// Returns true when this value is equal to `Dst::No`.
52
    ///
53
    /// `std` in this context refers to "standard time." That is, it is the
54
    /// offset from UTC used when DST is not in effect.
55
0
    pub fn is_std(self) -> bool {
56
0
        matches!(self, Dst::No)
57
0
    }
58
59
0
    pub(crate) fn from_jcore(dst: jcore::tz::Dst) -> Dst {
60
0
        match dst {
61
0
            jcore::tz::Dst::Yes => Dst::Yes,
62
0
            jcore::tz::Dst::No => Dst::No,
63
        }
64
0
    }
65
}
66
67
impl From<bool> for Dst {
68
0
    fn from(is_dst: bool) -> Dst {
69
0
        if is_dst {
70
0
            Dst::Yes
71
        } else {
72
0
            Dst::No
73
        }
74
0
    }
75
}
76
77
/// Represents a fixed time zone offset.
78
///
79
/// Negative offsets correspond to time zones west of the prime meridian, while
80
/// positive offsets correspond to time zones east of the prime meridian.
81
/// Equivalently, in all cases, `civil-time - offset = UTC`.
82
///
83
/// # Display format
84
///
85
/// This type implements the `std::fmt::Display` trait. It
86
/// will convert the offset to a string format in the form
87
/// `{sign}{hours}[:{minutes}[:{seconds}]]`, where `minutes` and `seconds` are
88
/// only present when non-zero. For example:
89
///
90
/// ```
91
/// use jiff::tz;
92
///
93
/// let o = tz::offset(-5);
94
/// assert_eq!(o.to_string(), "-05");
95
/// let o = tz::Offset::from_seconds(-18_000).unwrap();
96
/// assert_eq!(o.to_string(), "-05");
97
/// let o = tz::Offset::from_seconds(-18_060).unwrap();
98
/// assert_eq!(o.to_string(), "-05:01");
99
/// let o = tz::Offset::from_seconds(-18_062).unwrap();
100
/// assert_eq!(o.to_string(), "-05:01:02");
101
///
102
/// // The min value.
103
/// let o = tz::Offset::from_seconds(-93_599).unwrap();
104
/// assert_eq!(o.to_string(), "-25:59:59");
105
/// // The max value.
106
/// let o = tz::Offset::from_seconds(93_599).unwrap();
107
/// assert_eq!(o.to_string(), "+25:59:59");
108
/// // No offset.
109
/// let o = tz::offset(0);
110
/// assert_eq!(o.to_string(), "+00");
111
/// ```
112
///
113
/// # Example
114
///
115
/// This shows how to create a zoned datetime with a time zone using a fixed
116
/// offset:
117
///
118
/// ```
119
/// use jiff::{civil::date, tz, Zoned};
120
///
121
/// let offset = tz::offset(-4).to_time_zone();
122
/// let zdt = date(2024, 7, 8).at(15, 20, 0, 0).to_zoned(offset)?;
123
/// assert_eq!(zdt.to_string(), "2024-07-08T15:20:00-04:00[-04:00]");
124
///
125
/// # Ok::<(), Box<dyn std::error::Error>>(())
126
/// ```
127
///
128
/// Notice that the zoned datetime still includes a time zone annotation. But
129
/// since there is no time zone identifier, the offset instead is repeated as
130
/// an additional assertion that a fixed offset datetime was intended.
131
#[derive(Clone, Copy, Eq, Hash, PartialEq, PartialOrd, Ord)]
132
pub struct Offset {
133
    inner: JOffset,
134
}
135
136
impl Offset {
137
    /// The minimum possible time zone offset.
138
    ///
139
    /// This corresponds to the offset `-25:59:59`.
140
    pub const MIN: Offset = Offset { inner: JOffset::MIN };
141
142
    /// The maximum possible time zone offset.
143
    ///
144
    /// This corresponds to the offset `25:59:59`.
145
    pub const MAX: Offset = Offset { inner: JOffset::MAX };
146
147
    /// The offset corresponding to UTC. That is, no offset at all.
148
    ///
149
    /// This is defined to always be equivalent to `Offset::ZERO`, but it is
150
    /// semantically distinct. This ought to be used when UTC is desired
151
    /// specifically, while `Offset::ZERO` ought to be used when one wants to
152
    /// express "no offset." For example, when adding offsets, `Offset::ZERO`
153
    /// corresponds to the identity.
154
    pub const UTC: Offset = Offset { inner: JOffset::UTC };
155
156
    /// The offset corresponding to no offset at all.
157
    ///
158
    /// This is defined to always be equivalent to `Offset::UTC`, but it is
159
    /// semantically distinct. This ought to be used when a zero offset is
160
    /// desired specifically, while `Offset::UTC` ought to be used when one
161
    /// wants to express UTC. For example, when adding offsets, `Offset::ZERO`
162
    /// corresponds to the identity.
163
    pub const ZERO: Offset = Offset { inner: JOffset::UTC };
164
165
    /// Creates a new time zone offset in a `const` context from a given number
166
    /// of hours.
167
    ///
168
    /// Negative offsets correspond to time zones west of the prime meridian,
169
    /// while positive offsets correspond to time zones east of the prime
170
    /// meridian. Equivalently, in all cases, `civil-time - offset = UTC`.
171
    ///
172
    /// The fallible non-const version of this constructor is
173
    /// [`Offset::from_hours`].
174
    ///
175
    /// # Panics
176
    ///
177
    /// This routine panics when the given number of hours is out of range.
178
    /// Namely, `hours` must be in the range `-25..=25`.
179
    ///
180
    /// # Example
181
    ///
182
    /// ```
183
    /// use jiff::tz::Offset;
184
    ///
185
    /// let o = Offset::constant(-5);
186
    /// assert_eq!(o.seconds(), -18_000);
187
    /// let o = Offset::constant(5);
188
    /// assert_eq!(o.seconds(), 18_000);
189
    /// ```
190
    ///
191
    /// Alternatively, one can use the terser `jiff::tz::offset` free function:
192
    ///
193
    /// ```
194
    /// use jiff::tz;
195
    ///
196
    /// let o = tz::offset(-5);
197
    /// assert_eq!(o.seconds(), -18_000);
198
    /// let o = tz::offset(5);
199
    /// assert_eq!(o.seconds(), 18_000);
200
    /// ```
201
    #[inline]
202
0
    pub const fn constant(hours: i8) -> Offset {
203
0
        let hours = constant::unwrapr!(
204
0
            b::OffsetHours::checkc(hours as i64),
205
0
            "invalid time zone offset hours",
206
        );
207
0
        Offset::constant_seconds((hours as i32) * 60 * 60)
208
0
    }
209
210
    /// Creates a new time zone offset in a `const` context from a given number
211
    /// of seconds.
212
    ///
213
    /// Negative offsets correspond to time zones west of the prime meridian,
214
    /// while positive offsets correspond to time zones east of the prime
215
    /// meridian. Equivalently, in all cases, `civil-time - offset = UTC`.
216
    ///
217
    /// The fallible non-const version of this constructor is
218
    /// [`Offset::from_seconds`].
219
    ///
220
    /// # Panics
221
    ///
222
    /// This routine panics when the given number of seconds is out of range.
223
    /// The range corresponds to the offsets `-25:59:59..=25:59:59`. In units
224
    /// of seconds, that corresponds to `-93,599..=93,599`.
225
    ///
226
    /// # Example
227
    ///
228
    /// ```ignore
229
    /// use jiff::tz::Offset;
230
    ///
231
    /// let o = Offset::constant_seconds(-18_000);
232
    /// assert_eq!(o.seconds(), -18_000);
233
    /// let o = Offset::constant_seconds(18_000);
234
    /// assert_eq!(o.seconds(), 18_000);
235
    /// ```
236
    // This is currently unexported because I find the name too long and
237
    // very off-putting. I don't think non-hour offsets are used enough to
238
    // warrant its existence. And I think I'd rather `Offset::hms` be const and
239
    // exported instead of this monstrosity.
240
    #[inline]
241
0
    pub(crate) const fn constant_seconds(seconds: i32) -> Offset {
242
0
        let inner = constant::unwrapr!(
243
0
            JOffset::from_seconds(seconds),
244
0
            "invalid time zone offset seconds",
245
        );
246
0
        Offset { inner }
247
0
    }
248
249
    /// Creates a new time zone offset from a given number of hours.
250
    ///
251
    /// Negative offsets correspond to time zones west of the prime meridian,
252
    /// while positive offsets correspond to time zones east of the prime
253
    /// meridian. Equivalently, in all cases, `civil-time - offset = UTC`.
254
    ///
255
    /// # Errors
256
    ///
257
    /// This routine returns an error when the given number of hours is out of
258
    /// range. Namely, `hours` must be in the range `-25..=25`.
259
    ///
260
    /// # Example
261
    ///
262
    /// ```
263
    /// use jiff::tz::Offset;
264
    ///
265
    /// let o = Offset::from_hours(-5)?;
266
    /// assert_eq!(o.seconds(), -18_000);
267
    /// let o = Offset::from_hours(5)?;
268
    /// assert_eq!(o.seconds(), 18_000);
269
    ///
270
    /// # Ok::<(), Box<dyn std::error::Error>>(())
271
    /// ```
272
    #[inline]
273
0
    pub fn from_hours(hours: i8) -> Result<Offset, Error> {
274
0
        let inner = JOffset::from_hours(hours).map_err(Error::jcore_range)?;
275
0
        Ok(Offset { inner })
276
0
    }
277
278
    /// Creates a new time zone offset in a `const` context from a given number
279
    /// of seconds.
280
    ///
281
    /// Negative offsets correspond to time zones west of the prime meridian,
282
    /// while positive offsets correspond to time zones east of the prime
283
    /// meridian. Equivalently, in all cases, `civil-time - offset = UTC`.
284
    ///
285
    /// # Errors
286
    ///
287
    /// This routine returns an error when the given number of seconds is out
288
    /// of range. The range corresponds to the offsets `-25:59:59..=25:59:59`.
289
    /// In units of seconds, that corresponds to `-93,599..=93,599`.
290
    ///
291
    /// # Example
292
    ///
293
    /// ```
294
    /// use jiff::tz::Offset;
295
    ///
296
    /// let o = Offset::from_seconds(-18_000)?;
297
    /// assert_eq!(o.seconds(), -18_000);
298
    /// let o = Offset::from_seconds(18_000)?;
299
    /// assert_eq!(o.seconds(), 18_000);
300
    ///
301
    /// # Ok::<(), Box<dyn std::error::Error>>(())
302
    /// ```
303
    #[inline]
304
0
    pub fn from_seconds(seconds: i32) -> Result<Offset, Error> {
305
0
        let inner =
306
0
            JOffset::from_seconds(seconds).map_err(Error::jcore_range)?;
307
0
        Ok(Offset { inner })
308
0
    }
Unexecuted instantiation: <jiff::tz::offset::Offset>::from_seconds
Unexecuted instantiation: <jiff::tz::offset::Offset>::from_seconds
309
310
    /// Returns the total number of seconds in this offset.
311
    ///
312
    /// The value returned is guaranteed to represent an offset in the range
313
    /// `-25:59:59..=25:59:59`. Or more precisely, the value will be in units
314
    /// of seconds in the range `-93,599..=93,599`.
315
    ///
316
    /// Negative offsets correspond to time zones west of the prime meridian,
317
    /// while positive offsets correspond to time zones east of the prime
318
    /// meridian. Equivalently, in all cases, `civil-time - offset = UTC`.
319
    ///
320
    /// # Example
321
    ///
322
    /// ```
323
    /// use jiff::tz;
324
    ///
325
    /// let o = tz::offset(-5);
326
    /// assert_eq!(o.seconds(), -18_000);
327
    /// let o = tz::offset(5);
328
    /// assert_eq!(o.seconds(), 18_000);
329
    /// ```
330
    #[inline]
331
0
    pub const fn seconds(self) -> i32 {
332
0
        self.inner.seconds()
333
0
    }
Unexecuted instantiation: <jiff::tz::offset::Offset>::seconds
Unexecuted instantiation: <jiff::tz::offset::Offset>::seconds
334
335
    /// Returns the negation of this offset.
336
    ///
337
    /// A negative offset will become positive and vice versa. This is a no-op
338
    /// if the offset is zero.
339
    ///
340
    /// This never panics.
341
    ///
342
    /// # Example
343
    ///
344
    /// ```
345
    /// use jiff::tz;
346
    ///
347
    /// assert_eq!(tz::offset(-5).negate(), tz::offset(5));
348
    /// // It's also available via the `-` operator:
349
    /// assert_eq!(-tz::offset(-5), tz::offset(5));
350
    /// ```
351
0
    pub fn negate(self) -> Offset {
352
0
        let inner = self.inner.negate();
353
0
        Offset { inner }
354
0
    }
355
356
    /// Returns the "sign number" or "signum" of this offset.
357
    ///
358
    /// The number returned is `-1` when this offset is negative,
359
    /// `0` when this offset is zero and `1` when this span is positive.
360
    ///
361
    /// # Example
362
    ///
363
    /// ```
364
    /// use jiff::tz;
365
    ///
366
    /// assert_eq!(tz::offset(5).signum(), 1);
367
    /// assert_eq!(tz::offset(0).signum(), 0);
368
    /// assert_eq!(tz::offset(-5).signum(), -1);
369
    /// ```
370
    #[inline]
371
0
    pub fn signum(self) -> i8 {
372
0
        self.inner.signum()
373
0
    }
374
375
    /// Returns true if and only if this offset is positive.
376
    ///
377
    /// This returns false when the offset is zero or negative.
378
    ///
379
    /// # Example
380
    ///
381
    /// ```
382
    /// use jiff::tz;
383
    ///
384
    /// assert!(tz::offset(5).is_positive());
385
    /// assert!(!tz::offset(0).is_positive());
386
    /// assert!(!tz::offset(-5).is_positive());
387
    /// ```
388
0
    pub fn is_positive(self) -> bool {
389
0
        self.inner.is_positive()
390
0
    }
391
392
    /// Returns true if and only if this offset is less than zero.
393
    ///
394
    /// # Example
395
    ///
396
    /// ```
397
    /// use jiff::tz;
398
    ///
399
    /// assert!(!tz::offset(5).is_negative());
400
    /// assert!(!tz::offset(0).is_negative());
401
    /// assert!(tz::offset(-5).is_negative());
402
    /// ```
403
0
    pub fn is_negative(self) -> bool {
404
0
        self.inner.is_negative()
405
0
    }
406
407
    /// Returns true if and only if this offset is zero.
408
    ///
409
    /// Or equivalently, when this offset corresponds to [`Offset::UTC`].
410
    ///
411
    /// # Example
412
    ///
413
    /// ```
414
    /// use jiff::tz;
415
    ///
416
    /// assert!(!tz::offset(5).is_zero());
417
    /// assert!(tz::offset(0).is_zero());
418
    /// assert!(!tz::offset(-5).is_zero());
419
    /// ```
420
0
    pub fn is_zero(self) -> bool {
421
0
        self.inner.is_zero()
422
0
    }
423
424
    /// Converts this offset into a [`TimeZone`].
425
    ///
426
    /// This is a convenience function for calling [`TimeZone::fixed`] with
427
    /// this offset.
428
    ///
429
    /// # Example
430
    ///
431
    /// ```
432
    /// use jiff::tz::offset;
433
    ///
434
    /// let tz = offset(-4).to_time_zone();
435
    /// assert_eq!(
436
    ///     tz.to_datetime(jiff::Timestamp::UNIX_EPOCH).to_string(),
437
    ///     "1969-12-31T20:00:00",
438
    /// );
439
    /// ```
440
0
    pub fn to_time_zone(self) -> TimeZone {
441
0
        TimeZone::fixed(self)
442
0
    }
443
444
    /// Converts the given timestamp to a civil datetime using this offset.
445
    ///
446
    /// # Example
447
    ///
448
    /// ```
449
    /// use jiff::{civil::date, tz, Timestamp};
450
    ///
451
    /// assert_eq!(
452
    ///     tz::offset(-8).to_datetime(Timestamp::UNIX_EPOCH),
453
    ///     date(1969, 12, 31).at(16, 0, 0, 0),
454
    /// );
455
    /// ```
456
    #[inline]
457
0
    pub fn to_datetime(self, timestamp: Timestamp) -> civil::DateTime {
458
0
        civil::DateTime::from_jcore(
459
0
            self.inner.to_datetime(timestamp.to_jcore()),
460
        )
461
0
    }
Unexecuted instantiation: <jiff::tz::offset::Offset>::to_datetime
Unexecuted instantiation: <jiff::tz::offset::Offset>::to_datetime
Unexecuted instantiation: <jiff::tz::offset::Offset>::to_datetime
462
463
    /// Converts the given civil datetime to a timestamp using this offset.
464
    ///
465
    /// # Errors
466
    ///
467
    /// This returns an error if this would have returned a timestamp outside
468
    /// of its minimum and maximum values.
469
    ///
470
    /// # Example
471
    ///
472
    /// This example shows how to find the timestamp corresponding to
473
    /// `1969-12-31T16:00:00-08`.
474
    ///
475
    /// ```
476
    /// use jiff::{civil::date, tz, Timestamp};
477
    ///
478
    /// assert_eq!(
479
    ///     tz::offset(-8).to_timestamp(date(1969, 12, 31).at(16, 0, 0, 0))?,
480
    ///     Timestamp::UNIX_EPOCH,
481
    /// );
482
    /// # Ok::<(), Box<dyn std::error::Error>>(())
483
    /// ```
484
    ///
485
    /// This example shows some maximum boundary conditions where this routine
486
    /// will fail:
487
    ///
488
    /// ```
489
    /// use jiff::{civil::date, tz, Timestamp, ToSpan};
490
    ///
491
    /// let dt = date(9999, 12, 31).at(23, 0, 0, 0);
492
    /// assert!(tz::offset(-8).to_timestamp(dt).is_err());
493
    ///
494
    /// // If the offset is big enough, then converting it to a UTC
495
    /// // timestamp will fit, even when using the maximum civil datetime.
496
    /// let dt = date(9999, 12, 31).at(23, 59, 59, 999_999_999);
497
    /// assert_eq!(tz::Offset::MAX.to_timestamp(dt).unwrap(), Timestamp::MAX);
498
    /// // But adjust the offset down 1 second is enough to go out-of-bounds.
499
    /// assert!((tz::Offset::MAX - 1.seconds()).to_timestamp(dt).is_err());
500
    /// ```
501
    ///
502
    /// Same as above, but for minimum values:
503
    ///
504
    /// ```
505
    /// use jiff::{civil::date, tz, Timestamp, ToSpan};
506
    ///
507
    /// let dt = date(-9999, 1, 1).at(1, 0, 0, 0);
508
    /// assert!(tz::offset(8).to_timestamp(dt).is_err());
509
    ///
510
    /// // If the offset is small enough, then converting it to a UTC
511
    /// // timestamp will fit, even when using the minimum civil datetime.
512
    /// let dt = date(-9999, 1, 1).at(0, 0, 0, 0);
513
    /// assert_eq!(tz::Offset::MIN.to_timestamp(dt).unwrap(), Timestamp::MIN);
514
    /// // But adjust the offset up 1 second is enough to go out-of-bounds.
515
    /// assert!((tz::Offset::MIN + 1.seconds()).to_timestamp(dt).is_err());
516
    /// ```
517
    #[inline]
518
0
    pub fn to_timestamp(
519
0
        self,
520
0
        dt: civil::DateTime,
521
0
    ) -> Result<Timestamp, Error> {
522
0
        Ok(Timestamp::from_jcore(
523
0
            self.inner
524
0
                .to_timestamp(dt.to_jcore())
525
0
                .context(E::ConvertDateTimeToTimestamp { offset: self })?,
526
        ))
527
0
    }
Unexecuted instantiation: <jiff::tz::offset::Offset>::to_timestamp
Unexecuted instantiation: <jiff::tz::offset::Offset>::to_timestamp
528
529
    /// Adds the given span of time to this offset.
530
    ///
531
    /// Since time zone offsets have second resolution, any fractional seconds
532
    /// in the duration given are ignored.
533
    ///
534
    /// This operation accepts three different duration types: [`Span`],
535
    /// [`SignedDuration`] or [`std::time::Duration`]. This is achieved via
536
    /// `From` trait implementations for the [`OffsetArithmetic`] type.
537
    ///
538
    /// # Errors
539
    ///
540
    /// This returns an error if the result of adding the given span would
541
    /// exceed the minimum or maximum allowed `Offset` value.
542
    ///
543
    /// This also returns an error if the span given contains any non-zero
544
    /// units bigger than hours.
545
    ///
546
    /// # Example
547
    ///
548
    /// This example shows how to add one hour to an offset (if the offset
549
    /// corresponds to standard time, then adding an hour will usually give
550
    /// you DST time):
551
    ///
552
    /// ```
553
    /// use jiff::{tz, ToSpan};
554
    ///
555
    /// let off = tz::offset(-5);
556
    /// assert_eq!(off.checked_add(1.hours()).unwrap(), tz::offset(-4));
557
    /// ```
558
    ///
559
    /// And note that while fractional seconds are ignored, units less than
560
    /// seconds aren't ignored if they sum up to a duration at least as big
561
    /// as one second:
562
    ///
563
    /// ```
564
    /// use jiff::{tz, ToSpan};
565
    ///
566
    /// let off = tz::offset(5);
567
    /// let span = 900.milliseconds()
568
    ///     .microseconds(50_000)
569
    ///     .nanoseconds(50_000_000);
570
    /// assert_eq!(
571
    ///     off.checked_add(span).unwrap(),
572
    ///     tz::Offset::from_seconds((5 * 60 * 60) + 1).unwrap(),
573
    /// );
574
    /// // Any leftover fractional part is ignored.
575
    /// let span = 901.milliseconds()
576
    ///     .microseconds(50_001)
577
    ///     .nanoseconds(50_000_001);
578
    /// assert_eq!(
579
    ///     off.checked_add(span).unwrap(),
580
    ///     tz::Offset::from_seconds((5 * 60 * 60) + 1).unwrap(),
581
    /// );
582
    /// ```
583
    ///
584
    /// This example shows some cases where checked addition will fail.
585
    ///
586
    /// ```
587
    /// use jiff::{tz::Offset, ToSpan};
588
    ///
589
    /// // Adding units above 'hour' always results in an error.
590
    /// assert!(Offset::UTC.checked_add(1.day()).is_err());
591
    /// assert!(Offset::UTC.checked_add(1.week()).is_err());
592
    /// assert!(Offset::UTC.checked_add(1.month()).is_err());
593
    /// assert!(Offset::UTC.checked_add(1.year()).is_err());
594
    ///
595
    /// // Adding even 1 second to the max, or subtracting 1 from the min,
596
    /// // will result in overflow and thus an error will be returned.
597
    /// assert!(Offset::MIN.checked_add(-1.seconds()).is_err());
598
    /// assert!(Offset::MAX.checked_add(1.seconds()).is_err());
599
    /// ```
600
    ///
601
    /// # Example: adding absolute durations
602
    ///
603
    /// This shows how to add signed and unsigned absolute durations to an
604
    /// `Offset`. Like with `Span`s, any fractional seconds are ignored.
605
    ///
606
    /// ```
607
    /// use std::time::Duration;
608
    ///
609
    /// use jiff::{tz::offset, SignedDuration};
610
    ///
611
    /// let off = offset(-10);
612
    ///
613
    /// let dur = SignedDuration::from_hours(11);
614
    /// assert_eq!(off.checked_add(dur)?, offset(1));
615
    /// assert_eq!(off.checked_add(-dur)?, offset(-21));
616
    ///
617
    /// // Any leftover time is truncated. That is, only
618
    /// // whole seconds from the duration are considered.
619
    /// let dur = Duration::new(3 * 60 * 60, 999_999_999);
620
    /// assert_eq!(off.checked_add(dur)?, offset(-7));
621
    ///
622
    /// # Ok::<(), Box<dyn std::error::Error>>(())
623
    /// ```
624
    #[inline]
625
0
    pub fn checked_add<A: Into<OffsetArithmetic>>(
626
0
        self,
627
0
        duration: A,
628
0
    ) -> Result<Offset, Error> {
629
0
        let duration: OffsetArithmetic = duration.into();
630
0
        duration.checked_add(self)
631
0
    }
632
633
    #[inline]
634
0
    fn checked_add_span(self, span: &Span) -> Result<Offset, Error> {
635
0
        if let Some(err) = span.smallest_non_time_non_zero_unit_error() {
636
0
            return Err(err);
637
0
        }
638
639
0
        let span = b::OffsetTotalSeconds::check(
640
0
            span.to_invariant_duration().as_secs(),
641
0
        )?;
642
        // No overflow is possible here because even `Offset::MIN +
643
        // Offset::MIN` fits into an `i32`. And note that the number of seconds
644
        // in the span is limited to the range supported by `Offset`.
645
0
        Offset::from_seconds(span + self.seconds())
646
0
    }
647
648
    #[inline]
649
0
    fn checked_add_duration(
650
0
        self,
651
0
        duration: SignedDuration,
652
0
    ) -> Result<Offset, Error> {
653
0
        let duration = b::OffsetTotalSeconds::check(duration.as_secs())
654
0
            .context(E::OverflowAddSignedDuration)?;
655
0
        Offset::from_seconds(duration + self.seconds())
656
0
    }
657
658
    /// This routine is identical to [`Offset::checked_add`] with the duration
659
    /// negated.
660
    ///
661
    /// # Errors
662
    ///
663
    /// This has the same error conditions as [`Offset::checked_add`].
664
    ///
665
    /// # Example
666
    ///
667
    /// ```
668
    /// use std::time::Duration;
669
    ///
670
    /// use jiff::{tz, SignedDuration, ToSpan};
671
    ///
672
    /// let off = tz::offset(-4);
673
    /// assert_eq!(
674
    ///     off.checked_sub(1.hours())?,
675
    ///     tz::offset(-5),
676
    /// );
677
    /// assert_eq!(
678
    ///     off.checked_sub(SignedDuration::from_hours(1))?,
679
    ///     tz::offset(-5),
680
    /// );
681
    /// assert_eq!(
682
    ///     off.checked_sub(Duration::from_secs(60 * 60))?,
683
    ///     tz::offset(-5),
684
    /// );
685
    ///
686
    /// # Ok::<(), Box<dyn std::error::Error>>(())
687
    /// ```
688
    #[inline]
689
0
    pub fn checked_sub<A: Into<OffsetArithmetic>>(
690
0
        self,
691
0
        duration: A,
692
0
    ) -> Result<Offset, Error> {
693
0
        let duration: OffsetArithmetic = duration.into();
694
0
        duration.checked_neg().and_then(|oa| oa.checked_add(self))
695
0
    }
696
697
    /// This routine is identical to [`Offset::checked_add`], except the
698
    /// result saturates on overflow. That is, instead of overflow, either
699
    /// [`Offset::MIN`] or [`Offset::MAX`] is returned.
700
    ///
701
    /// # Example
702
    ///
703
    /// This example shows some cases where saturation will occur.
704
    ///
705
    /// ```
706
    /// use jiff::{tz::Offset, SignedDuration, ToSpan};
707
    ///
708
    /// // Adding units above 'day' always results in saturation.
709
    /// assert_eq!(Offset::UTC.saturating_add(1.weeks()), Offset::MAX);
710
    /// assert_eq!(Offset::UTC.saturating_add(1.months()), Offset::MAX);
711
    /// assert_eq!(Offset::UTC.saturating_add(1.years()), Offset::MAX);
712
    ///
713
    /// // Adding even 1 second to the max, or subtracting 1 from the min,
714
    /// // will result in saturationg.
715
    /// assert_eq!(Offset::MIN.saturating_add(-1.seconds()), Offset::MIN);
716
    /// assert_eq!(Offset::MAX.saturating_add(1.seconds()), Offset::MAX);
717
    ///
718
    /// // Adding absolute durations also saturates as expected.
719
    /// assert_eq!(Offset::UTC.saturating_add(SignedDuration::MAX), Offset::MAX);
720
    /// assert_eq!(Offset::UTC.saturating_add(SignedDuration::MIN), Offset::MIN);
721
    /// assert_eq!(Offset::UTC.saturating_add(std::time::Duration::MAX), Offset::MAX);
722
    /// ```
723
    #[inline]
724
0
    pub fn saturating_add<A: Into<OffsetArithmetic>>(
725
0
        self,
726
0
        duration: A,
727
0
    ) -> Offset {
728
0
        let duration: OffsetArithmetic = duration.into();
729
0
        self.checked_add(duration).unwrap_or_else(|_| {
730
0
            if duration.is_negative() {
731
0
                Offset::MIN
732
            } else {
733
0
                Offset::MAX
734
            }
735
0
        })
736
0
    }
737
738
    /// This routine is identical to [`Offset::saturating_add`] with the span
739
    /// parameter negated.
740
    ///
741
    /// # Example
742
    ///
743
    /// This example shows some cases where saturation will occur.
744
    ///
745
    /// ```
746
    /// use jiff::{tz::Offset, SignedDuration, ToSpan};
747
    ///
748
    /// // Adding units above 'day' always results in saturation.
749
    /// assert_eq!(Offset::UTC.saturating_sub(1.weeks()), Offset::MIN);
750
    /// assert_eq!(Offset::UTC.saturating_sub(1.months()), Offset::MIN);
751
    /// assert_eq!(Offset::UTC.saturating_sub(1.years()), Offset::MIN);
752
    ///
753
    /// // Adding even 1 second to the max, or subtracting 1 from the min,
754
    /// // will result in saturationg.
755
    /// assert_eq!(Offset::MIN.saturating_sub(1.seconds()), Offset::MIN);
756
    /// assert_eq!(Offset::MAX.saturating_sub(-1.seconds()), Offset::MAX);
757
    ///
758
    /// // Adding absolute durations also saturates as expected.
759
    /// assert_eq!(Offset::UTC.saturating_sub(SignedDuration::MAX), Offset::MIN);
760
    /// assert_eq!(Offset::UTC.saturating_sub(SignedDuration::MIN), Offset::MAX);
761
    /// assert_eq!(Offset::UTC.saturating_sub(std::time::Duration::MAX), Offset::MIN);
762
    /// ```
763
    #[inline]
764
0
    pub fn saturating_sub<A: Into<OffsetArithmetic>>(
765
0
        self,
766
0
        duration: A,
767
0
    ) -> Offset {
768
0
        let duration: OffsetArithmetic = duration.into();
769
0
        let Ok(duration) = duration.checked_neg() else { return Offset::MIN };
770
0
        self.saturating_add(duration)
771
0
    }
772
773
    /// Returns the span of time from this offset until the other given.
774
    ///
775
    /// When the `other` offset is more west (i.e., more negative) of the prime
776
    /// meridian than this offset, then the span returned will be negative.
777
    ///
778
    /// # Properties
779
    ///
780
    /// Adding the span returned to this offset will always equal the `other`
781
    /// offset given.
782
    ///
783
    /// # Examples
784
    ///
785
    /// ```
786
    /// use jiff::{tz, ToSpan};
787
    ///
788
    /// assert_eq!(
789
    ///     tz::offset(-5).until(tz::Offset::UTC),
790
    ///     (5 * 60 * 60).seconds().fieldwise(),
791
    /// );
792
    /// // Flipping the operands in this case results in a negative span.
793
    /// assert_eq!(
794
    ///     tz::Offset::UTC.until(tz::offset(-5)),
795
    ///     -(5 * 60 * 60).seconds().fieldwise(),
796
    /// );
797
    /// // The maximum span you can get:
798
    /// assert_eq!(
799
    ///     tz::Offset::MIN.until(tz::Offset::MAX),
800
    ///     187_198.seconds().fieldwise(),
801
    /// );
802
    /// ```
803
    #[inline]
804
0
    pub fn until(self, other: Offset) -> Span {
805
        // OK because `Offset::MIN - Offset::MAX` will
806
        // never overflow `i32`.
807
0
        let diff = other.seconds() - self.seconds();
808
0
        Span::new().seconds(diff)
809
0
    }
810
811
    /// Returns the span of time since the other offset given from this offset.
812
    ///
813
    /// When the `other` is more east (i.e., more positive) of the prime
814
    /// meridian than this offset, then the span returned will be negative.
815
    ///
816
    /// # Properties
817
    ///
818
    /// Adding the span returned to the `other` offset will always equal this
819
    /// offset.
820
    ///
821
    /// # Examples
822
    ///
823
    /// ```
824
    /// use jiff::{tz, ToSpan};
825
    ///
826
    /// assert_eq!(
827
    ///     tz::Offset::UTC.since(tz::offset(-5)),
828
    ///     (5 * 60 * 60).seconds().fieldwise(),
829
    /// );
830
    /// // Flipping the operands in this case results in a negative span.
831
    /// assert_eq!(
832
    ///     tz::offset(-5).since(tz::Offset::UTC),
833
    ///     -(5 * 60 * 60).seconds().fieldwise(),
834
    /// );
835
    /// ```
836
    #[inline]
837
0
    pub fn since(self, other: Offset) -> Span {
838
0
        self.until(other).negate()
839
0
    }
840
841
    /// Returns an absolute duration representing the difference in time from
842
    /// this offset until the given `other` offset.
843
    ///
844
    /// When the `other` offset is more west (i.e., more negative) of the prime
845
    /// meridian than this offset, then the duration returned will be negative.
846
    ///
847
    /// Unlike [`Offset::until`], this returns a duration corresponding to a
848
    /// 96-bit integer of nanoseconds between two offsets.
849
    ///
850
    /// # When should I use this versus [`Offset::until`]?
851
    ///
852
    /// See the type documentation for [`SignedDuration`] for the section on
853
    /// when one should use [`Span`] and when one should use `SignedDuration`.
854
    /// In short, use `Span` (and therefore `Offset::until`) unless you have a
855
    /// specific reason to do otherwise.
856
    ///
857
    /// # Examples
858
    ///
859
    /// ```
860
    /// use jiff::{tz, SignedDuration};
861
    ///
862
    /// assert_eq!(
863
    ///     tz::offset(-5).duration_until(tz::Offset::UTC),
864
    ///     SignedDuration::from_hours(5),
865
    /// );
866
    /// // Flipping the operands in this case results in a negative span.
867
    /// assert_eq!(
868
    ///     tz::Offset::UTC.duration_until(tz::offset(-5)),
869
    ///     SignedDuration::from_hours(-5),
870
    /// );
871
    /// ```
872
    #[inline]
873
0
    pub fn duration_until(self, other: Offset) -> SignedDuration {
874
0
        SignedDuration::offset_until(self, other)
875
0
    }
876
877
    /// This routine is identical to [`Offset::duration_until`], but the order
878
    /// of the parameters is flipped.
879
    ///
880
    /// # Examples
881
    ///
882
    /// ```
883
    /// use jiff::{tz, SignedDuration};
884
    ///
885
    /// assert_eq!(
886
    ///     tz::Offset::UTC.duration_since(tz::offset(-5)),
887
    ///     SignedDuration::from_hours(5),
888
    /// );
889
    /// assert_eq!(
890
    ///     tz::offset(-5).duration_since(tz::Offset::UTC),
891
    ///     SignedDuration::from_hours(-5),
892
    /// );
893
    /// ```
894
    #[inline]
895
0
    pub fn duration_since(self, other: Offset) -> SignedDuration {
896
0
        SignedDuration::offset_until(other, self)
897
0
    }
898
899
    /// Returns a new offset that is rounded according to the given
900
    /// configuration.
901
    ///
902
    /// Rounding an offset has a number of parameters, all of which are
903
    /// optional. When no parameters are given, then no rounding is done, and
904
    /// the offset as given is returned. That is, it's a no-op.
905
    ///
906
    /// As is consistent with `Offset` itself, rounding only supports units of
907
    /// hours, minutes or seconds. If any other unit is provided, then an error
908
    /// is returned.
909
    ///
910
    /// The parameters are, in brief:
911
    ///
912
    /// * [`OffsetRound::smallest`] sets the smallest [`Unit`] that is allowed
913
    /// to be non-zero in the offset returned. By default, it is set to
914
    /// [`Unit::Second`], i.e., no rounding occurs. When the smallest unit is
915
    /// set to something bigger than seconds, then the non-zero units in the
916
    /// offset smaller than the smallest unit are used to determine how the
917
    /// offset should be rounded. For example, rounding `+01:59` to the nearest
918
    /// hour using the default rounding mode would produce `+02:00`.
919
    /// * [`OffsetRound::mode`] determines how to handle the remainder
920
    /// when rounding. The default is [`RoundMode::HalfExpand`], which
921
    /// corresponds to how you were likely taught to round in school.
922
    /// Alternative modes, like [`RoundMode::Trunc`], exist too. For example,
923
    /// a truncating rounding of `+01:59` to the nearest hour would
924
    /// produce `+01:00`.
925
    /// * [`OffsetRound::increment`] sets the rounding granularity to
926
    /// use for the configured smallest unit. For example, if the smallest unit
927
    /// is minutes and the increment is `15`, then the offset returned will
928
    /// always have its minute component set to a multiple of `15`.
929
    ///
930
    /// # Errors
931
    ///
932
    /// In general, there are two main ways for rounding to fail: an improper
933
    /// configuration like trying to round an offset to the nearest unit other
934
    /// than hours/minutes/seconds, or when overflow occurs. Overflow can occur
935
    /// when the offset would exceed the minimum or maximum `Offset` values.
936
    /// Typically, this can only realistically happen if the offset before
937
    /// rounding is already close to its minimum or maximum value.
938
    ///
939
    /// # Example: rounding to the nearest multiple of 15 minutes
940
    ///
941
    /// Most time zone offsets fall on an hour boundary, but some fall on the
942
    /// half-hour or even 15 minute boundary:
943
    ///
944
    /// ```
945
    /// use jiff::{tz::Offset, Unit};
946
    ///
947
    /// let offset = Offset::from_seconds(-(44 * 60 + 30)).unwrap();
948
    /// let rounded = offset.round((Unit::Minute, 15))?;
949
    /// assert_eq!(rounded, Offset::from_seconds(-45 * 60).unwrap());
950
    ///
951
    /// # Ok::<(), Box<dyn std::error::Error>>(())
952
    /// ```
953
    ///
954
    /// # Example: rounding can fail via overflow
955
    ///
956
    /// ```
957
    /// use jiff::{tz::Offset, Unit};
958
    ///
959
    /// assert_eq!(Offset::MAX.to_string(), "+25:59:59");
960
    /// assert_eq!(
961
    ///     Offset::MAX.round(Unit::Minute).unwrap_err().to_string(),
962
    ///     "rounding time zone offset resulted in a duration that overflows: \
963
    ///      parameter 'time zone offset total seconds' is not \
964
    ///      in the required range of -93599..=93599",
965
    /// );
966
    /// ```
967
    #[inline]
968
0
    pub fn round<R: Into<OffsetRound>>(
969
0
        self,
970
0
        options: R,
971
0
    ) -> Result<Offset, Error> {
972
0
        let options: OffsetRound = options.into();
973
0
        options.round(self)
974
0
    }
975
}
976
977
impl Offset {
978
    /// This creates an `Offset` via hours/minutes/seconds components.
979
    ///
980
    /// Currently, it exists because it's convenient for use in tests.
981
    ///
982
    /// I originally wanted to expose this in the public API, but I couldn't
983
    /// decide on how I wanted to treat signedness. There are a variety of
984
    /// choices:
985
    ///
986
    /// * Require all values to be positive, and ask the caller to use
987
    /// `-offset` to negate it.
988
    /// * Require all values to have the same sign. If any differs, either
989
    /// panic or return an error.
990
    /// * If any have a negative sign, then behave as if all have a negative
991
    /// sign.
992
    /// * Permit any combination of sign and combine them correctly.
993
    /// Similar to how `std::time::Duration::new(-1s, 1ns)` is turned into
994
    /// `-999,999,999ns`.
995
    ///
996
    /// I think the last option is probably the right behavior, but also the
997
    /// most annoying to implement. But if someone wants to take a crack at it,
998
    /// a PR is welcome.
999
    #[cfg(test)]
1000
    #[inline]
1001
    pub(crate) const fn hms(hours: i8, minutes: i8, seconds: i8) -> Offset {
1002
        let hours = constant::unwrapr!(
1003
            b::OffsetHours::checkc(hours as i64),
1004
            "invalid time zone offset hours",
1005
        );
1006
        let minutes = constant::unwrapr!(
1007
            b::OffsetMinutes::checkc(minutes as i64),
1008
            "invalid time zone offset minutes",
1009
        );
1010
        let seconds = constant::unwrapr!(
1011
            b::OffsetSeconds::checkc(seconds as i64),
1012
            "invalid time zone offset seconds",
1013
        );
1014
        let seconds = (hours as i32 * c::SECS_PER_HOUR_32)
1015
            + (minutes as i32 * c::SECS_PER_MIN_32)
1016
            + (seconds as i32);
1017
        let inner =
1018
            constant::unwrapr!(JOffset::from_seconds(seconds), "valid offset");
1019
        Offset { inner }
1020
    }
1021
1022
    #[inline]
1023
0
    pub(crate) fn part_hours(self) -> i8 {
1024
0
        (self.seconds() / c::SECS_PER_HOUR_32) as i8
1025
0
    }
1026
1027
    #[inline]
1028
0
    pub(crate) fn part_minutes(self) -> i8 {
1029
0
        ((self.seconds() / c::SECS_PER_MIN_32) % c::MINS_PER_HOUR_32) as i8
1030
0
    }
1031
1032
    #[inline]
1033
0
    pub(crate) fn part_seconds(self) -> i8 {
1034
0
        (self.seconds() % c::SECS_PER_MIN_32) as i8
1035
0
    }
1036
1037
    #[inline]
1038
0
    pub(crate) const fn from_jcore(offset: JOffset) -> Offset {
1039
0
        Offset { inner: offset }
1040
0
    }
Unexecuted instantiation: <jiff::tz::offset::Offset>::from_jcore
Unexecuted instantiation: <jiff::tz::offset::Offset>::from_jcore
Unexecuted instantiation: <jiff::tz::offset::Offset>::from_jcore
Unexecuted instantiation: <jiff::tz::offset::Offset>::from_jcore
1041
1042
    #[inline]
1043
0
    pub(crate) const fn from_seconds_unchecked(seconds: i32) -> Offset {
1044
        // TODO: Benchmark whether the check here is hurting us. If it is,
1045
        // then we'll need a safety boundary in jiff-core to support this
1046
        // operation.
1047
0
        let inner =
1048
0
            constant::unwrapr!(JOffset::from_seconds(seconds), "valid offset");
1049
0
        Offset { inner }
1050
0
    }
Unexecuted instantiation: <jiff::tz::offset::Offset>::from_seconds_unchecked
Unexecuted instantiation: <jiff::tz::offset::Offset>::from_seconds_unchecked
Unexecuted instantiation: <jiff::tz::offset::Offset>::from_seconds_unchecked
Unexecuted instantiation: <jiff::tz::offset::Offset>::from_seconds_unchecked
1051
1052
    #[inline]
1053
0
    pub(crate) fn to_abbreviation(&self) -> jcore::tz::Abbreviation {
1054
        use core::fmt::Write;
1055
1056
0
        let mut dst = jcore::util::ArrayStr::<9>::new("").unwrap();
1057
        // OK because the string representation of an offset
1058
        // can never exceed 9 bytes. The longest possible, e.g.,
1059
        // is `-25:59:59`.
1060
0
        write!(&mut dst, "{}", self).unwrap();
1061
        // The correctness argument here is unfortunately convuleted. In
1062
        // environments with `alloc`, this will always succeed because the
1063
        // heap is used as a fallback. But in core-only environments, the
1064
        // abbreviation capacity is specifically set to `9` in jiff-core to
1065
        // acommodate this use case. Thus, this can never fail.
1066
0
        jcore::tz::Abbreviation::new(dst.as_str())
1067
0
            .expect("`Abbreviation` capacity is big enough")
1068
0
    }
1069
1070
    /// Round this offset to the nearest minute and returns the hour/minute
1071
    /// components as unsigned integers.
1072
    ///
1073
    /// Generally speaking, the second component on an offset is always zero.
1074
    /// There are _some_ cases in the tzdb where this isn't true (like
1075
    /// `Africa/Monrovia` before `1972-01-07`), but virtually all time zones
1076
    /// use offsets with whole hours. Some go to whole minutes. The only other
1077
    /// way to get non-zero seconds is to explicitly use a fixed offset.
1078
    ///
1079
    /// A pathological case is the minimum or maximum offset. In this case,
1080
    /// truncation is used instead of rounding to the nearest whole minute.
1081
    #[inline]
1082
0
    pub(crate) fn round_to_nearest_minute(self) -> (u8, u8) {
1083
        #[inline(never)]
1084
        #[cold]
1085
0
        fn round(mut hours: u8, mut minutes: u8) -> (u8, u8) {
1086
            const MAX_HOURS: u8 = b::OffsetHours::MAX.unsigned_abs();
1087
            const MAX_MINS: u8 = b::OffsetMinutes::MAX.unsigned_abs();
1088
1089
0
            if minutes == 59 {
1090
0
                hours += 1;
1091
0
                minutes = 0;
1092
                // An edge case: if rounding results in an offset beyond
1093
                // Jiff's boundaries, then we truncate to the max (or min)
1094
                // offset supported.
1095
0
                if hours > MAX_HOURS {
1096
0
                    hours = MAX_HOURS;
1097
0
                    minutes = MAX_MINS;
1098
0
                }
1099
0
            } else {
1100
0
                minutes += 1;
1101
0
            }
1102
0
            (hours, minutes)
1103
0
        }
1104
1105
0
        let total_seconds = self.seconds().unsigned_abs();
1106
0
        let hours = (total_seconds / (60 * 60)) as u8;
1107
0
        let minutes = ((total_seconds / 60) % 60) as u8;
1108
0
        let seconds = (total_seconds % 60) as u8;
1109
1110
        // RFCs 2822, 3339 and 9557 require that time zone offsets are an
1111
        // integral number of minutes. While rounding based on seconds doesn't
1112
        // seem clearly indicated, the `1937-01-01T12:00:27.87+00:20` example
1113
        // in RFC 3339 seems to suggest that the number of minutes should be
1114
        // "as close as possible" to the actual offset. So we just do basic
1115
        // rounding here.
1116
0
        if seconds >= 30 {
1117
0
            return round(hours, minutes);
1118
0
        }
1119
0
        (hours, minutes)
1120
0
    }
1121
}
1122
1123
impl core::fmt::Debug for Offset {
1124
0
    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
1125
0
        let sign = if self.is_negative() { "-" } else { "" };
1126
0
        write!(
1127
0
            f,
1128
0
            "{sign}{:02}:{:02}:{:02}",
1129
0
            self.part_hours().unsigned_abs(),
1130
0
            self.part_minutes().unsigned_abs(),
1131
0
            self.part_seconds().unsigned_abs(),
1132
        )
1133
0
    }
1134
}
1135
1136
impl core::fmt::Display for Offset {
1137
0
    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
1138
0
        let sign = if self.is_negative() { "-" } else { "+" };
1139
0
        let hours = self.part_hours().unsigned_abs();
1140
0
        let minutes = self.part_minutes().unsigned_abs();
1141
0
        let seconds = self.part_seconds().unsigned_abs();
1142
0
        if hours == 0 && minutes == 0 && seconds == 0 {
1143
0
            f.write_str("+00")
1144
0
        } else if hours != 0 && minutes == 0 && seconds == 0 {
1145
0
            write!(f, "{sign}{hours:02}")
1146
0
        } else if minutes != 0 && seconds == 0 {
1147
0
            write!(f, "{sign}{hours:02}:{minutes:02}")
1148
        } else {
1149
0
            write!(f, "{sign}{hours:02}:{minutes:02}:{seconds:02}")
1150
        }
1151
0
    }
1152
}
1153
1154
/// Adds a span of time to an offset. This panics on overflow.
1155
///
1156
/// For checked arithmetic, see [`Offset::checked_add`].
1157
impl Add<Span> for Offset {
1158
    type Output = Offset;
1159
1160
    #[inline]
1161
0
    fn add(self, rhs: Span) -> Offset {
1162
0
        self.checked_add(rhs)
1163
0
            .expect("adding span to offset should not overflow")
1164
0
    }
1165
}
1166
1167
/// Adds a span of time to an offset in place. This panics on overflow.
1168
///
1169
/// For checked arithmetic, see [`Offset::checked_add`].
1170
impl AddAssign<Span> for Offset {
1171
    #[inline]
1172
0
    fn add_assign(&mut self, rhs: Span) {
1173
0
        *self = self.add(rhs);
1174
0
    }
1175
}
1176
1177
/// Subtracts a span of time from an offset. This panics on overflow.
1178
///
1179
/// For checked arithmetic, see [`Offset::checked_sub`].
1180
impl Sub<Span> for Offset {
1181
    type Output = Offset;
1182
1183
    #[inline]
1184
0
    fn sub(self, rhs: Span) -> Offset {
1185
0
        self.checked_sub(rhs)
1186
0
            .expect("subtracting span from offsetsshould not overflow")
1187
0
    }
1188
}
1189
1190
/// Subtracts a span of time from an offset in place. This panics on overflow.
1191
///
1192
/// For checked arithmetic, see [`Offset::checked_sub`].
1193
impl SubAssign<Span> for Offset {
1194
    #[inline]
1195
0
    fn sub_assign(&mut self, rhs: Span) {
1196
0
        *self = self.sub(rhs);
1197
0
    }
1198
}
1199
1200
/// Computes the span of time between two offsets.
1201
///
1202
/// This will return a negative span when the offset being subtracted is
1203
/// greater (i.e., more east with respect to the prime meridian).
1204
impl Sub for Offset {
1205
    type Output = Span;
1206
1207
    #[inline]
1208
0
    fn sub(self, rhs: Offset) -> Span {
1209
0
        self.since(rhs)
1210
0
    }
1211
}
1212
1213
/// Adds a signed duration of time to an offset. This panics on overflow.
1214
///
1215
/// For checked arithmetic, see [`Offset::checked_add`].
1216
impl Add<SignedDuration> for Offset {
1217
    type Output = Offset;
1218
1219
    #[inline]
1220
0
    fn add(self, rhs: SignedDuration) -> Offset {
1221
0
        self.checked_add(rhs)
1222
0
            .expect("adding signed duration to offset should not overflow")
1223
0
    }
1224
}
1225
1226
/// Adds a signed duration of time to an offset in place. This panics on
1227
/// overflow.
1228
///
1229
/// For checked arithmetic, see [`Offset::checked_add`].
1230
impl AddAssign<SignedDuration> for Offset {
1231
    #[inline]
1232
0
    fn add_assign(&mut self, rhs: SignedDuration) {
1233
0
        *self = self.add(rhs);
1234
0
    }
1235
}
1236
1237
/// Subtracts a signed duration of time from an offset. This panics on
1238
/// overflow.
1239
///
1240
/// For checked arithmetic, see [`Offset::checked_sub`].
1241
impl Sub<SignedDuration> for Offset {
1242
    type Output = Offset;
1243
1244
    #[inline]
1245
0
    fn sub(self, rhs: SignedDuration) -> Offset {
1246
0
        self.checked_sub(rhs).expect(
1247
0
            "subtracting signed duration from offsetsshould not overflow",
1248
        )
1249
0
    }
1250
}
1251
1252
/// Subtracts a signed duration of time from an offset in place. This panics on
1253
/// overflow.
1254
///
1255
/// For checked arithmetic, see [`Offset::checked_sub`].
1256
impl SubAssign<SignedDuration> for Offset {
1257
    #[inline]
1258
0
    fn sub_assign(&mut self, rhs: SignedDuration) {
1259
0
        *self = self.sub(rhs);
1260
0
    }
1261
}
1262
1263
/// Adds an unsigned duration of time to an offset. This panics on overflow.
1264
///
1265
/// For checked arithmetic, see [`Offset::checked_add`].
1266
impl Add<UnsignedDuration> for Offset {
1267
    type Output = Offset;
1268
1269
    #[inline]
1270
0
    fn add(self, rhs: UnsignedDuration) -> Offset {
1271
0
        self.checked_add(rhs)
1272
0
            .expect("adding unsigned duration to offset should not overflow")
1273
0
    }
1274
}
1275
1276
/// Adds an unsigned duration of time to an offset in place. This panics on
1277
/// overflow.
1278
///
1279
/// For checked arithmetic, see [`Offset::checked_add`].
1280
impl AddAssign<UnsignedDuration> for Offset {
1281
    #[inline]
1282
0
    fn add_assign(&mut self, rhs: UnsignedDuration) {
1283
0
        *self = self.add(rhs);
1284
0
    }
1285
}
1286
1287
/// Subtracts an unsigned duration of time from an offset. This panics on
1288
/// overflow.
1289
///
1290
/// For checked arithmetic, see [`Offset::checked_sub`].
1291
impl Sub<UnsignedDuration> for Offset {
1292
    type Output = Offset;
1293
1294
    #[inline]
1295
0
    fn sub(self, rhs: UnsignedDuration) -> Offset {
1296
0
        self.checked_sub(rhs).expect(
1297
0
            "subtracting unsigned duration from offsetsshould not overflow",
1298
        )
1299
0
    }
1300
}
1301
1302
/// Subtracts an unsigned duration of time from an offset in place. This panics
1303
/// on overflow.
1304
///
1305
/// For checked arithmetic, see [`Offset::checked_sub`].
1306
impl SubAssign<UnsignedDuration> for Offset {
1307
    #[inline]
1308
0
    fn sub_assign(&mut self, rhs: UnsignedDuration) {
1309
0
        *self = self.sub(rhs);
1310
0
    }
1311
}
1312
1313
/// Negate this offset.
1314
///
1315
/// A positive offset becomes negative and vice versa. This is a no-op for the
1316
/// zero offset.
1317
///
1318
/// This never panics.
1319
impl Neg for Offset {
1320
    type Output = Offset;
1321
1322
    #[inline]
1323
0
    fn neg(self) -> Offset {
1324
0
        self.negate()
1325
0
    }
1326
}
1327
1328
/// Converts a `SignedDuration` to a time zone offset.
1329
///
1330
/// If the signed duration has fractional seconds, then it is automatically
1331
/// rounded to the nearest second. (Because an `Offset` has only second
1332
/// precision.)
1333
///
1334
/// # Errors
1335
///
1336
/// This returns an error if the duration overflows the limits of an `Offset`.
1337
///
1338
/// # Example
1339
///
1340
/// ```
1341
/// use jiff::{tz::{self, Offset}, SignedDuration};
1342
///
1343
/// let sdur = SignedDuration::from_secs(-5 * 60 * 60);
1344
/// let offset = Offset::try_from(sdur)?;
1345
/// assert_eq!(offset, tz::offset(-5));
1346
///
1347
/// // Sub-seconds results in rounded.
1348
/// let sdur = SignedDuration::new(-5 * 60 * 60, -500_000_000);
1349
/// let offset = Offset::try_from(sdur)?;
1350
/// assert_eq!(offset, tz::Offset::from_seconds(-(5 * 60 * 60 + 1)).unwrap());
1351
///
1352
/// # Ok::<(), Box<dyn std::error::Error>>(())
1353
/// ```
1354
impl TryFrom<SignedDuration> for Offset {
1355
    type Error = Error;
1356
1357
0
    fn try_from(sdur: SignedDuration) -> Result<Offset, Error> {
1358
0
        let mut seconds = sdur.as_secs();
1359
0
        let subsec = sdur.subsec_nanos();
1360
0
        if subsec >= 500_000_000 {
1361
0
            seconds = seconds.saturating_add(1);
1362
0
        } else if subsec <= -500_000_000 {
1363
0
            seconds = seconds.saturating_sub(1);
1364
0
        }
1365
0
        let seconds =
1366
0
            i32::try_from(seconds).map_err(|_| E::OverflowSignedDuration)?;
1367
0
        Offset::from_seconds(seconds)
1368
0
            .map_err(|_| Error::from(E::OverflowSignedDuration))
1369
0
    }
1370
}
1371
1372
#[cfg(feature = "defmt")]
1373
impl defmt::Format for Offset {
1374
    fn format(&self, f: defmt::Formatter) {
1375
        let sign = if self.is_negative() { "-" } else { "" };
1376
        defmt::write!(
1377
            f,
1378
            "{=str}{=u8:02}:{=u8:02}:{=u8:02}",
1379
            sign,
1380
            self.part_hours().unsigned_abs(),
1381
            self.part_minutes().unsigned_abs(),
1382
            self.part_seconds().unsigned_abs(),
1383
        )
1384
    }
1385
}
1386
1387
/// Options for [`Offset::checked_add`] and [`Offset::checked_sub`].
1388
///
1389
/// This type provides a way to ergonomically add one of a few different
1390
/// duration types to a [`Offset`].
1391
///
1392
/// The main way to construct values of this type is with its `From` trait
1393
/// implementations:
1394
///
1395
/// * `From<Span> for OffsetArithmetic` adds (or subtracts) the given span to
1396
/// the receiver offset.
1397
/// * `From<SignedDuration> for OffsetArithmetic` adds (or subtracts)
1398
/// the given signed duration to the receiver offset.
1399
/// * `From<std::time::Duration> for OffsetArithmetic` adds (or subtracts)
1400
/// the given unsigned duration to the receiver offset.
1401
///
1402
/// # Example
1403
///
1404
/// ```
1405
/// use std::time::Duration;
1406
///
1407
/// use jiff::{tz::offset, SignedDuration, ToSpan};
1408
///
1409
/// let off = offset(-10);
1410
/// assert_eq!(off.checked_add(11.hours())?, offset(1));
1411
/// assert_eq!(off.checked_add(SignedDuration::from_hours(11))?, offset(1));
1412
/// assert_eq!(off.checked_add(Duration::from_secs(11 * 60 * 60))?, offset(1));
1413
///
1414
/// # Ok::<(), Box<dyn std::error::Error>>(())
1415
/// ```
1416
#[derive(Clone, Copy, Debug)]
1417
pub struct OffsetArithmetic {
1418
    duration: Duration,
1419
}
1420
1421
impl OffsetArithmetic {
1422
    #[inline]
1423
0
    fn checked_add(self, offset: Offset) -> Result<Offset, Error> {
1424
0
        match self.duration.to_signed()? {
1425
0
            SDuration::Span(span) => offset.checked_add_span(span),
1426
0
            SDuration::Absolute(sdur) => offset.checked_add_duration(sdur),
1427
        }
1428
0
    }
1429
1430
    #[inline]
1431
0
    fn checked_neg(self) -> Result<OffsetArithmetic, Error> {
1432
0
        let duration = self.duration.checked_neg()?;
1433
0
        Ok(OffsetArithmetic { duration })
1434
0
    }
1435
1436
    #[inline]
1437
0
    fn is_negative(&self) -> bool {
1438
0
        self.duration.is_negative()
1439
0
    }
1440
}
1441
1442
impl From<Span> for OffsetArithmetic {
1443
0
    fn from(span: Span) -> OffsetArithmetic {
1444
0
        let duration = Duration::from(span);
1445
0
        OffsetArithmetic { duration }
1446
0
    }
1447
}
1448
1449
impl From<SignedDuration> for OffsetArithmetic {
1450
0
    fn from(sdur: SignedDuration) -> OffsetArithmetic {
1451
0
        let duration = Duration::from(sdur);
1452
0
        OffsetArithmetic { duration }
1453
0
    }
1454
}
1455
1456
impl From<UnsignedDuration> for OffsetArithmetic {
1457
0
    fn from(udur: UnsignedDuration) -> OffsetArithmetic {
1458
0
        let duration = Duration::from(udur);
1459
0
        OffsetArithmetic { duration }
1460
0
    }
1461
}
1462
1463
impl<'a> From<&'a Span> for OffsetArithmetic {
1464
0
    fn from(span: &'a Span) -> OffsetArithmetic {
1465
0
        OffsetArithmetic::from(*span)
1466
0
    }
1467
}
1468
1469
impl<'a> From<&'a SignedDuration> for OffsetArithmetic {
1470
0
    fn from(sdur: &'a SignedDuration) -> OffsetArithmetic {
1471
0
        OffsetArithmetic::from(*sdur)
1472
0
    }
1473
}
1474
1475
impl<'a> From<&'a UnsignedDuration> for OffsetArithmetic {
1476
0
    fn from(udur: &'a UnsignedDuration) -> OffsetArithmetic {
1477
0
        OffsetArithmetic::from(*udur)
1478
0
    }
1479
}
1480
1481
/// Options for [`Offset::round`].
1482
///
1483
/// This type provides a way to configure the rounding of an offset. This
1484
/// includes setting the smallest unit (i.e., the unit to round), the rounding
1485
/// increment and the rounding mode (e.g., "ceil" or "truncate").
1486
///
1487
/// [`Offset::round`] accepts anything that implements
1488
/// `Into<OffsetRound>`. There are a few key trait implementations that
1489
/// make this convenient:
1490
///
1491
/// * `From<Unit> for OffsetRound` will construct a rounding
1492
/// configuration where the smallest unit is set to the one given.
1493
/// * `From<(Unit, i64)> for OffsetRound` will construct a rounding
1494
/// configuration where the smallest unit and the rounding increment are set to
1495
/// the ones given.
1496
///
1497
/// In order to set other options (like the rounding mode), one must explicitly
1498
/// create a `OffsetRound` and pass it to `Offset::round`.
1499
///
1500
/// # Example
1501
///
1502
/// This example shows how to always round up to the nearest half-hour:
1503
///
1504
/// ```
1505
/// use jiff::{tz::{Offset, OffsetRound}, RoundMode, Unit};
1506
///
1507
/// let offset = Offset::from_seconds(4 * 60 * 60 + 17 * 60).unwrap();
1508
/// let rounded = offset.round(
1509
///     OffsetRound::new()
1510
///         .smallest(Unit::Minute)
1511
///         .increment(30)
1512
///         .mode(RoundMode::Expand),
1513
/// )?;
1514
/// assert_eq!(rounded, Offset::from_seconds(4 * 60 * 60 + 30 * 60).unwrap());
1515
///
1516
/// # Ok::<(), Box<dyn std::error::Error>>(())
1517
/// ```
1518
#[derive(Clone, Copy, Debug)]
1519
pub struct OffsetRound {
1520
    smallest: Unit,
1521
    mode: RoundMode,
1522
    increment: i64,
1523
}
1524
1525
impl OffsetRound {
1526
    /// Create a new default configuration for rounding a time zone offset via
1527
    /// [`Offset::round`].
1528
    ///
1529
    /// The default configuration does no rounding.
1530
    #[inline]
1531
0
    pub fn new() -> OffsetRound {
1532
0
        OffsetRound {
1533
0
            smallest: Unit::Second,
1534
0
            mode: RoundMode::HalfExpand,
1535
0
            increment: 1,
1536
0
        }
1537
0
    }
1538
1539
    /// Set the smallest units allowed in the offset returned. These are the
1540
    /// units that the offset is rounded to.
1541
    ///
1542
    /// # Errors
1543
    ///
1544
    /// The unit must be [`Unit::Hour`], [`Unit::Minute`] or [`Unit::Second`].
1545
    ///
1546
    /// # Example
1547
    ///
1548
    /// A basic example that rounds to the nearest minute:
1549
    ///
1550
    /// ```
1551
    /// use jiff::{tz::Offset, Unit};
1552
    ///
1553
    /// let offset = Offset::from_seconds(-(5 * 60 * 60 + 30)).unwrap();
1554
    /// assert_eq!(offset.round(Unit::Hour)?, Offset::from_hours(-5).unwrap());
1555
    ///
1556
    /// # Ok::<(), Box<dyn std::error::Error>>(())
1557
    /// ```
1558
    #[inline]
1559
0
    pub fn smallest(self, unit: Unit) -> OffsetRound {
1560
0
        OffsetRound { smallest: unit, ..self }
1561
0
    }
1562
1563
    /// Set the rounding mode.
1564
    ///
1565
    /// This defaults to [`RoundMode::HalfExpand`], which makes rounding work
1566
    /// like how you were taught in school.
1567
    ///
1568
    /// # Example
1569
    ///
1570
    /// A basic example that rounds to the nearest hour, but changing its
1571
    /// rounding mode to truncation:
1572
    ///
1573
    /// ```
1574
    /// use jiff::{tz::{Offset, OffsetRound}, RoundMode, Unit};
1575
    ///
1576
    /// let offset = Offset::from_seconds(-(5 * 60 * 60 + 30 * 60)).unwrap();
1577
    /// assert_eq!(
1578
    ///     offset.round(OffsetRound::new()
1579
    ///         .smallest(Unit::Hour)
1580
    ///         .mode(RoundMode::Trunc),
1581
    ///     )?,
1582
    ///     // The default round mode does rounding like
1583
    ///     // how you probably learned in school, and would
1584
    ///     // result in rounding to -6 hours. But we
1585
    ///     // change it to truncation here, which makes it
1586
    ///     // round -5.
1587
    ///     Offset::from_hours(-5).unwrap(),
1588
    /// );
1589
    ///
1590
    /// # Ok::<(), Box<dyn std::error::Error>>(())
1591
    /// ```
1592
    #[inline]
1593
0
    pub fn mode(self, mode: RoundMode) -> OffsetRound {
1594
0
        OffsetRound { mode, ..self }
1595
0
    }
1596
1597
    /// Set the rounding increment for the smallest unit.
1598
    ///
1599
    /// The default value is `1`. Other values permit rounding the smallest
1600
    /// unit to the nearest integer increment specified. For example, if the
1601
    /// smallest unit is set to [`Unit::Minute`], then a rounding increment of
1602
    /// `30` would result in rounding in increments of a half hour. That is,
1603
    /// the only minute value that could result would be `0` or `30`.
1604
    ///
1605
    /// # Errors
1606
    ///
1607
    /// Unlike rounding a [`Span`](crate::Span), the increment does not need to
1608
    /// divide evenly into the next largest unit. Callers can round an offset
1609
    /// to any increment value so long as it is greater than zero and less than
1610
    /// or equal to `1_000_000_000`.
1611
    ///
1612
    /// # Example
1613
    ///
1614
    /// This shows how to round an offset to the nearest 30 minute increment:
1615
    ///
1616
    /// ```
1617
    /// use jiff::{tz::Offset, Unit};
1618
    ///
1619
    /// let offset = Offset::from_seconds(4 * 60 * 60 + 15 * 60).unwrap();
1620
    /// assert_eq!(
1621
    ///     offset.round((Unit::Minute, 30))?,
1622
    ///     Offset::from_seconds(4 * 60 * 60 + 30 * 60).unwrap(),
1623
    /// );
1624
    ///
1625
    /// # Ok::<(), Box<dyn std::error::Error>>(())
1626
    /// ```
1627
    #[inline]
1628
0
    pub fn increment(self, increment: i64) -> OffsetRound {
1629
0
        OffsetRound { increment, ..self }
1630
0
    }
1631
1632
    /// Does the actual offset rounding.
1633
0
    fn round(&self, offset: Offset) -> Result<Offset, Error> {
1634
0
        let increment = Increment::for_offset(self.smallest, self.increment)?;
1635
        // let rounded_sdur = SignedDuration::from(offset).round(self.0)?;
1636
0
        let rounded = increment
1637
0
            .round(self.mode, SignedDuration::from(offset))
1638
0
            .context(E::RoundOverflow)?;
1639
0
        Offset::try_from(rounded)
1640
0
            .map_err(|_| b::OffsetTotalSeconds::error())
1641
0
            .context(E::RoundOverflow)
1642
0
    }
1643
}
1644
1645
impl Default for OffsetRound {
1646
0
    fn default() -> OffsetRound {
1647
0
        OffsetRound::new()
1648
0
    }
1649
}
1650
1651
impl From<Unit> for OffsetRound {
1652
0
    fn from(unit: Unit) -> OffsetRound {
1653
0
        OffsetRound::default().smallest(unit)
1654
0
    }
1655
}
1656
1657
impl From<(Unit, i64)> for OffsetRound {
1658
0
    fn from((unit, increment): (Unit, i64)) -> OffsetRound {
1659
0
        OffsetRound::default().smallest(unit).increment(increment)
1660
0
    }
1661
}
1662
1663
/// Configuration for resolving disparities between an offset and a time zone.
1664
///
1665
/// A conflict between an offset and a time zone most commonly appears in a
1666
/// datetime string. For example, `2024-06-14T17:30-05[America/New_York]`
1667
/// has a definitive inconsistency between the reported offset (`-05`) and
1668
/// the time zone (`America/New_York`), because at this time in New York,
1669
/// daylight saving time (DST) was in effect. In New York in the year 2024,
1670
/// DST corresponded to the UTC offset `-04`.
1671
///
1672
/// Other conflict variations exist. For example, in 2019, Brazil abolished
1673
/// DST completely. But if one were to create a datetime for 2020 in 2018, that
1674
/// datetime in 2020 would reflect the DST rules as they exist in 2018. That
1675
/// could in turn result in a datetime with an offset that is incorrect with
1676
/// respect to the rules in 2019.
1677
///
1678
/// For this reason, this crate exposes a few ways of resolving these
1679
/// conflicts. It is most commonly used as configuration for parsing
1680
/// [`Zoned`](crate::Zoned) values via
1681
/// [`fmt::temporal::DateTimeParser::offset_conflict`](crate::fmt::temporal::DateTimeParser::offset_conflict). But this configuration can also be used directly via
1682
/// [`OffsetConflict::resolve`].
1683
///
1684
/// The default value is `OffsetConflict::Reject`, which results in an
1685
/// error being returned if the offset and a time zone are not in agreement.
1686
/// This is the default so that Jiff does not automatically make silent choices
1687
/// about whether to prefer the time zone or the offset. The
1688
/// [`fmt::temporal::DateTimeParser::parse_zoned_with`](crate::fmt::temporal::DateTimeParser::parse_zoned_with)
1689
/// documentation shows an example demonstrating its utility in the face
1690
/// of changes in the law, such as the abolition of daylight saving time.
1691
/// By rejecting such things, one can ensure that the original timestamp is
1692
/// preserved or else an error occurs.
1693
///
1694
/// This enum is non-exhaustive so that other forms of offset conflicts may be
1695
/// added in semver compatible releases.
1696
///
1697
/// # Example
1698
///
1699
/// This example shows how to always use the time zone even if the offset is
1700
/// wrong.
1701
///
1702
/// ```
1703
/// use jiff::{civil::date, tz};
1704
///
1705
/// let dt = date(2024, 6, 14).at(17, 30, 0, 0);
1706
/// let offset = tz::offset(-5); // wrong! should be -4
1707
/// let newyork = tz::db().get("America/New_York")?;
1708
///
1709
/// // The default conflict resolution, 'Reject', will error.
1710
/// let result = tz::OffsetConflict::Reject
1711
///     .resolve(dt, offset, newyork.clone());
1712
/// assert!(result.is_err());
1713
///
1714
/// // But we can change it to always prefer the time zone.
1715
/// let zdt = tz::OffsetConflict::AlwaysTimeZone
1716
///     .resolve(dt, offset, newyork.clone())?
1717
///     .unambiguous()?;
1718
/// assert_eq!(zdt.datetime(), date(2024, 6, 14).at(17, 30, 0, 0));
1719
/// // The offset has been corrected automatically.
1720
/// assert_eq!(zdt.offset(), tz::offset(-4));
1721
///
1722
/// # Ok::<(), Box<dyn std::error::Error>>(())
1723
/// ```
1724
///
1725
/// # Example: parsing
1726
///
1727
/// This example shows how to set the offset conflict resolution configuration
1728
/// while parsing a [`Zoned`](crate::Zoned) datetime. In this example, we
1729
/// always prefer the offset, even if it conflicts with the time zone.
1730
///
1731
/// ```
1732
/// use jiff::{civil::date, fmt::temporal::DateTimeParser, tz};
1733
///
1734
/// static PARSER: DateTimeParser = DateTimeParser::new()
1735
///     .offset_conflict(tz::OffsetConflict::AlwaysOffset);
1736
///
1737
/// let zdt = PARSER.parse_zoned("2024-06-14T17:30-05[America/New_York]")?;
1738
/// // The time *and* offset have been corrected. The offset given was invalid,
1739
/// // so it cannot be kept, but the timestamp returned is equivalent to
1740
/// // `2024-06-14T17:30-05`. It is just adjusted automatically to be correct
1741
/// // in the `America/New_York` time zone.
1742
/// assert_eq!(zdt.datetime(), date(2024, 6, 14).at(18, 30, 0, 0));
1743
/// assert_eq!(zdt.offset(), tz::offset(-4));
1744
///
1745
/// # Ok::<(), Box<dyn std::error::Error>>(())
1746
/// ```
1747
#[derive(Clone, Copy, Debug, Default)]
1748
#[non_exhaustive]
1749
pub enum OffsetConflict {
1750
    /// When the offset and time zone are in conflict, this will always use
1751
    /// the offset to interpret the date time.
1752
    ///
1753
    /// When resolving to a [`AmbiguousZoned`], the time zone attached
1754
    /// to the timestamp will still be the same as the time zone given. The
1755
    /// difference here is that the offset will be adjusted such that it is
1756
    /// correct for the given time zone. However, the timestamp itself will
1757
    /// always match the datetime and offset given (and which is always
1758
    /// unambiguous).
1759
    ///
1760
    /// Basically, you should use this option when you want to keep the exact
1761
    /// time unchanged (as indicated by the datetime and offset), even if it
1762
    /// means a change to civil time.
1763
    AlwaysOffset,
1764
    /// When the offset and time zone are in conflict, this will always use
1765
    /// the time zone to interpret the date time.
1766
    ///
1767
    /// When resolving to an [`AmbiguousZoned`], the offset attached to the
1768
    /// timestamp will always be determined by only looking at the time zone.
1769
    /// This in turn implies that the timestamp returned could be ambiguous,
1770
    /// since this conflict resolution strategy specifically ignores the
1771
    /// offset. (And, we're only at this point because the offset is not
1772
    /// possible for the given time zone, so it can't be used in concert with
1773
    /// the time zone anyway.) This is unlike the `AlwaysOffset` strategy where
1774
    /// the timestamp returned is guaranteed to be unambiguous.
1775
    ///
1776
    /// You should use this option when you want to keep the civil time
1777
    /// unchanged even if it means a change to the exact time.
1778
    AlwaysTimeZone,
1779
    /// Always attempt to use the offset to resolve a datetime to a timestamp,
1780
    /// unless the offset is invalid for the provided time zone. In that case,
1781
    /// use the time zone. When the time zone is used, it's possible for an
1782
    /// ambiguous datetime to be returned.
1783
    ///
1784
    /// See [`ZonedWith::offset_conflict`](crate::ZonedWith::offset_conflict)
1785
    /// for an example of when this strategy is useful.
1786
    PreferOffset,
1787
    /// When the offset and time zone are in conflict, this strategy always
1788
    /// results in conflict resolution returning an error.
1789
    ///
1790
    /// This is the default since a conflict between the offset and the time
1791
    /// zone usually implies an invalid datetime in some way.
1792
    #[default]
1793
    Reject,
1794
}
1795
1796
impl OffsetConflict {
1797
    /// Resolve a potential conflict between an [`Offset`] and a [`TimeZone`].
1798
    ///
1799
    /// # Errors
1800
    ///
1801
    /// This returns an error if this would have returned a timestamp outside
1802
    /// of its minimum and maximum values.
1803
    ///
1804
    /// This can also return an error when using the [`OffsetConflict::Reject`]
1805
    /// strategy. Namely, when using the `Reject` strategy, any offset that is
1806
    /// not compatible with the given datetime and time zone will always result
1807
    /// in an error.
1808
    ///
1809
    /// # Example
1810
    ///
1811
    /// This example shows how each of the different conflict resolution
1812
    /// strategies are applied.
1813
    ///
1814
    /// ```
1815
    /// use jiff::{civil::date, tz};
1816
    ///
1817
    /// let dt = date(2024, 6, 14).at(17, 30, 0, 0);
1818
    /// let offset = tz::offset(-5); // wrong! should be -4
1819
    /// let newyork = tz::db().get("America/New_York")?;
1820
    ///
1821
    /// // Here, we use the offset and ignore the time zone.
1822
    /// let zdt = tz::OffsetConflict::AlwaysOffset
1823
    ///     .resolve(dt, offset, newyork.clone())?
1824
    ///     .unambiguous()?;
1825
    /// // The datetime (and offset) have been corrected automatically
1826
    /// // and the resulting Zoned instant corresponds precisely to
1827
    /// // `2024-06-14T17:30-05[UTC]`.
1828
    /// assert_eq!(zdt.to_string(), "2024-06-14T18:30:00-04:00[America/New_York]");
1829
    ///
1830
    /// // Here, we use the time zone and ignore the offset.
1831
    /// let zdt = tz::OffsetConflict::AlwaysTimeZone
1832
    ///     .resolve(dt, offset, newyork.clone())?
1833
    ///     .unambiguous()?;
1834
    /// // The offset has been corrected automatically and the resulting
1835
    /// // Zoned instant corresponds precisely to `2024-06-14T17:30-04[UTC]`.
1836
    /// // Notice how the civil time remains the same, but the exact instant
1837
    /// // has changed!
1838
    /// assert_eq!(zdt.to_string(), "2024-06-14T17:30:00-04:00[America/New_York]");
1839
    ///
1840
    /// // Here, we prefer the offset, but fall back to the time zone.
1841
    /// // In this example, it has the same behavior as `AlwaysTimeZone`.
1842
    /// let zdt = tz::OffsetConflict::PreferOffset
1843
    ///     .resolve(dt, offset, newyork.clone())?
1844
    ///     .unambiguous()?;
1845
    /// assert_eq!(zdt.to_string(), "2024-06-14T17:30:00-04:00[America/New_York]");
1846
    ///
1847
    /// // The default conflict resolution, 'Reject', will error.
1848
    /// let result = tz::OffsetConflict::Reject
1849
    ///     .resolve(dt, offset, newyork.clone());
1850
    /// assert!(result.is_err());
1851
    ///
1852
    /// # Ok::<(), Box<dyn std::error::Error>>(())
1853
    /// ```
1854
0
    pub fn resolve(
1855
0
        self,
1856
0
        dt: civil::DateTime,
1857
0
        offset: Offset,
1858
0
        tz: TimeZone,
1859
0
    ) -> Result<AmbiguousZoned, Error> {
1860
0
        self.resolve_with(dt, offset, tz, |off1, off2| off1 == off2)
1861
0
    }
1862
1863
    /// Resolve a potential conflict between an [`Offset`] and a [`TimeZone`]
1864
    /// using the given definition of equality for an `Offset`.
1865
    ///
1866
    /// The equality predicate is always given a pair of offsets where the
1867
    /// first is the offset given to `resolve_with` and the second is the
1868
    /// offset found in the `TimeZone`.
1869
    ///
1870
    /// # Errors
1871
    ///
1872
    /// This returns an error if this would have returned a timestamp outside
1873
    /// of its minimum and maximum values.
1874
    ///
1875
    /// This can also return an error when using the [`OffsetConflict::Reject`]
1876
    /// strategy. Namely, when using the `Reject` strategy, any offset that is
1877
    /// not compatible with the given datetime and time zone will always result
1878
    /// in an error.
1879
    ///
1880
    /// # Example
1881
    ///
1882
    /// Unlike [`OffsetConflict::resolve`], this routine permits overriding
1883
    /// the definition of equality used for comparing offsets. In
1884
    /// `OffsetConflict::resolve`, exact equality is used. This can be
1885
    /// troublesome in some cases when a time zone has an offset with
1886
    /// fractional minutes, such as `Africa/Monrovia` before 1972.
1887
    ///
1888
    /// Because RFC 3339 and RFC 9557 do not support time zone offsets
1889
    /// with fractional minutes, Jiff will serialize offsets with
1890
    /// fractional minutes by rounding to the nearest minute. This
1891
    /// will result in a different offset than what is actually
1892
    /// used in the time zone. Parsing this _should_ succeed, but
1893
    /// if exact offset equality is used, it won't. This is why a
1894
    /// [`fmt::temporal::DateTimeParser`](crate::fmt::temporal::DateTimeParser)
1895
    /// uses this routine with offset equality that rounds offsets to the
1896
    /// nearest minute before comparison.
1897
    ///
1898
    /// ```
1899
    /// use jiff::{civil::date, tz::{Offset, OffsetConflict, TimeZone}, Unit};
1900
    ///
1901
    /// let dt = date(1968, 2, 1).at(23, 15, 0, 0);
1902
    /// let offset = Offset::from_seconds(-(44 * 60 + 30)).unwrap();
1903
    /// let zdt = dt.in_tz("Africa/Monrovia")?;
1904
    /// assert_eq!(zdt.offset(), offset);
1905
    /// // Notice that the offset has been rounded!
1906
    /// assert_eq!(zdt.to_string(), "1968-02-01T23:15:00-00:45[Africa/Monrovia]");
1907
    ///
1908
    /// // Now imagine parsing extracts the civil datetime, the offset and
1909
    /// // the time zone, and then naively does exact offset comparison:
1910
    /// let tz = TimeZone::get("Africa/Monrovia")?;
1911
    /// // This is the parsed offset, which won't precisely match the actual
1912
    /// // offset used by `Africa/Monrovia` at this time.
1913
    /// let offset = Offset::from_seconds(-45 * 60).unwrap();
1914
    /// let result = OffsetConflict::Reject.resolve(dt, offset, tz.clone());
1915
    /// assert_eq!(
1916
    ///     result.unwrap_err().to_string(),
1917
    ///     "datetime could not resolve to a timestamp since `reject` \
1918
    ///      conflict resolution was chosen, and because datetime has offset \
1919
    ///      `-00:45`, but the time zone `Africa/Monrovia` for the given \
1920
    ///      datetime unambiguously has offset `-00:44:30`",
1921
    /// );
1922
    /// let is_equal = |parsed: Offset, candidate: Offset| {
1923
    ///     parsed == candidate || candidate.round(Unit::Minute).map_or(
1924
    ///         parsed == candidate,
1925
    ///         |candidate| parsed == candidate,
1926
    ///     )
1927
    /// };
1928
    /// let zdt = OffsetConflict::Reject.resolve_with(
1929
    ///     dt,
1930
    ///     offset,
1931
    ///     tz.clone(),
1932
    ///     is_equal,
1933
    /// )?.unambiguous()?;
1934
    /// // Notice that the offset is the actual offset from the time zone:
1935
    /// assert_eq!(zdt.offset(), Offset::from_seconds(-(44 * 60 + 30)).unwrap());
1936
    /// // But when we serialize, the offset gets rounded. If we didn't
1937
    /// // do this, we'd risk the datetime not being parsable by other
1938
    /// // implementations since RFC 3339 and RFC 9557 don't support fractional
1939
    /// // minutes in the offset.
1940
    /// assert_eq!(zdt.to_string(), "1968-02-01T23:15:00-00:45[Africa/Monrovia]");
1941
    ///
1942
    /// # Ok::<(), Box<dyn std::error::Error>>(())
1943
    /// ```
1944
    ///
1945
    /// And indeed, notice that parsing uses this same kind of offset equality
1946
    /// to permit zoned datetimes whose offsets would be equivalent after
1947
    /// rounding:
1948
    ///
1949
    /// ```
1950
    /// use jiff::{tz::Offset, Zoned};
1951
    ///
1952
    /// let zdt: Zoned = "1968-02-01T23:15:00-00:45[Africa/Monrovia]".parse()?;
1953
    /// // As above, notice that even though we parsed `-00:45` as the
1954
    /// // offset, the actual offset of our zoned datetime is the correct
1955
    /// // one from the time zone.
1956
    /// assert_eq!(zdt.offset(), Offset::from_seconds(-(44 * 60 + 30)).unwrap());
1957
    /// // And similarly, re-serializing it results in rounding the offset
1958
    /// // again for compatibility with RFC 3339 and RFC 9557.
1959
    /// assert_eq!(zdt.to_string(), "1968-02-01T23:15:00-00:45[Africa/Monrovia]");
1960
    ///
1961
    /// // And we also support parsing the actual fractional minute offset
1962
    /// // as well:
1963
    /// let zdt: Zoned = "1968-02-01T23:15:00-00:44:30[Africa/Monrovia]".parse()?;
1964
    /// assert_eq!(zdt.offset(), Offset::from_seconds(-(44 * 60 + 30)).unwrap());
1965
    /// assert_eq!(zdt.to_string(), "1968-02-01T23:15:00-00:45[Africa/Monrovia]");
1966
    ///
1967
    /// # Ok::<(), Box<dyn std::error::Error>>(())
1968
    /// ```
1969
    ///
1970
    /// Rounding does not occur when the parsed offset itself contains
1971
    /// sub-minute precision. In that case, exact equality is used:
1972
    ///
1973
    /// ```
1974
    /// use jiff::Zoned;
1975
    ///
1976
    /// let result = "1970-06-01T00-00:45:00[Africa/Monrovia]".parse::<Zoned>();
1977
    /// assert_eq!(
1978
    ///     result.unwrap_err().to_string(),
1979
    ///     "datetime could not resolve to a timestamp since `reject` \
1980
    ///      conflict resolution was chosen, and because datetime has offset \
1981
    ///      `-00:45`, but the time zone `Africa/Monrovia` for the given \
1982
    ///      datetime unambiguously has offset `-00:44:30`",
1983
    /// );
1984
    /// ```
1985
0
    pub fn resolve_with<F>(
1986
0
        self,
1987
0
        dt: civil::DateTime,
1988
0
        offset: Offset,
1989
0
        tz: TimeZone,
1990
0
        is_equal: F,
1991
0
    ) -> Result<AmbiguousZoned, Error>
1992
0
    where
1993
0
        F: FnMut(Offset, Offset) -> bool,
1994
    {
1995
0
        match self {
1996
            // In this case, we ignore any TZ annotation (although still
1997
            // require that it exists) and always use the provided offset.
1998
            OffsetConflict::AlwaysOffset => {
1999
0
                let kind = AmbiguousOffset::Unambiguous { offset };
2000
0
                Ok(AmbiguousTimestamp::new(dt, kind).into_ambiguous_zoned(tz))
2001
            }
2002
            // In this case, we ignore any provided offset and always use the
2003
            // time zone annotation.
2004
0
            OffsetConflict::AlwaysTimeZone => Ok(tz.into_ambiguous_zoned(dt)),
2005
            // In this case, we use the offset if it's correct, but otherwise
2006
            // fall back to the time zone annotation if it's not.
2007
0
            OffsetConflict::PreferOffset => Ok(
2008
0
                OffsetConflict::resolve_via_prefer(dt, offset, tz, is_equal),
2009
0
            ),
2010
            // In this case, if the offset isn't possible for the provided time
2011
            // zone annotation, then we return an error.
2012
            OffsetConflict::Reject => {
2013
0
                OffsetConflict::resolve_via_reject(dt, offset, tz, is_equal)
2014
            }
2015
        }
2016
0
    }
Unexecuted instantiation: <jiff::tz::offset::OffsetConflict>::resolve_with::<<jiff::tz::offset::OffsetConflict>::resolve::{closure#0}>
Unexecuted instantiation: <jiff::tz::offset::OffsetConflict>::resolve_with::<<jiff::fmt::temporal::parser::ParsedDateTime>::to_ambiguous_zoned::{closure#1}>
2017
2018
    /// Given a parsed datetime, a parsed offset and a parsed time zone, this
2019
    /// attempts to resolve the datetime to a particular instant based on the
2020
    /// 'prefer' strategy.
2021
    ///
2022
    /// In the 'prefer' strategy, we prefer to use the parsed offset to resolve
2023
    /// any ambiguity in the parsed datetime and time zone, but only if the
2024
    /// parsed offset is valid for the parsed datetime and time zone. If the
2025
    /// parsed offset isn't valid, then it is ignored. In the case where it is
2026
    /// ignored, it is possible for an ambiguous instant to be returned.
2027
0
    fn resolve_via_prefer(
2028
0
        dt: civil::DateTime,
2029
0
        given: Offset,
2030
0
        tz: TimeZone,
2031
0
        mut is_equal: impl FnMut(Offset, Offset) -> bool,
2032
0
    ) -> AmbiguousZoned {
2033
        use crate::tz::AmbiguousOffset::*;
2034
2035
0
        let amb = tz.to_ambiguous_timestamp(dt);
2036
0
        match amb.offset() {
2037
            // We only look for folds because we consider all offsets for gaps
2038
            // to be invalid. Which is consistent with how they're treated as
2039
            // `OffsetConflict::Reject`. Thus, like any other invalid offset,
2040
            // we fallback to disambiguation (which is handled by the caller).
2041
0
            Fold { before, after }
2042
0
                if is_equal(given, before) || is_equal(given, after) =>
2043
            {
2044
0
                let kind = Unambiguous { offset: given };
2045
0
                AmbiguousTimestamp::new(dt, kind)
2046
            }
2047
0
            _ => amb,
2048
        }
2049
0
        .into_ambiguous_zoned(tz)
2050
0
    }
Unexecuted instantiation: <jiff::tz::offset::OffsetConflict>::resolve_via_prefer::<<jiff::tz::offset::OffsetConflict>::resolve::{closure#0}>
Unexecuted instantiation: <jiff::tz::offset::OffsetConflict>::resolve_via_prefer::<<jiff::fmt::temporal::parser::ParsedDateTime>::to_ambiguous_zoned::{closure#1}>
2051
2052
    /// Given a parsed datetime, a parsed offset and a parsed time zone, this
2053
    /// attempts to resolve the datetime to a particular instant based on the
2054
    /// 'reject' strategy.
2055
    ///
2056
    /// That is, if the offset is not possibly valid for the given datetime and
2057
    /// time zone, then this returns an error.
2058
    ///
2059
    /// This guarantees that on success, an unambiguous timestamp is returned.
2060
    /// This occurs because if the datetime is ambiguous for the given time
2061
    /// zone, then the parsed offset either matches one of the possible offsets
2062
    /// (and thus provides an unambiguous choice), or it doesn't and an error
2063
    /// is returned.
2064
0
    fn resolve_via_reject(
2065
0
        dt: civil::DateTime,
2066
0
        given: Offset,
2067
0
        tz: TimeZone,
2068
0
        mut is_equal: impl FnMut(Offset, Offset) -> bool,
2069
0
    ) -> Result<AmbiguousZoned, Error> {
2070
        use crate::tz::AmbiguousOffset::*;
2071
2072
0
        let amb = tz.to_ambiguous_timestamp(dt);
2073
0
        match amb.offset() {
2074
0
            Unambiguous { offset } if !is_equal(given, offset) => {
2075
0
                Err(Error::from(E::ResolveRejectUnambiguous {
2076
0
                    given,
2077
0
                    offset,
2078
0
                    tz,
2079
0
                }))
2080
            }
2081
0
            Unambiguous { .. } => Ok(amb.into_ambiguous_zoned(tz)),
2082
0
            Gap { before, after } => {
2083
                // In `jiff 0.1`, we reported an error when we found a gap
2084
                // where neither offset matched what was given. But now we
2085
                // report an error whenever we find a gap, as we consider
2086
                // all offsets to be invalid for the gap. This now matches
2087
                // Temporal's behavior which I think is more consistent. And in
2088
                // particular, this makes it more consistent with the behavior
2089
                // of `PreferOffset` when a gap is found (which was also
2090
                // changed to treat all offsets in a gap as invalid).
2091
                //
2092
                // Ref: https://github.com/tc39/proposal-temporal/issues/2892
2093
0
                Err(Error::from(E::ResolveRejectGap {
2094
0
                    given,
2095
0
                    before,
2096
0
                    after,
2097
0
                    tz,
2098
0
                }))
2099
            }
2100
0
            Fold { before, after }
2101
0
                if !is_equal(given, before) && !is_equal(given, after) =>
2102
            {
2103
0
                Err(Error::from(E::ResolveRejectFold {
2104
0
                    given,
2105
0
                    before,
2106
0
                    after,
2107
0
                    tz,
2108
0
                }))
2109
            }
2110
            Fold { .. } => {
2111
0
                let kind = Unambiguous { offset: given };
2112
0
                Ok(AmbiguousTimestamp::new(dt, kind).into_ambiguous_zoned(tz))
2113
            }
2114
        }
2115
0
    }
Unexecuted instantiation: <jiff::tz::offset::OffsetConflict>::resolve_via_reject::<<jiff::tz::offset::OffsetConflict>::resolve::{closure#0}>
Unexecuted instantiation: <jiff::tz::offset::OffsetConflict>::resolve_via_reject::<<jiff::fmt::temporal::parser::ParsedDateTime>::to_ambiguous_zoned::{closure#1}>
2116
}