Skip to main content

jiff/tz/
offset.rs

1use core::{
2    ops::{Add, AddAssign, Neg, Sub, SubAssign},
3    time::Duration as UnsignedDuration,
4};
5
6use crate::{
7    civil,
8    duration::{Duration, SDuration},
9    error::{tz::offset::Error as E, Error, ErrorContext},
10    shared::util::itime::IOffset,
11    span::Span,
12    timestamp::Timestamp,
13    tz::{AmbiguousOffset, AmbiguousTimestamp, AmbiguousZoned, TimeZone},
14    util::{
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)
530            .context(E::ConvertDateTimeToTimestamp { offset: self })
531    }
532
533    /// Adds the given span of time to this offset.
534    ///
535    /// Since time zone offsets have second resolution, any fractional seconds
536    /// in the duration given are ignored.
537    ///
538    /// This operation accepts three different duration types: [`Span`],
539    /// [`SignedDuration`] or [`std::time::Duration`]. This is achieved via
540    /// `From` trait implementations for the [`OffsetArithmetic`] type.
541    ///
542    /// # Errors
543    ///
544    /// This returns an error if the result of adding the given span would
545    /// exceed the minimum or maximum allowed `Offset` value.
546    ///
547    /// This also returns an error if the span given contains any non-zero
548    /// units bigger than hours.
549    ///
550    /// # Example
551    ///
552    /// This example shows how to add one hour to an offset (if the offset
553    /// corresponds to standard time, then adding an hour will usually give
554    /// you DST time):
555    ///
556    /// ```
557    /// use jiff::{tz, ToSpan};
558    ///
559    /// let off = tz::offset(-5);
560    /// assert_eq!(off.checked_add(1.hours()).unwrap(), tz::offset(-4));
561    /// ```
562    ///
563    /// And note that while fractional seconds are ignored, units less than
564    /// seconds aren't ignored if they sum up to a duration at least as big
565    /// as one second:
566    ///
567    /// ```
568    /// use jiff::{tz, ToSpan};
569    ///
570    /// let off = tz::offset(5);
571    /// let span = 900.milliseconds()
572    ///     .microseconds(50_000)
573    ///     .nanoseconds(50_000_000);
574    /// assert_eq!(
575    ///     off.checked_add(span).unwrap(),
576    ///     tz::Offset::from_seconds((5 * 60 * 60) + 1).unwrap(),
577    /// );
578    /// // Any leftover fractional part is ignored.
579    /// let span = 901.milliseconds()
580    ///     .microseconds(50_001)
581    ///     .nanoseconds(50_000_001);
582    /// assert_eq!(
583    ///     off.checked_add(span).unwrap(),
584    ///     tz::Offset::from_seconds((5 * 60 * 60) + 1).unwrap(),
585    /// );
586    /// ```
587    ///
588    /// This example shows some cases where checked addition will fail.
589    ///
590    /// ```
591    /// use jiff::{tz::Offset, ToSpan};
592    ///
593    /// // Adding units above 'hour' always results in an error.
594    /// assert!(Offset::UTC.checked_add(1.day()).is_err());
595    /// assert!(Offset::UTC.checked_add(1.week()).is_err());
596    /// assert!(Offset::UTC.checked_add(1.month()).is_err());
597    /// assert!(Offset::UTC.checked_add(1.year()).is_err());
598    ///
599    /// // Adding even 1 second to the max, or subtracting 1 from the min,
600    /// // will result in overflow and thus an error will be returned.
601    /// assert!(Offset::MIN.checked_add(-1.seconds()).is_err());
602    /// assert!(Offset::MAX.checked_add(1.seconds()).is_err());
603    /// ```
604    ///
605    /// # Example: adding absolute durations
606    ///
607    /// This shows how to add signed and unsigned absolute durations to an
608    /// `Offset`. Like with `Span`s, any fractional seconds are ignored.
609    ///
610    /// ```
611    /// use std::time::Duration;
612    ///
613    /// use jiff::{tz::offset, SignedDuration};
614    ///
615    /// let off = offset(-10);
616    ///
617    /// let dur = SignedDuration::from_hours(11);
618    /// assert_eq!(off.checked_add(dur)?, offset(1));
619    /// assert_eq!(off.checked_add(-dur)?, offset(-21));
620    ///
621    /// // Any leftover time is truncated. That is, only
622    /// // whole seconds from the duration are considered.
623    /// let dur = Duration::new(3 * 60 * 60, 999_999_999);
624    /// assert_eq!(off.checked_add(dur)?, offset(-7));
625    ///
626    /// # Ok::<(), Box<dyn std::error::Error>>(())
627    /// ```
628    #[inline]
629    pub fn checked_add<A: Into<OffsetArithmetic>>(
630        self,
631        duration: A,
632    ) -> Result<Offset, Error> {
633        let duration: OffsetArithmetic = duration.into();
634        duration.checked_add(self)
635    }
636
637    #[inline]
638    fn checked_add_span(self, span: Span) -> Result<Offset, Error> {
639        if let Some(err) = span.smallest_non_time_non_zero_unit_error() {
640            return Err(err);
641        }
642        let span_seconds = t::SpanZoneOffset::try_rfrom(
643            "span-seconds",
644            span.to_invariant_nanoseconds().div_ceil(t::NANOS_PER_SECOND),
645        )?;
646        let offset_seconds = self.seconds_ranged();
647        let seconds =
648            offset_seconds.try_checked_add("offset-seconds", span_seconds)?;
649        Ok(Offset::from_seconds_ranged(seconds))
650    }
651
652    #[inline]
653    fn checked_add_duration(
654        self,
655        duration: SignedDuration,
656    ) -> Result<Offset, Error> {
657        let duration =
658            t::SpanZoneOffset::try_new("duration-seconds", duration.as_secs())
659                .context(E::OverflowAddSignedDuration)?;
660        let offset_seconds = self.seconds_ranged();
661        let seconds = offset_seconds
662            .try_checked_add("offset-seconds", duration)
663            .context(E::OverflowAddSignedDuration)?;
664        Ok(Offset::from_seconds_ranged(seconds))
665    }
666
667    /// This routine is identical to [`Offset::checked_add`] with the duration
668    /// negated.
669    ///
670    /// # Errors
671    ///
672    /// This has the same error conditions as [`Offset::checked_add`].
673    ///
674    /// # Example
675    ///
676    /// ```
677    /// use std::time::Duration;
678    ///
679    /// use jiff::{tz, SignedDuration, ToSpan};
680    ///
681    /// let off = tz::offset(-4);
682    /// assert_eq!(
683    ///     off.checked_sub(1.hours())?,
684    ///     tz::offset(-5),
685    /// );
686    /// assert_eq!(
687    ///     off.checked_sub(SignedDuration::from_hours(1))?,
688    ///     tz::offset(-5),
689    /// );
690    /// assert_eq!(
691    ///     off.checked_sub(Duration::from_secs(60 * 60))?,
692    ///     tz::offset(-5),
693    /// );
694    ///
695    /// # Ok::<(), Box<dyn std::error::Error>>(())
696    /// ```
697    #[inline]
698    pub fn checked_sub<A: Into<OffsetArithmetic>>(
699        self,
700        duration: A,
701    ) -> Result<Offset, Error> {
702        let duration: OffsetArithmetic = duration.into();
703        duration.checked_neg().and_then(|oa| oa.checked_add(self))
704    }
705
706    /// This routine is identical to [`Offset::checked_add`], except the
707    /// result saturates on overflow. That is, instead of overflow, either
708    /// [`Offset::MIN`] or [`Offset::MAX`] is returned.
709    ///
710    /// # Example
711    ///
712    /// This example shows some cases where saturation will occur.
713    ///
714    /// ```
715    /// use jiff::{tz::Offset, SignedDuration, ToSpan};
716    ///
717    /// // Adding units above 'day' always results in saturation.
718    /// assert_eq!(Offset::UTC.saturating_add(1.weeks()), Offset::MAX);
719    /// assert_eq!(Offset::UTC.saturating_add(1.months()), Offset::MAX);
720    /// assert_eq!(Offset::UTC.saturating_add(1.years()), Offset::MAX);
721    ///
722    /// // Adding even 1 second to the max, or subtracting 1 from the min,
723    /// // will result in saturationg.
724    /// assert_eq!(Offset::MIN.saturating_add(-1.seconds()), Offset::MIN);
725    /// assert_eq!(Offset::MAX.saturating_add(1.seconds()), Offset::MAX);
726    ///
727    /// // Adding absolute durations also saturates as expected.
728    /// assert_eq!(Offset::UTC.saturating_add(SignedDuration::MAX), Offset::MAX);
729    /// assert_eq!(Offset::UTC.saturating_add(SignedDuration::MIN), Offset::MIN);
730    /// assert_eq!(Offset::UTC.saturating_add(std::time::Duration::MAX), Offset::MAX);
731    /// ```
732    #[inline]
733    pub fn saturating_add<A: Into<OffsetArithmetic>>(
734        self,
735        duration: A,
736    ) -> Offset {
737        let duration: OffsetArithmetic = duration.into();
738        self.checked_add(duration).unwrap_or_else(|_| {
739            if duration.is_negative() {
740                Offset::MIN
741            } else {
742                Offset::MAX
743            }
744        })
745    }
746
747    /// This routine is identical to [`Offset::saturating_add`] with the span
748    /// parameter negated.
749    ///
750    /// # Example
751    ///
752    /// This example shows some cases where saturation will occur.
753    ///
754    /// ```
755    /// use jiff::{tz::Offset, SignedDuration, ToSpan};
756    ///
757    /// // Adding units above 'day' always results in saturation.
758    /// assert_eq!(Offset::UTC.saturating_sub(1.weeks()), Offset::MIN);
759    /// assert_eq!(Offset::UTC.saturating_sub(1.months()), Offset::MIN);
760    /// assert_eq!(Offset::UTC.saturating_sub(1.years()), Offset::MIN);
761    ///
762    /// // Adding even 1 second to the max, or subtracting 1 from the min,
763    /// // will result in saturationg.
764    /// assert_eq!(Offset::MIN.saturating_sub(1.seconds()), Offset::MIN);
765    /// assert_eq!(Offset::MAX.saturating_sub(-1.seconds()), Offset::MAX);
766    ///
767    /// // Adding absolute durations also saturates as expected.
768    /// assert_eq!(Offset::UTC.saturating_sub(SignedDuration::MAX), Offset::MIN);
769    /// assert_eq!(Offset::UTC.saturating_sub(SignedDuration::MIN), Offset::MAX);
770    /// assert_eq!(Offset::UTC.saturating_sub(std::time::Duration::MAX), Offset::MIN);
771    /// ```
772    #[inline]
773    pub fn saturating_sub<A: Into<OffsetArithmetic>>(
774        self,
775        duration: A,
776    ) -> Offset {
777        let duration: OffsetArithmetic = duration.into();
778        let Ok(duration) = duration.checked_neg() else { return Offset::MIN };
779        self.saturating_add(duration)
780    }
781
782    /// Returns the span of time from this offset until the other given.
783    ///
784    /// When the `other` offset is more west (i.e., more negative) of the prime
785    /// meridian than this offset, then the span returned will be negative.
786    ///
787    /// # Properties
788    ///
789    /// Adding the span returned to this offset will always equal the `other`
790    /// offset given.
791    ///
792    /// # Examples
793    ///
794    /// ```
795    /// use jiff::{tz, ToSpan};
796    ///
797    /// assert_eq!(
798    ///     tz::offset(-5).until(tz::Offset::UTC),
799    ///     (5 * 60 * 60).seconds().fieldwise(),
800    /// );
801    /// // Flipping the operands in this case results in a negative span.
802    /// assert_eq!(
803    ///     tz::Offset::UTC.until(tz::offset(-5)),
804    ///     -(5 * 60 * 60).seconds().fieldwise(),
805    /// );
806    /// ```
807    #[inline]
808    pub fn until(self, other: Offset) -> Span {
809        let diff = other.seconds_ranged() - self.seconds_ranged();
810        Span::new().seconds_ranged(diff.rinto())
811    }
812
813    /// Returns the span of time since the other offset given from this offset.
814    ///
815    /// When the `other` is more east (i.e., more positive) of the prime
816    /// meridian than this offset, then the span returned will be negative.
817    ///
818    /// # Properties
819    ///
820    /// Adding the span returned to the `other` offset will always equal this
821    /// offset.
822    ///
823    /// # Examples
824    ///
825    /// ```
826    /// use jiff::{tz, ToSpan};
827    ///
828    /// assert_eq!(
829    ///     tz::Offset::UTC.since(tz::offset(-5)),
830    ///     (5 * 60 * 60).seconds().fieldwise(),
831    /// );
832    /// // Flipping the operands in this case results in a negative span.
833    /// assert_eq!(
834    ///     tz::offset(-5).since(tz::Offset::UTC),
835    ///     -(5 * 60 * 60).seconds().fieldwise(),
836    /// );
837    /// ```
838    #[inline]
839    pub fn since(self, other: Offset) -> Span {
840        self.until(other).negate()
841    }
842
843    /// Returns an absolute duration representing the difference in time from
844    /// this offset until the given `other` offset.
845    ///
846    /// When the `other` offset is more west (i.e., more negative) of the prime
847    /// meridian than this offset, then the duration returned will be negative.
848    ///
849    /// Unlike [`Offset::until`], this returns a duration corresponding to a
850    /// 96-bit integer of nanoseconds between two offsets.
851    ///
852    /// # When should I use this versus [`Offset::until`]?
853    ///
854    /// See the type documentation for [`SignedDuration`] for the section on
855    /// when one should use [`Span`] and when one should use `SignedDuration`.
856    /// In short, use `Span` (and therefore `Offset::until`) unless you have a
857    /// specific reason to do otherwise.
858    ///
859    /// # Examples
860    ///
861    /// ```
862    /// use jiff::{tz, SignedDuration};
863    ///
864    /// assert_eq!(
865    ///     tz::offset(-5).duration_until(tz::Offset::UTC),
866    ///     SignedDuration::from_hours(5),
867    /// );
868    /// // Flipping the operands in this case results in a negative span.
869    /// assert_eq!(
870    ///     tz::Offset::UTC.duration_until(tz::offset(-5)),
871    ///     SignedDuration::from_hours(-5),
872    /// );
873    /// ```
874    #[inline]
875    pub fn duration_until(self, other: Offset) -> SignedDuration {
876        SignedDuration::offset_until(self, other)
877    }
878
879    /// This routine is identical to [`Offset::duration_until`], but the order
880    /// of the parameters is flipped.
881    ///
882    /// # Examples
883    ///
884    /// ```
885    /// use jiff::{tz, SignedDuration};
886    ///
887    /// assert_eq!(
888    ///     tz::Offset::UTC.duration_since(tz::offset(-5)),
889    ///     SignedDuration::from_hours(5),
890    /// );
891    /// assert_eq!(
892    ///     tz::offset(-5).duration_since(tz::Offset::UTC),
893    ///     SignedDuration::from_hours(-5),
894    /// );
895    /// ```
896    #[inline]
897    pub fn duration_since(self, other: Offset) -> SignedDuration {
898        SignedDuration::offset_until(other, self)
899    }
900
901    /// Returns a new offset that is rounded according to the given
902    /// configuration.
903    ///
904    /// Rounding an offset has a number of parameters, all of which are
905    /// optional. When no parameters are given, then no rounding is done, and
906    /// the offset as given is returned. That is, it's a no-op.
907    ///
908    /// As is consistent with `Offset` itself, rounding only supports units of
909    /// hours, minutes or seconds. If any other unit is provided, then an error
910    /// is returned.
911    ///
912    /// The parameters are, in brief:
913    ///
914    /// * [`OffsetRound::smallest`] sets the smallest [`Unit`] that is allowed
915    /// to be non-zero in the offset returned. By default, it is set to
916    /// [`Unit::Second`], i.e., no rounding occurs. When the smallest unit is
917    /// set to something bigger than seconds, then the non-zero units in the
918    /// offset smaller than the smallest unit are used to determine how the
919    /// offset should be rounded. For example, rounding `+01:59` to the nearest
920    /// hour using the default rounding mode would produce `+02:00`.
921    /// * [`OffsetRound::mode`] determines how to handle the remainder
922    /// when rounding. The default is [`RoundMode::HalfExpand`], which
923    /// corresponds to how you were likely taught to round in school.
924    /// Alternative modes, like [`RoundMode::Trunc`], exist too. For example,
925    /// a truncating rounding of `+01:59` to the nearest hour would
926    /// produce `+01:00`.
927    /// * [`OffsetRound::increment`] sets the rounding granularity to
928    /// use for the configured smallest unit. For example, if the smallest unit
929    /// is minutes and the increment is `15`, then the offset returned will
930    /// always have its minute component set to a multiple of `15`.
931    ///
932    /// # Errors
933    ///
934    /// In general, there are two main ways for rounding to fail: an improper
935    /// configuration like trying to round an offset to the nearest unit other
936    /// than hours/minutes/seconds, or when overflow occurs. Overflow can occur
937    /// when the offset would exceed the minimum or maximum `Offset` values.
938    /// Typically, this can only realistically happen if the offset before
939    /// rounding is already close to its minimum or maximum value.
940    ///
941    /// # Example: rounding to the nearest multiple of 15 minutes
942    ///
943    /// Most time zone offsets fall on an hour boundary, but some fall on the
944    /// half-hour or even 15 minute boundary:
945    ///
946    /// ```
947    /// use jiff::{tz::Offset, Unit};
948    ///
949    /// let offset = Offset::from_seconds(-(44 * 60 + 30)).unwrap();
950    /// let rounded = offset.round((Unit::Minute, 15))?;
951    /// assert_eq!(rounded, Offset::from_seconds(-45 * 60).unwrap());
952    ///
953    /// # Ok::<(), Box<dyn std::error::Error>>(())
954    /// ```
955    ///
956    /// # Example: rounding can fail via overflow
957    ///
958    /// ```
959    /// use jiff::{tz::Offset, Unit};
960    ///
961    /// assert_eq!(Offset::MAX.to_string(), "+25:59:59");
962    /// assert_eq!(
963    ///     Offset::MAX.round(Unit::Minute).unwrap_err().to_string(),
964    ///     "rounding time zone offset resulted in a duration that overflows",
965    /// );
966    /// ```
967    #[inline]
968    pub fn round<R: Into<OffsetRound>>(
969        self,
970        options: R,
971    ) -> Result<Offset, Error> {
972        let options: OffsetRound = options.into();
973        options.round(self)
974    }
975}
976
977impl Offset {
978    /// This creates an `Offset` via hours/minutes/seconds components.
979    ///
980    /// Currently, it exists because it's convenient for use in tests.
981    ///
982    /// I originally wanted to expose this in the public API, but I couldn't
983    /// decide on how I wanted to treat signedness. There are a variety of
984    /// choices:
985    ///
986    /// * Require all values to be positive, and ask the caller to use
987    /// `-offset` to negate it.
988    /// * Require all values to have the same sign. If any differs, either
989    /// panic or return an error.
990    /// * If any have a negative sign, then behave as if all have a negative
991    /// sign.
992    /// * Permit any combination of sign and combine them correctly.
993    /// Similar to how `std::time::Duration::new(-1s, 1ns)` is turned into
994    /// `-999,999,999ns`.
995    ///
996    /// I think the last option is probably the right behavior, but also the
997    /// most annoying to implement. But if someone wants to take a crack at it,
998    /// a PR is welcome.
999    #[cfg(test)]
1000    #[inline]
1001    pub(crate) const fn hms(hours: i8, minutes: i8, seconds: i8) -> Offset {
1002        let total = (hours as i32 * 60 * 60)
1003            + (minutes as i32 * 60)
1004            + (seconds as i32);
1005        Offset { span: t::SpanZoneOffset::new_unchecked(total) }
1006    }
1007
1008    #[inline]
1009    pub(crate) fn from_hours_ranged(
1010        hours: impl RInto<t::SpanZoneOffsetHours>,
1011    ) -> Offset {
1012        let hours: t::SpanZoneOffset = hours.rinto().rinto();
1013        Offset::from_seconds_ranged(hours * t::SECONDS_PER_HOUR)
1014    }
1015
1016    #[inline]
1017    pub(crate) fn from_seconds_ranged(
1018        seconds: impl RInto<t::SpanZoneOffset>,
1019    ) -> Offset {
1020        Offset { span: seconds.rinto() }
1021    }
1022
1023    /*
1024    #[inline]
1025    pub(crate) fn from_ioffset(ioff: Composite<IOffset>) -> Offset {
1026        let span = rangeint::uncomposite!(ioff, c => (c.second));
1027        Offset { span: span.to_rint() }
1028    }
1029    */
1030
1031    #[inline]
1032    pub(crate) fn to_ioffset(self) -> Composite<IOffset> {
1033        rangeint::composite! {
1034            (second = self.span) => {
1035                IOffset { second }
1036            }
1037        }
1038    }
1039
1040    #[inline]
1041    pub(crate) const fn from_ioffset_const(ioff: IOffset) -> Offset {
1042        Offset::from_seconds_unchecked(ioff.second)
1043    }
1044
1045    #[inline]
1046    pub(crate) const fn from_seconds_unchecked(second: i32) -> Offset {
1047        Offset { span: t::SpanZoneOffset::new_unchecked(second) }
1048    }
1049
1050    /*
1051    #[inline]
1052    pub(crate) const fn to_ioffset_const(self) -> IOffset {
1053        IOffset { second: self.span.get_unchecked() }
1054    }
1055    */
1056
1057    #[inline]
1058    pub(crate) const fn seconds_ranged(self) -> t::SpanZoneOffset {
1059        self.span
1060    }
1061
1062    #[inline]
1063    pub(crate) fn part_hours_ranged(self) -> t::SpanZoneOffsetHours {
1064        self.span.div_ceil(t::SECONDS_PER_HOUR).rinto()
1065    }
1066
1067    #[inline]
1068    pub(crate) fn part_minutes_ranged(self) -> t::SpanZoneOffsetMinutes {
1069        self.span
1070            .div_ceil(t::SECONDS_PER_MINUTE)
1071            .rem_ceil(t::MINUTES_PER_HOUR)
1072            .rinto()
1073    }
1074
1075    #[inline]
1076    pub(crate) fn part_seconds_ranged(self) -> t::SpanZoneOffsetSeconds {
1077        self.span.rem_ceil(t::SECONDS_PER_MINUTE).rinto()
1078    }
1079
1080    #[inline]
1081    pub(crate) fn to_array_str(&self) -> ArrayStr<9> {
1082        use core::fmt::Write;
1083
1084        let mut dst = ArrayStr::new("").unwrap();
1085        // OK because the string representation of an offset
1086        // can never exceed 9 bytes. The longest possible, e.g.,
1087        // is `-25:59:59`.
1088        write!(&mut dst, "{}", self).unwrap();
1089        dst
1090    }
1091
1092    /// Round this offset to the nearest minute and returns the hour/minute
1093    /// components as unsigned integers.
1094    ///
1095    /// Generally speaking, the second component on an offset is always zero.
1096    /// There are _some_ cases in the tzdb where this isn't true (like
1097    /// `Africa/Monrovia` before `1972-01-07`), but virtually all time zones
1098    /// use offsets with whole hours. Some go to whole minutes. The only other
1099    /// way to get non-zero seconds is to explicitly use a fixed offset.
1100    ///
1101    /// A pathological case is the minimum or maximum offset. In this case,
1102    /// truncation is used instead of rounding to the nearest whole minute.
1103    #[inline]
1104    pub(crate) fn round_to_nearest_minute(self) -> (u8, u8) {
1105        #[inline(never)]
1106        #[cold]
1107        fn round(mut hours: u8, mut minutes: u8) -> (u8, u8) {
1108            const MAX_HOURS: u8 =
1109                t::SpanZoneOffsetHours::MAX_REPR.unsigned_abs();
1110            const MAX_MINS: u8 =
1111                t::SpanZoneOffsetMinutes::MAX_REPR.unsigned_abs();
1112
1113            if minutes == 59 {
1114                hours += 1;
1115                minutes = 0;
1116                // An edge case: if rounding results in an offset beyond
1117                // Jiff's boundaries, then we truncate to the max (or min)
1118                // offset supported.
1119                if hours > MAX_HOURS {
1120                    hours = MAX_HOURS;
1121                    minutes = MAX_MINS;
1122                }
1123            } else {
1124                minutes += 1;
1125            }
1126            (hours, minutes)
1127        }
1128
1129        let total_seconds = self.seconds().unsigned_abs();
1130        let hours = (total_seconds / (60 * 60)) as u8;
1131        let minutes = ((total_seconds / 60) % 60) as u8;
1132        let seconds = (total_seconds % 60) as u8;
1133
1134        // RFCs 2822, 3339 and 9557 require that time zone offsets are an
1135        // integral number of minutes. While rounding based on seconds doesn't
1136        // seem clearly indicated, the `1937-01-01T12:00:27.87+00:20` example
1137        // in RFC 3339 seems to suggest that the number of minutes should be
1138        // "as close as possible" to the actual offset. So we just do basic
1139        // rounding here.
1140        if seconds >= 30 {
1141            return round(hours, minutes);
1142        }
1143        (hours, minutes)
1144    }
1145}
1146
1147impl core::fmt::Debug for Offset {
1148    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
1149        let sign = if self.seconds_ranged() < C(0) { "-" } else { "" };
1150        write!(
1151            f,
1152            "{sign}{:02}:{:02}:{:02}",
1153            self.part_hours_ranged().abs(),
1154            self.part_minutes_ranged().abs(),
1155            self.part_seconds_ranged().abs(),
1156        )
1157    }
1158}
1159
1160impl core::fmt::Display for Offset {
1161    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
1162        let sign = if self.span < C(0) { "-" } else { "+" };
1163        let hours = self.part_hours_ranged().abs().get();
1164        let minutes = self.part_minutes_ranged().abs().get();
1165        let seconds = self.part_seconds_ranged().abs().get();
1166        if hours == 0 && minutes == 0 && seconds == 0 {
1167            f.write_str("+00")
1168        } else if hours != 0 && minutes == 0 && seconds == 0 {
1169            write!(f, "{sign}{hours:02}")
1170        } else if minutes != 0 && seconds == 0 {
1171            write!(f, "{sign}{hours:02}:{minutes:02}")
1172        } else {
1173            write!(f, "{sign}{hours:02}:{minutes:02}:{seconds:02}")
1174        }
1175    }
1176}
1177
1178/// Adds a span of time to an offset. This panics on overflow.
1179///
1180/// For checked arithmetic, see [`Offset::checked_add`].
1181impl Add<Span> for Offset {
1182    type Output = Offset;
1183
1184    #[inline]
1185    fn add(self, rhs: Span) -> Offset {
1186        self.checked_add(rhs)
1187            .expect("adding span to offset should not overflow")
1188    }
1189}
1190
1191/// Adds a span of time to an offset in place. This panics on overflow.
1192///
1193/// For checked arithmetic, see [`Offset::checked_add`].
1194impl AddAssign<Span> for Offset {
1195    #[inline]
1196    fn add_assign(&mut self, rhs: Span) {
1197        *self = self.add(rhs);
1198    }
1199}
1200
1201/// Subtracts a span of time from an offset. This panics on overflow.
1202///
1203/// For checked arithmetic, see [`Offset::checked_sub`].
1204impl Sub<Span> for Offset {
1205    type Output = Offset;
1206
1207    #[inline]
1208    fn sub(self, rhs: Span) -> Offset {
1209        self.checked_sub(rhs)
1210            .expect("subtracting span from offsetsshould not overflow")
1211    }
1212}
1213
1214/// Subtracts a span of time from an offset in place. This panics on overflow.
1215///
1216/// For checked arithmetic, see [`Offset::checked_sub`].
1217impl SubAssign<Span> for Offset {
1218    #[inline]
1219    fn sub_assign(&mut self, rhs: Span) {
1220        *self = self.sub(rhs);
1221    }
1222}
1223
1224/// Computes the span of time between two offsets.
1225///
1226/// This will return a negative span when the offset being subtracted is
1227/// greater (i.e., more east with respect to the prime meridian).
1228impl Sub for Offset {
1229    type Output = Span;
1230
1231    #[inline]
1232    fn sub(self, rhs: Offset) -> Span {
1233        self.since(rhs)
1234    }
1235}
1236
1237/// Adds a signed duration of time to an offset. This panics on overflow.
1238///
1239/// For checked arithmetic, see [`Offset::checked_add`].
1240impl Add<SignedDuration> for Offset {
1241    type Output = Offset;
1242
1243    #[inline]
1244    fn add(self, rhs: SignedDuration) -> Offset {
1245        self.checked_add(rhs)
1246            .expect("adding signed duration to offset should not overflow")
1247    }
1248}
1249
1250/// Adds a signed duration of time to an offset in place. This panics on
1251/// overflow.
1252///
1253/// For checked arithmetic, see [`Offset::checked_add`].
1254impl AddAssign<SignedDuration> for Offset {
1255    #[inline]
1256    fn add_assign(&mut self, rhs: SignedDuration) {
1257        *self = self.add(rhs);
1258    }
1259}
1260
1261/// Subtracts a signed duration of time from an offset. This panics on
1262/// overflow.
1263///
1264/// For checked arithmetic, see [`Offset::checked_sub`].
1265impl Sub<SignedDuration> for Offset {
1266    type Output = Offset;
1267
1268    #[inline]
1269    fn sub(self, rhs: SignedDuration) -> Offset {
1270        self.checked_sub(rhs).expect(
1271            "subtracting signed duration from offsetsshould not overflow",
1272        )
1273    }
1274}
1275
1276/// Subtracts a signed duration of time from an offset in place. This panics on
1277/// overflow.
1278///
1279/// For checked arithmetic, see [`Offset::checked_sub`].
1280impl SubAssign<SignedDuration> for Offset {
1281    #[inline]
1282    fn sub_assign(&mut self, rhs: SignedDuration) {
1283        *self = self.sub(rhs);
1284    }
1285}
1286
1287/// Adds an unsigned duration of time to an offset. This panics on overflow.
1288///
1289/// For checked arithmetic, see [`Offset::checked_add`].
1290impl Add<UnsignedDuration> for Offset {
1291    type Output = Offset;
1292
1293    #[inline]
1294    fn add(self, rhs: UnsignedDuration) -> Offset {
1295        self.checked_add(rhs)
1296            .expect("adding unsigned duration to offset should not overflow")
1297    }
1298}
1299
1300/// Adds an unsigned duration of time to an offset in place. This panics on
1301/// overflow.
1302///
1303/// For checked arithmetic, see [`Offset::checked_add`].
1304impl AddAssign<UnsignedDuration> for Offset {
1305    #[inline]
1306    fn add_assign(&mut self, rhs: UnsignedDuration) {
1307        *self = self.add(rhs);
1308    }
1309}
1310
1311/// Subtracts an unsigned duration of time from an offset. This panics on
1312/// overflow.
1313///
1314/// For checked arithmetic, see [`Offset::checked_sub`].
1315impl Sub<UnsignedDuration> for Offset {
1316    type Output = Offset;
1317
1318    #[inline]
1319    fn sub(self, rhs: UnsignedDuration) -> Offset {
1320        self.checked_sub(rhs).expect(
1321            "subtracting unsigned duration from offsetsshould not overflow",
1322        )
1323    }
1324}
1325
1326/// Subtracts an unsigned duration of time from an offset in place. This panics
1327/// on overflow.
1328///
1329/// For checked arithmetic, see [`Offset::checked_sub`].
1330impl SubAssign<UnsignedDuration> for Offset {
1331    #[inline]
1332    fn sub_assign(&mut self, rhs: UnsignedDuration) {
1333        *self = self.sub(rhs);
1334    }
1335}
1336
1337/// Negate this offset.
1338///
1339/// A positive offset becomes negative and vice versa. This is a no-op for the
1340/// zero offset.
1341///
1342/// This never panics.
1343impl Neg for Offset {
1344    type Output = Offset;
1345
1346    #[inline]
1347    fn neg(self) -> Offset {
1348        self.negate()
1349    }
1350}
1351
1352/// Converts a `SignedDuration` to a time zone offset.
1353///
1354/// If the signed duration has fractional seconds, then it is automatically
1355/// rounded to the nearest second. (Because an `Offset` has only second
1356/// precision.)
1357///
1358/// # Errors
1359///
1360/// This returns an error if the duration overflows the limits of an `Offset`.
1361///
1362/// # Example
1363///
1364/// ```
1365/// use jiff::{tz::{self, Offset}, SignedDuration};
1366///
1367/// let sdur = SignedDuration::from_secs(-5 * 60 * 60);
1368/// let offset = Offset::try_from(sdur)?;
1369/// assert_eq!(offset, tz::offset(-5));
1370///
1371/// // Sub-seconds results in rounded.
1372/// let sdur = SignedDuration::new(-5 * 60 * 60, -500_000_000);
1373/// let offset = Offset::try_from(sdur)?;
1374/// assert_eq!(offset, tz::Offset::from_seconds(-(5 * 60 * 60 + 1)).unwrap());
1375///
1376/// # Ok::<(), Box<dyn std::error::Error>>(())
1377/// ```
1378impl TryFrom<SignedDuration> for Offset {
1379    type Error = Error;
1380
1381    fn try_from(sdur: SignedDuration) -> Result<Offset, Error> {
1382        let mut seconds = sdur.as_secs();
1383        let subsec = sdur.subsec_nanos();
1384        if subsec >= 500_000_000 {
1385            seconds = seconds.saturating_add(1);
1386        } else if subsec <= -500_000_000 {
1387            seconds = seconds.saturating_sub(1);
1388        }
1389        let seconds =
1390            i32::try_from(seconds).map_err(|_| E::OverflowSignedDuration)?;
1391        Offset::from_seconds(seconds)
1392            .map_err(|_| Error::from(E::OverflowSignedDuration))
1393    }
1394}
1395
1396/// Options for [`Offset::checked_add`] and [`Offset::checked_sub`].
1397///
1398/// This type provides a way to ergonomically add one of a few different
1399/// duration types to a [`Offset`].
1400///
1401/// The main way to construct values of this type is with its `From` trait
1402/// implementations:
1403///
1404/// * `From<Span> for OffsetArithmetic` adds (or subtracts) the given span to
1405/// the receiver offset.
1406/// * `From<SignedDuration> for OffsetArithmetic` adds (or subtracts)
1407/// the given signed duration to the receiver offset.
1408/// * `From<std::time::Duration> for OffsetArithmetic` adds (or subtracts)
1409/// the given unsigned duration to the receiver offset.
1410///
1411/// # Example
1412///
1413/// ```
1414/// use std::time::Duration;
1415///
1416/// use jiff::{tz::offset, SignedDuration, ToSpan};
1417///
1418/// let off = offset(-10);
1419/// assert_eq!(off.checked_add(11.hours())?, offset(1));
1420/// assert_eq!(off.checked_add(SignedDuration::from_hours(11))?, offset(1));
1421/// assert_eq!(off.checked_add(Duration::from_secs(11 * 60 * 60))?, offset(1));
1422///
1423/// # Ok::<(), Box<dyn std::error::Error>>(())
1424/// ```
1425#[derive(Clone, Copy, Debug)]
1426pub struct OffsetArithmetic {
1427    duration: Duration,
1428}
1429
1430impl OffsetArithmetic {
1431    #[inline]
1432    fn checked_add(self, offset: Offset) -> Result<Offset, Error> {
1433        match self.duration.to_signed()? {
1434            SDuration::Span(span) => offset.checked_add_span(span),
1435            SDuration::Absolute(sdur) => offset.checked_add_duration(sdur),
1436        }
1437    }
1438
1439    #[inline]
1440    fn checked_neg(self) -> Result<OffsetArithmetic, Error> {
1441        let duration = self.duration.checked_neg()?;
1442        Ok(OffsetArithmetic { duration })
1443    }
1444
1445    #[inline]
1446    fn is_negative(&self) -> bool {
1447        self.duration.is_negative()
1448    }
1449}
1450
1451impl From<Span> for OffsetArithmetic {
1452    fn from(span: Span) -> OffsetArithmetic {
1453        let duration = Duration::from(span);
1454        OffsetArithmetic { duration }
1455    }
1456}
1457
1458impl From<SignedDuration> for OffsetArithmetic {
1459    fn from(sdur: SignedDuration) -> OffsetArithmetic {
1460        let duration = Duration::from(sdur);
1461        OffsetArithmetic { duration }
1462    }
1463}
1464
1465impl From<UnsignedDuration> for OffsetArithmetic {
1466    fn from(udur: UnsignedDuration) -> OffsetArithmetic {
1467        let duration = Duration::from(udur);
1468        OffsetArithmetic { duration }
1469    }
1470}
1471
1472impl<'a> From<&'a Span> for OffsetArithmetic {
1473    fn from(span: &'a Span) -> OffsetArithmetic {
1474        OffsetArithmetic::from(*span)
1475    }
1476}
1477
1478impl<'a> From<&'a SignedDuration> for OffsetArithmetic {
1479    fn from(sdur: &'a SignedDuration) -> OffsetArithmetic {
1480        OffsetArithmetic::from(*sdur)
1481    }
1482}
1483
1484impl<'a> From<&'a UnsignedDuration> for OffsetArithmetic {
1485    fn from(udur: &'a UnsignedDuration) -> OffsetArithmetic {
1486        OffsetArithmetic::from(*udur)
1487    }
1488}
1489
1490/// Options for [`Offset::round`].
1491///
1492/// This type provides a way to configure the rounding of an offset. This
1493/// includes setting the smallest unit (i.e., the unit to round), the rounding
1494/// increment and the rounding mode (e.g., "ceil" or "truncate").
1495///
1496/// [`Offset::round`] accepts anything that implements
1497/// `Into<OffsetRound>`. There are a few key trait implementations that
1498/// make this convenient:
1499///
1500/// * `From<Unit> for OffsetRound` will construct a rounding
1501/// configuration where the smallest unit is set to the one given.
1502/// * `From<(Unit, i64)> for OffsetRound` will construct a rounding
1503/// configuration where the smallest unit and the rounding increment are set to
1504/// the ones given.
1505///
1506/// In order to set other options (like the rounding mode), one must explicitly
1507/// create a `OffsetRound` and pass it to `Offset::round`.
1508///
1509/// # Example
1510///
1511/// This example shows how to always round up to the nearest half-hour:
1512///
1513/// ```
1514/// use jiff::{tz::{Offset, OffsetRound}, RoundMode, Unit};
1515///
1516/// let offset = Offset::from_seconds(4 * 60 * 60 + 17 * 60).unwrap();
1517/// let rounded = offset.round(
1518///     OffsetRound::new()
1519///         .smallest(Unit::Minute)
1520///         .increment(30)
1521///         .mode(RoundMode::Expand),
1522/// )?;
1523/// assert_eq!(rounded, Offset::from_seconds(4 * 60 * 60 + 30 * 60).unwrap());
1524///
1525/// # Ok::<(), Box<dyn std::error::Error>>(())
1526/// ```
1527#[derive(Clone, Copy, Debug)]
1528pub struct OffsetRound(SignedDurationRound);
1529
1530impl OffsetRound {
1531    /// Create a new default configuration for rounding a time zone offset via
1532    /// [`Offset::round`].
1533    ///
1534    /// The default configuration does no rounding.
1535    #[inline]
1536    pub fn new() -> OffsetRound {
1537        OffsetRound(SignedDurationRound::new().smallest(Unit::Second))
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(self.0.smallest(unit))
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(self.0.mode(mode))
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    /// The rounding increment must divide evenly into the next highest unit
1609    /// after the smallest unit configured (and must not be equivalent to
1610    /// it). For example, if the smallest unit is [`Unit::Second`], then
1611    /// *some* of the valid values for the rounding increment are `1`, `2`,
1612    /// `4`, `5`, `15` and `30`. Namely, any integer that divides evenly into
1613    /// `60` seconds since there are `60` seconds in the next highest unit
1614    /// (minutes).
1615    ///
1616    /// # Example
1617    ///
1618    /// This shows how to round an offset to the nearest 30 minute increment:
1619    ///
1620    /// ```
1621    /// use jiff::{tz::Offset, Unit};
1622    ///
1623    /// let offset = Offset::from_seconds(4 * 60 * 60 + 15 * 60).unwrap();
1624    /// assert_eq!(
1625    ///     offset.round((Unit::Minute, 30))?,
1626    ///     Offset::from_seconds(4 * 60 * 60 + 30 * 60).unwrap(),
1627    /// );
1628    ///
1629    /// # Ok::<(), Box<dyn std::error::Error>>(())
1630    /// ```
1631    #[inline]
1632    pub fn increment(self, increment: i64) -> OffsetRound {
1633        OffsetRound(self.0.increment(increment))
1634    }
1635
1636    /// Does the actual offset rounding.
1637    fn round(&self, offset: Offset) -> Result<Offset, Error> {
1638        let smallest = self.0.get_smallest();
1639        if !(Unit::Second <= smallest && smallest <= Unit::Hour) {
1640            return Err(Error::from(E::RoundInvalidUnit { unit: smallest }));
1641        }
1642        let rounded_sdur = SignedDuration::from(offset).round(self.0)?;
1643        Offset::try_from(rounded_sdur)
1644            .map_err(|_| Error::from(E::RoundOverflow))
1645    }
1646}
1647
1648impl Default for OffsetRound {
1649    fn default() -> OffsetRound {
1650        OffsetRound::new()
1651    }
1652}
1653
1654impl From<Unit> for OffsetRound {
1655    fn from(unit: Unit) -> OffsetRound {
1656        OffsetRound::default().smallest(unit)
1657    }
1658}
1659
1660impl From<(Unit, i64)> for OffsetRound {
1661    fn from((unit, increment): (Unit, i64)) -> OffsetRound {
1662        OffsetRound::default().smallest(unit).increment(increment)
1663    }
1664}
1665
1666/// Configuration for resolving disparities between an offset and a time zone.
1667///
1668/// A conflict between an offset and a time zone most commonly appears in a
1669/// datetime string. For example, `2024-06-14T17:30-05[America/New_York]`
1670/// has a definitive inconsistency between the reported offset (`-05`) and
1671/// the time zone (`America/New_York`), because at this time in New York,
1672/// daylight saving time (DST) was in effect. In New York in the year 2024,
1673/// DST corresponded to the UTC offset `-04`.
1674///
1675/// Other conflict variations exist. For example, in 2019, Brazil abolished
1676/// DST completely. But if one were to create a datetime for 2020 in 2018, that
1677/// datetime in 2020 would reflect the DST rules as they exist in 2018. That
1678/// could in turn result in a datetime with an offset that is incorrect with
1679/// respect to the rules in 2019.
1680///
1681/// For this reason, this crate exposes a few ways of resolving these
1682/// conflicts. It is most commonly used as configuration for parsing
1683/// [`Zoned`](crate::Zoned) values via
1684/// [`fmt::temporal::DateTimeParser::offset_conflict`](crate::fmt::temporal::DateTimeParser::offset_conflict). But this configuration can also be used directly via
1685/// [`OffsetConflict::resolve`].
1686///
1687/// The default value is `OffsetConflict::Reject`, which results in an
1688/// error being returned if the offset and a time zone are not in agreement.
1689/// This is the default so that Jiff does not automatically make silent choices
1690/// about whether to prefer the time zone or the offset. The
1691/// [`fmt::temporal::DateTimeParser::parse_zoned_with`](crate::fmt::temporal::DateTimeParser::parse_zoned_with)
1692/// documentation shows an example demonstrating its utility in the face
1693/// of changes in the law, such as the abolition of daylight saving time.
1694/// By rejecting such things, one can ensure that the original timestamp is
1695/// preserved or else an error occurs.
1696///
1697/// This enum is non-exhaustive so that other forms of offset conflicts may be
1698/// added in semver compatible releases.
1699///
1700/// # Example
1701///
1702/// This example shows how to always use the time zone even if the offset is
1703/// wrong.
1704///
1705/// ```
1706/// use jiff::{civil::date, tz};
1707///
1708/// let dt = date(2024, 6, 14).at(17, 30, 0, 0);
1709/// let offset = tz::offset(-5); // wrong! should be -4
1710/// let newyork = tz::db().get("America/New_York")?;
1711///
1712/// // The default conflict resolution, 'Reject', will error.
1713/// let result = tz::OffsetConflict::Reject
1714///     .resolve(dt, offset, newyork.clone());
1715/// assert!(result.is_err());
1716///
1717/// // But we can change it to always prefer the time zone.
1718/// let zdt = tz::OffsetConflict::AlwaysTimeZone
1719///     .resolve(dt, offset, newyork.clone())?
1720///     .unambiguous()?;
1721/// assert_eq!(zdt.datetime(), date(2024, 6, 14).at(17, 30, 0, 0));
1722/// // The offset has been corrected automatically.
1723/// assert_eq!(zdt.offset(), tz::offset(-4));
1724///
1725/// # Ok::<(), Box<dyn std::error::Error>>(())
1726/// ```
1727///
1728/// # Example: parsing
1729///
1730/// This example shows how to set the offset conflict resolution configuration
1731/// while parsing a [`Zoned`](crate::Zoned) datetime. In this example, we
1732/// always prefer the offset, even if it conflicts with the time zone.
1733///
1734/// ```
1735/// use jiff::{civil::date, fmt::temporal::DateTimeParser, tz};
1736///
1737/// static PARSER: DateTimeParser = DateTimeParser::new()
1738///     .offset_conflict(tz::OffsetConflict::AlwaysOffset);
1739///
1740/// let zdt = PARSER.parse_zoned("2024-06-14T17:30-05[America/New_York]")?;
1741/// // The time *and* offset have been corrected. The offset given was invalid,
1742/// // so it cannot be kept, but the timestamp returned is equivalent to
1743/// // `2024-06-14T17:30-05`. It is just adjusted automatically to be correct
1744/// // in the `America/New_York` time zone.
1745/// assert_eq!(zdt.datetime(), date(2024, 6, 14).at(18, 30, 0, 0));
1746/// assert_eq!(zdt.offset(), tz::offset(-4));
1747///
1748/// # Ok::<(), Box<dyn std::error::Error>>(())
1749/// ```
1750#[derive(Clone, Copy, Debug, Default)]
1751#[non_exhaustive]
1752pub enum OffsetConflict {
1753    /// When the offset and time zone are in conflict, this will always use
1754    /// the offset to interpret the date time.
1755    ///
1756    /// When resolving to a [`AmbiguousZoned`], the time zone attached
1757    /// to the timestamp will still be the same as the time zone given. The
1758    /// difference here is that the offset will be adjusted such that it is
1759    /// correct for the given time zone. However, the timestamp itself will
1760    /// always match the datetime and offset given (and which is always
1761    /// unambiguous).
1762    ///
1763    /// Basically, you should use this option when you want to keep the exact
1764    /// time unchanged (as indicated by the datetime and offset), even if it
1765    /// means a change to civil time.
1766    AlwaysOffset,
1767    /// When the offset and time zone are in conflict, this will always use
1768    /// the time zone to interpret the date time.
1769    ///
1770    /// When resolving to an [`AmbiguousZoned`], the offset attached to the
1771    /// timestamp will always be determined by only looking at the time zone.
1772    /// This in turn implies that the timestamp returned could be ambiguous,
1773    /// since this conflict resolution strategy specifically ignores the
1774    /// offset. (And, we're only at this point because the offset is not
1775    /// possible for the given time zone, so it can't be used in concert with
1776    /// the time zone anyway.) This is unlike the `AlwaysOffset` strategy where
1777    /// the timestamp returned is guaranteed to be unambiguous.
1778    ///
1779    /// You should use this option when you want to keep the civil time
1780    /// unchanged even if it means a change to the exact time.
1781    AlwaysTimeZone,
1782    /// Always attempt to use the offset to resolve a datetime to a timestamp,
1783    /// unless the offset is invalid for the provided time zone. In that case,
1784    /// use the time zone. When the time zone is used, it's possible for an
1785    /// ambiguous datetime to be returned.
1786    ///
1787    /// See [`ZonedWith::offset_conflict`](crate::ZonedWith::offset_conflict)
1788    /// for an example of when this strategy is useful.
1789    PreferOffset,
1790    /// When the offset and time zone are in conflict, this strategy always
1791    /// results in conflict resolution returning an error.
1792    ///
1793    /// This is the default since a conflict between the offset and the time
1794    /// zone usually implies an invalid datetime in some way.
1795    #[default]
1796    Reject,
1797}
1798
1799impl OffsetConflict {
1800    /// Resolve a potential conflict between an [`Offset`] and a [`TimeZone`].
1801    ///
1802    /// # Errors
1803    ///
1804    /// This returns an error if this would have returned a timestamp outside
1805    /// of its minimum and maximum values.
1806    ///
1807    /// This can also return an error when using the [`OffsetConflict::Reject`]
1808    /// strategy. Namely, when using the `Reject` strategy, any offset that is
1809    /// not compatible with the given datetime and time zone will always result
1810    /// in an error.
1811    ///
1812    /// # Example
1813    ///
1814    /// This example shows how each of the different conflict resolution
1815    /// strategies are applied.
1816    ///
1817    /// ```
1818    /// use jiff::{civil::date, tz};
1819    ///
1820    /// let dt = date(2024, 6, 14).at(17, 30, 0, 0);
1821    /// let offset = tz::offset(-5); // wrong! should be -4
1822    /// let newyork = tz::db().get("America/New_York")?;
1823    ///
1824    /// // Here, we use the offset and ignore the time zone.
1825    /// let zdt = tz::OffsetConflict::AlwaysOffset
1826    ///     .resolve(dt, offset, newyork.clone())?
1827    ///     .unambiguous()?;
1828    /// // The datetime (and offset) have been corrected automatically
1829    /// // and the resulting Zoned instant corresponds precisely to
1830    /// // `2024-06-14T17:30-05[UTC]`.
1831    /// assert_eq!(zdt.to_string(), "2024-06-14T18:30:00-04:00[America/New_York]");
1832    ///
1833    /// // Here, we use the time zone and ignore the offset.
1834    /// let zdt = tz::OffsetConflict::AlwaysTimeZone
1835    ///     .resolve(dt, offset, newyork.clone())?
1836    ///     .unambiguous()?;
1837    /// // The offset has been corrected automatically and the resulting
1838    /// // Zoned instant corresponds precisely to `2024-06-14T17:30-04[UTC]`.
1839    /// // Notice how the civil time remains the same, but the exact instant
1840    /// // has changed!
1841    /// assert_eq!(zdt.to_string(), "2024-06-14T17:30:00-04:00[America/New_York]");
1842    ///
1843    /// // Here, we prefer the offset, but fall back to the time zone.
1844    /// // In this example, it has the same behavior as `AlwaysTimeZone`.
1845    /// let zdt = tz::OffsetConflict::PreferOffset
1846    ///     .resolve(dt, offset, newyork.clone())?
1847    ///     .unambiguous()?;
1848    /// assert_eq!(zdt.to_string(), "2024-06-14T17:30:00-04:00[America/New_York]");
1849    ///
1850    /// // The default conflict resolution, 'Reject', will error.
1851    /// let result = tz::OffsetConflict::Reject
1852    ///     .resolve(dt, offset, newyork.clone());
1853    /// assert!(result.is_err());
1854    ///
1855    /// # Ok::<(), Box<dyn std::error::Error>>(())
1856    /// ```
1857    pub fn resolve(
1858        self,
1859        dt: civil::DateTime,
1860        offset: Offset,
1861        tz: TimeZone,
1862    ) -> Result<AmbiguousZoned, Error> {
1863        self.resolve_with(dt, offset, tz, |off1, off2| off1 == off2)
1864    }
1865
1866    /// Resolve a potential conflict between an [`Offset`] and a [`TimeZone`]
1867    /// using the given definition of equality for an `Offset`.
1868    ///
1869    /// The equality predicate is always given a pair of offsets where the
1870    /// first is the offset given to `resolve_with` and the second is the
1871    /// offset found in the `TimeZone`.
1872    ///
1873    /// # Errors
1874    ///
1875    /// This returns an error if this would have returned a timestamp outside
1876    /// of its minimum and maximum values.
1877    ///
1878    /// This can also return an error when using the [`OffsetConflict::Reject`]
1879    /// strategy. Namely, when using the `Reject` strategy, any offset that is
1880    /// not compatible with the given datetime and time zone will always result
1881    /// in an error.
1882    ///
1883    /// # Example
1884    ///
1885    /// Unlike [`OffsetConflict::resolve`], this routine permits overriding
1886    /// the definition of equality used for comparing offsets. In
1887    /// `OffsetConflict::resolve`, exact equality is used. This can be
1888    /// troublesome in some cases when a time zone has an offset with
1889    /// fractional minutes, such as `Africa/Monrovia` before 1972.
1890    ///
1891    /// Because RFC 3339 and RFC 9557 do not support time zone offsets
1892    /// with fractional minutes, Jiff will serialize offsets with
1893    /// fractional minutes by rounding to the nearest minute. This
1894    /// will result in a different offset than what is actually
1895    /// used in the time zone. Parsing this _should_ succeed, but
1896    /// if exact offset equality is used, it won't. This is why a
1897    /// [`fmt::temporal::DateTimeParser`](crate::fmt::temporal::DateTimeParser)
1898    /// uses this routine with offset equality that rounds offsets to the
1899    /// nearest minute before comparison.
1900    ///
1901    /// ```
1902    /// use jiff::{civil::date, tz::{Offset, OffsetConflict, TimeZone}, Unit};
1903    ///
1904    /// let dt = date(1968, 2, 1).at(23, 15, 0, 0);
1905    /// let offset = Offset::from_seconds(-(44 * 60 + 30)).unwrap();
1906    /// let zdt = dt.in_tz("Africa/Monrovia")?;
1907    /// assert_eq!(zdt.offset(), offset);
1908    /// // Notice that the offset has been rounded!
1909    /// assert_eq!(zdt.to_string(), "1968-02-01T23:15:00-00:45[Africa/Monrovia]");
1910    ///
1911    /// // Now imagine parsing extracts the civil datetime, the offset and
1912    /// // the time zone, and then naively does exact offset comparison:
1913    /// let tz = TimeZone::get("Africa/Monrovia")?;
1914    /// // This is the parsed offset, which won't precisely match the actual
1915    /// // offset used by `Africa/Monrovia` at this time.
1916    /// let offset = Offset::from_seconds(-45 * 60).unwrap();
1917    /// let result = OffsetConflict::Reject.resolve(dt, offset, tz.clone());
1918    /// assert_eq!(
1919    ///     result.unwrap_err().to_string(),
1920    ///     "datetime could not resolve to a timestamp since `reject` \
1921    ///      conflict resolution was chosen, and because datetime has offset \
1922    ///      `-00:45`, but the time zone `Africa/Monrovia` for the given \
1923    ///      datetime unambiguously has offset `-00:44:30`",
1924    /// );
1925    /// let is_equal = |parsed: Offset, candidate: Offset| {
1926    ///     parsed == candidate || candidate.round(Unit::Minute).map_or(
1927    ///         parsed == candidate,
1928    ///         |candidate| parsed == candidate,
1929    ///     )
1930    /// };
1931    /// let zdt = OffsetConflict::Reject.resolve_with(
1932    ///     dt,
1933    ///     offset,
1934    ///     tz.clone(),
1935    ///     is_equal,
1936    /// )?.unambiguous()?;
1937    /// // Notice that the offset is the actual offset from the time zone:
1938    /// assert_eq!(zdt.offset(), Offset::from_seconds(-(44 * 60 + 30)).unwrap());
1939    /// // But when we serialize, the offset gets rounded. If we didn't
1940    /// // do this, we'd risk the datetime not being parsable by other
1941    /// // implementations since RFC 3339 and RFC 9557 don't support fractional
1942    /// // minutes in the offset.
1943    /// assert_eq!(zdt.to_string(), "1968-02-01T23:15:00-00:45[Africa/Monrovia]");
1944    ///
1945    /// # Ok::<(), Box<dyn std::error::Error>>(())
1946    /// ```
1947    ///
1948    /// And indeed, notice that parsing uses this same kind of offset equality
1949    /// to permit zoned datetimes whose offsets would be equivalent after
1950    /// rounding:
1951    ///
1952    /// ```
1953    /// use jiff::{tz::Offset, Zoned};
1954    ///
1955    /// let zdt: Zoned = "1968-02-01T23:15:00-00:45[Africa/Monrovia]".parse()?;
1956    /// // As above, notice that even though we parsed `-00:45` as the
1957    /// // offset, the actual offset of our zoned datetime is the correct
1958    /// // one from the time zone.
1959    /// assert_eq!(zdt.offset(), Offset::from_seconds(-(44 * 60 + 30)).unwrap());
1960    /// // And similarly, re-serializing it results in rounding the offset
1961    /// // again for compatibility with RFC 3339 and RFC 9557.
1962    /// assert_eq!(zdt.to_string(), "1968-02-01T23:15:00-00:45[Africa/Monrovia]");
1963    ///
1964    /// // And we also support parsing the actual fractional minute offset
1965    /// // as well:
1966    /// let zdt: Zoned = "1968-02-01T23:15:00-00:44:30[Africa/Monrovia]".parse()?;
1967    /// assert_eq!(zdt.offset(), Offset::from_seconds(-(44 * 60 + 30)).unwrap());
1968    /// assert_eq!(zdt.to_string(), "1968-02-01T23:15:00-00:45[Africa/Monrovia]");
1969    ///
1970    /// # Ok::<(), Box<dyn std::error::Error>>(())
1971    /// ```
1972    ///
1973    /// Rounding does not occur when the parsed offset itself contains
1974    /// sub-minute precision. In that case, exact equality is used:
1975    ///
1976    /// ```
1977    /// use jiff::Zoned;
1978    ///
1979    /// let result = "1970-06-01T00-00:45:00[Africa/Monrovia]".parse::<Zoned>();
1980    /// assert_eq!(
1981    ///     result.unwrap_err().to_string(),
1982    ///     "datetime could not resolve to a timestamp since `reject` \
1983    ///      conflict resolution was chosen, and because datetime has offset \
1984    ///      `-00:45`, but the time zone `Africa/Monrovia` for the given \
1985    ///      datetime unambiguously has offset `-00:44:30`",
1986    /// );
1987    /// ```
1988    pub fn resolve_with<F>(
1989        self,
1990        dt: civil::DateTime,
1991        offset: Offset,
1992        tz: TimeZone,
1993        is_equal: F,
1994    ) -> Result<AmbiguousZoned, Error>
1995    where
1996        F: FnMut(Offset, Offset) -> bool,
1997    {
1998        match self {
1999            // In this case, we ignore any TZ annotation (although still
2000            // require that it exists) and always use the provided offset.
2001            OffsetConflict::AlwaysOffset => {
2002                let kind = AmbiguousOffset::Unambiguous { offset };
2003                Ok(AmbiguousTimestamp::new(dt, kind).into_ambiguous_zoned(tz))
2004            }
2005            // In this case, we ignore any provided offset and always use the
2006            // time zone annotation.
2007            OffsetConflict::AlwaysTimeZone => Ok(tz.into_ambiguous_zoned(dt)),
2008            // In this case, we use the offset if it's correct, but otherwise
2009            // fall back to the time zone annotation if it's not.
2010            OffsetConflict::PreferOffset => Ok(
2011                OffsetConflict::resolve_via_prefer(dt, offset, tz, is_equal),
2012            ),
2013            // In this case, if the offset isn't possible for the provided time
2014            // zone annotation, then we return an error.
2015            OffsetConflict::Reject => {
2016                OffsetConflict::resolve_via_reject(dt, offset, tz, is_equal)
2017            }
2018        }
2019    }
2020
2021    /// Given a parsed datetime, a parsed offset and a parsed time zone, this
2022    /// attempts to resolve the datetime to a particular instant based on the
2023    /// 'prefer' strategy.
2024    ///
2025    /// In the 'prefer' strategy, we prefer to use the parsed offset to resolve
2026    /// any ambiguity in the parsed datetime and time zone, but only if the
2027    /// parsed offset is valid for the parsed datetime and time zone. If the
2028    /// parsed offset isn't valid, then it is ignored. In the case where it is
2029    /// ignored, it is possible for an ambiguous instant to be returned.
2030    fn resolve_via_prefer(
2031        dt: civil::DateTime,
2032        given: Offset,
2033        tz: TimeZone,
2034        mut is_equal: impl FnMut(Offset, Offset) -> bool,
2035    ) -> AmbiguousZoned {
2036        use crate::tz::AmbiguousOffset::*;
2037
2038        let amb = tz.to_ambiguous_timestamp(dt);
2039        match amb.offset() {
2040            // We only look for folds because we consider all offsets for gaps
2041            // to be invalid. Which is consistent with how they're treated as
2042            // `OffsetConflict::Reject`. Thus, like any other invalid offset,
2043            // we fallback to disambiguation (which is handled by the caller).
2044            Fold { before, after }
2045                if is_equal(given, before) || is_equal(given, after) =>
2046            {
2047                let kind = Unambiguous { offset: given };
2048                AmbiguousTimestamp::new(dt, kind)
2049            }
2050            _ => amb,
2051        }
2052        .into_ambiguous_zoned(tz)
2053    }
2054
2055    /// Given a parsed datetime, a parsed offset and a parsed time zone, this
2056    /// attempts to resolve the datetime to a particular instant based on the
2057    /// 'reject' strategy.
2058    ///
2059    /// That is, if the offset is not possibly valid for the given datetime and
2060    /// time zone, then this returns an error.
2061    ///
2062    /// This guarantees that on success, an unambiguous timestamp is returned.
2063    /// This occurs because if the datetime is ambiguous for the given time
2064    /// zone, then the parsed offset either matches one of the possible offsets
2065    /// (and thus provides an unambiguous choice), or it doesn't and an error
2066    /// is returned.
2067    fn resolve_via_reject(
2068        dt: civil::DateTime,
2069        given: Offset,
2070        tz: TimeZone,
2071        mut is_equal: impl FnMut(Offset, Offset) -> bool,
2072    ) -> Result<AmbiguousZoned, Error> {
2073        use crate::tz::AmbiguousOffset::*;
2074
2075        let amb = tz.to_ambiguous_timestamp(dt);
2076        match amb.offset() {
2077            Unambiguous { offset } if !is_equal(given, offset) => {
2078                Err(Error::from(E::ResolveRejectUnambiguous {
2079                    given,
2080                    offset,
2081                    tz,
2082                }))
2083            }
2084            Unambiguous { .. } => Ok(amb.into_ambiguous_zoned(tz)),
2085            Gap { before, after } => {
2086                // In `jiff 0.1`, we reported an error when we found a gap
2087                // where neither offset matched what was given. But now we
2088                // report an error whenever we find a gap, as we consider
2089                // all offsets to be invalid for the gap. This now matches
2090                // Temporal's behavior which I think is more consistent. And in
2091                // particular, this makes it more consistent with the behavior
2092                // of `PreferOffset` when a gap is found (which was also
2093                // changed to treat all offsets in a gap as invalid).
2094                //
2095                // Ref: https://github.com/tc39/proposal-temporal/issues/2892
2096                Err(Error::from(E::ResolveRejectGap {
2097                    given,
2098                    before,
2099                    after,
2100                    tz,
2101                }))
2102            }
2103            Fold { before, after }
2104                if !is_equal(given, before) && !is_equal(given, after) =>
2105            {
2106                Err(Error::from(E::ResolveRejectFold {
2107                    given,
2108                    before,
2109                    after,
2110                    tz,
2111                }))
2112            }
2113            Fold { .. } => {
2114                let kind = Unambiguous { offset: given };
2115                Ok(AmbiguousTimestamp::new(dt, kind).into_ambiguous_zoned(tz))
2116            }
2117        }
2118    }
2119}