jiff/tz/
offset.rs

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