Skip to main content

jiff/tz/
offset.rs

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