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