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