Skip to main content

jiff_core/tz/
offset.rs

1use crate::{
2    bounds::{self as b, RangeError},
3    civil::{self, DateTime},
4    constants as c,
5    macros::{rtry, unwrapr},
6    Timestamp,
7};
8
9/// A fixed offset, in seconds, from UTC.
10#[derive(Clone, Copy, Eq, Hash, PartialEq, PartialOrd, Ord)]
11pub struct Offset {
12    seconds: i32,
13}
14
15impl Offset {
16    /// The minimum possible offset from UTC.
17    pub const MIN: Offset = Offset { seconds: b::OffsetTotalSeconds::MIN };
18
19    /// The maximum possible offset from UTC.
20    pub const MAX: Offset = Offset { seconds: b::OffsetTotalSeconds::MAX };
21
22    /// The UTC offset.
23    pub const UTC: Offset = Offset { seconds: 0 };
24
25    /// The zero offset.
26    pub const ZERO: Offset = Offset { seconds: 0 };
27
28    /// Creates a new time zone offset in a `const` context from a given number
29    /// of hours.
30    #[inline]
31    pub const fn constant(hours: i8) -> Offset {
32        unwrapr!(Offset::from_hours(hours), "invalid time zone offset hours")
33    }
34
35    /// Creates a new time zone offset in a `const` context from a given number
36    /// of seconds.
37    #[inline]
38    pub const fn constant_seconds(seconds: i32) -> Offset {
39        unwrapr!(
40            Offset::from_seconds(seconds),
41            "invalid time zone offset seconds",
42        )
43    }
44
45    /// Creates a new time zone offset from a given number of hours.
46    ///
47    /// Negative offsets correspond to time zones west of the prime meridian,
48    /// while positive offsets correspond to time zones east of the prime
49    /// meridian. Equivalently, in all cases, `civil-time - offset = UTC`.
50    #[inline]
51    pub const fn from_hours(hours: i8) -> Result<Offset, RangeError> {
52        Offset::from_seconds(hours as i32 * c::SECS_PER_HOUR_32)
53    }
54
55    /// Returns a new time zone offset from UTC given its representation in
56    /// seconds.
57    ///
58    /// An error is also returned when `seconds` is not in the range specified
59    /// by [`OffsetTotalSeconds`](b::OffsetTotalSeconds).
60    #[inline]
61    pub const fn from_seconds(seconds: i32) -> Result<Offset, RangeError> {
62        let seconds = rtry!(b::OffsetTotalSeconds::checkc(seconds as i64));
63        Ok(Offset { seconds })
64    }
65
66    /// Returns the seconds value corresponding to this time zone offset.
67    #[inline]
68    pub const fn seconds(self) -> i32 {
69        self.seconds
70    }
71
72    /// Returns the negation of this offset.
73    ///
74    /// A negative offset will become positive and vice versa. This is a no-op
75    /// if the offset is zero.
76    ///
77    /// This never panics.
78    #[inline]
79    pub const fn negate(self) -> Offset {
80        // OK because of the boundaries we enforce. `seconds` can never be
81        // `i32::MIN`.
82        Offset { seconds: -self.seconds() }
83    }
84
85    /// Returns the "sign number" or "signum" of this offset.
86    ///
87    /// The number returned is `-1` when this offset is negative,
88    /// `0` when this offset is zero and `1` when this span is positive.
89    #[inline]
90    pub const fn signum(self) -> i8 {
91        self.seconds().signum() as i8
92    }
93
94    /// Returns true if and only if this offset is positive.
95    ///
96    /// This returns false when the offset is zero or negative.
97    #[inline]
98    pub const fn is_positive(self) -> bool {
99        self.seconds() > 0
100    }
101
102    /// Returns true if and only if this offset is less than zero.
103    ///
104    /// This returns false when the offset is zero or positive.
105    #[inline]
106    pub const fn is_negative(self) -> bool {
107        self.seconds() < 0
108    }
109
110    /// Returns true if and only if this offset is zero.
111    ///
112    /// Or equivalently, when this offset corresponds to [`Offset::UTC`].
113    #[inline]
114    pub const fn is_zero(self) -> bool {
115        self.seconds() == 0
116    }
117
118    /// Adds the given number of seconds to this offset.
119    ///
120    /// If the resulting offset would be outside the an offset's boundaries,
121    /// an error is returned.
122    #[inline]
123    pub const fn checked_add(
124        self,
125        seconds: i32,
126    ) -> Result<Offset, RangeError> {
127        let seconds =
128            rtry!(b::OffsetTotalSeconds::checked_add(self.seconds(), seconds));
129        Ok(Offset { seconds })
130    }
131
132    /// Subtracts the given number of seconds from this offset.
133    ///
134    /// If the resulting offset would be outside the an offset's boundaries,
135    /// an error is returned.
136    #[inline]
137    pub const fn checked_sub(
138        self,
139        seconds: i32,
140    ) -> Result<Offset, RangeError> {
141        let seconds = rtry!(b::OffsetTotalSeconds::checkc(
142            self.seconds() as i64 - seconds as i64,
143        ));
144        Ok(Offset { seconds })
145    }
146
147    /// Returns the number of seconds from this offset to `other`.
148    #[inline]
149    pub const fn until(self, other: Offset) -> i32 {
150        other.seconds() - self.seconds()
151    }
152
153    /// Returns the number of seconds since this offset from `other`.
154    #[inline]
155    pub const fn since(self, other: Offset) -> i32 {
156        self.seconds() - other.seconds()
157    }
158
159    /// Converts a Unix timestamp with an offset to a Gregorian datetime.
160    ///
161    /// The offset should correspond to the number of seconds required to
162    /// add to this timestamp to get the local time.
163    #[inline]
164    pub const fn to_datetime(self, timestamp: Timestamp) -> civil::DateTime {
165        let offset = self;
166        let second = timestamp.as_second();
167        let mut nanosecond = timestamp.subsec_nanosecond();
168
169        // Shift second comfortably into the postive domain
170        // so that division and remainder can use unsigned math
171        // which is much faster.
172        // 30 * 400 years: 12,000 yr range > [-9,999..1970]
173        // (146097 being the number of days per 400 years).
174        const DAY_SHIFT: i32 = 30 * 146097;
175        const SEC_SHIFT: i64 = (DAY_SHIFT as i64) * 86_400;
176
177        let pos_sec = (second + (offset.seconds() as i64) + SEC_SHIFT) as u64;
178        let mut epoch_day = (pos_sec / 86_400) as i32;
179        let mut second = (pos_sec % 86_400) as i32;
180
181        if nanosecond < 0 {
182            if second > 0 {
183                second -= 1;
184                nanosecond += 1_000_000_000;
185            } else {
186                epoch_day -= 1;
187                second += 86_399;
188                nanosecond += 1_000_000_000;
189            }
190        }
191
192        epoch_day -= DAY_SHIFT;
193
194        // We should check whether having unchecked APIs
195        // would be beneficial here. In particular, the
196        // math above, coupled with the ranges allowed on
197        // `Timestamp` and `Offset` (by design) guarantee
198        // that our resulting datetime will always be in
199        // range.
200        let date = unwrapr!(
201            civil::UnixEpochDay::new(epoch_day),
202            "always valid Unix epoch day",
203        )
204        .to_date();
205        let time = unwrapr!(
206            unwrapr!(
207                civil::TimeSecond::new(second),
208                "always valid civil second time"
209            )
210            .to_time()
211            .with_subsec_nanosecond(nanosecond),
212            "always valid civil subsecond"
213        );
214        civil::DateTime::from_parts(date, time)
215    }
216
217    /// Converts the given civil datetime to a timestamp using this offset.
218    ///
219    /// # Errors
220    ///
221    /// This returns an error if this would have returned a timestamp outside
222    /// of its minimum and maximum values.
223    #[inline]
224    pub const fn to_timestamp(
225        self,
226        dt: civil::DateTime,
227    ) -> Result<Timestamp, RangeError> {
228        let offset = self;
229        let epoch_day = dt.date().to_unix_epoch_day().day();
230        let mut second = (epoch_day as i64) * c::SECS_PER_CIVIL_DAY
231            + (dt.time().to_second().second() as i64);
232        let mut nanosecond = dt.time().subsec_nanosecond();
233        second -= offset.seconds() as i64;
234        if second < 0 && nanosecond != 0 {
235            second += 1;
236            nanosecond -= c::NANOS_PER_SEC_32;
237        }
238        let second = rtry!(b::UnixEpochSeconds::checkc(second));
239        Ok(Timestamp::new_unchecked(second, nanosecond))
240    }
241}
242
243impl Offset {
244    #[inline]
245    fn part_hours(self) -> i8 {
246        (self.seconds() / c::SECS_PER_HOUR_32) as i8
247    }
248
249    #[inline]
250    fn part_minutes(self) -> i8 {
251        ((self.seconds() / c::SECS_PER_MIN_32) % c::MINS_PER_HOUR_32) as i8
252    }
253
254    #[inline]
255    fn part_seconds(self) -> i8 {
256        (self.seconds() % c::SECS_PER_MIN_32) as i8
257    }
258}
259
260/// Negate this offset.
261///
262/// A positive offset becomes negative and vice versa. This is a no-op for the
263/// zero offset.
264///
265/// This never panics.
266impl core::ops::Neg for Offset {
267    type Output = Offset;
268
269    #[inline]
270    fn neg(self) -> Offset {
271        self.negate()
272    }
273}
274
275/// Adds a number of seconds to an `Offset`.
276///
277/// # Panics
278///
279/// When adding would result in a value outside the boundaries of a
280/// `Offset`.
281impl core::ops::Add<i32> for Offset {
282    type Output = Offset;
283
284    fn add(self, seconds: i32) -> Offset {
285        self.checked_add(seconds).unwrap()
286    }
287}
288
289/// Adds a number of seconds into an `Offset`.
290///
291/// # Panics
292///
293/// When adding would result in a value outside the boundaries of a
294/// `Offset`.
295impl core::ops::AddAssign<i32> for Offset {
296    #[inline]
297    fn add_assign(&mut self, rhs: i32) {
298        *self = *self + rhs;
299    }
300}
301
302/// Subtracts a number of seconds from an `Offset`.
303///
304/// # Panics
305///
306/// When adding would result in a value outside the boundaries of a
307/// `Offset`.
308impl core::ops::Sub<i32> for Offset {
309    type Output = Offset;
310
311    fn sub(self, seconds: i32) -> Offset {
312        self.checked_sub(seconds).unwrap()
313    }
314}
315
316/// Subtracts a number of seconds from an `Offset` in place.
317///
318/// # Panics
319///
320/// When adding would result in a value outside the boundaries of a
321/// `Offset`.
322impl core::ops::SubAssign<i32> for Offset {
323    #[inline]
324    fn sub_assign(&mut self, rhs: i32) {
325        *self = *self - rhs;
326    }
327}
328
329impl core::fmt::Debug for Offset {
330    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
331        let sign = if self.is_negative() { "-" } else { "" };
332        write!(
333            f,
334            "{sign}{:02}:{:02}:{:02}",
335            self.part_hours().unsigned_abs(),
336            self.part_minutes().unsigned_abs(),
337            self.part_seconds().unsigned_abs(),
338        )
339    }
340}
341
342impl core::fmt::Display for Offset {
343    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
344        let sign = if self.is_negative() { "-" } else { "+" };
345        let hours = self.part_hours().unsigned_abs();
346        let minutes = self.part_minutes().unsigned_abs();
347        let seconds = self.part_seconds().unsigned_abs();
348        if hours == 0 && minutes == 0 && seconds == 0 {
349            f.write_str("+00")
350        } else if hours != 0 && minutes == 0 && seconds == 0 {
351            write!(f, "{sign}{hours:02}")
352        } else if minutes != 0 && seconds == 0 {
353            write!(f, "{sign}{hours:02}:{minutes:02}")
354        } else {
355            write!(f, "{sign}{hours:02}:{minutes:02}:{seconds:02}")
356        }
357    }
358}
359
360#[cfg(feature = "defmt")]
361impl defmt::Format for Offset {
362    fn format(&self, f: defmt::Formatter) {
363        let sign = if self.is_negative() { "-" } else { "" };
364        defmt::write!(
365            f,
366            "{=str}{=u8:02}:{=u8:02}:{=u8:02}",
367            sign,
368            self.part_hours().unsigned_abs(),
369            self.part_minutes().unsigned_abs(),
370            self.part_seconds().unsigned_abs(),
371        )
372    }
373}
374
375#[cfg(feature = "arbitrary")]
376impl<'a> arbitrary::Arbitrary<'a> for Offset {
377    fn arbitrary(
378        u: &mut arbitrary::Unstructured<'a>,
379    ) -> arbitrary::Result<Offset> {
380        let secs = u.int_in_range(
381            b::OffsetTotalSeconds::MIN..=b::OffsetTotalSeconds::MAX,
382        )?;
383        Ok(Offset::from_seconds(secs).unwrap_or(Offset::UTC))
384    }
385
386    fn size_hint(depth: usize) -> (usize, Option<usize>) {
387        <i32 as arbitrary::Arbitrary>::size_hint(depth)
388    }
389}
390
391#[cfg(test)]
392impl quickcheck::Arbitrary for Offset {
393    fn arbitrary(g: &mut quickcheck::Gen) -> Offset {
394        let secs = b::OffsetTotalSeconds::arbitrary(g);
395        Offset::from_seconds(secs).unwrap_or(Offset::UTC)
396    }
397
398    fn shrink(&self) -> alloc::boxed::Box<dyn Iterator<Item = Self>> {
399        let secs = self.seconds();
400        alloc::boxed::Box::new(secs.shrink().filter_map(|secs| {
401            let secs = b::OffsetTotalSeconds::check(secs).ok()?;
402            Offset::from_seconds(secs).ok()
403        }))
404    }
405}
406
407/// A possibly ambiguous [`Offset`].
408///
409/// One of three possibilities encoded by this type occurs when converting a
410/// civil datetime into a specific instant in time. In rare cases, the civil
411/// datetime can fall into a gap or a fold, in which case, one of two offsets
412/// could be applicable. Or, perhaps, neither. Callers must decide how best to
413/// handle these cases.
414#[derive(Clone, Copy, Debug, Eq, PartialEq)]
415#[cfg_attr(feature = "defmt", derive(defmt::Format))]
416pub enum AmbiguousOffset {
417    /// The offset for a particular civil datetime and time zone is
418    /// unambiguous.
419    ///
420    /// This is the overwhelmingly common case. In general, the only time this
421    /// case does not occur is when there is a transition to a different time
422    /// zone (rare) or to/from daylight saving time (occurs for 1 hour twice
423    /// in year in many geographic locations).
424    Unambiguous {
425        /// The offset from UTC for the corresponding civil datetime given. The
426        /// offset is determined via the relevant time zone data, and in this
427        /// case, there is only one possible offset that could be applied to
428        /// the given civil datetime.
429        offset: Offset,
430    },
431    /// The offset for a particular civil datetime and time zone is ambiguous
432    /// because there is a gap.
433    ///
434    /// This most commonly occurs when a civil datetime corresponds to an hour
435    /// that was "skipped" in a jump to DST (daylight saving time).
436    Gap {
437        /// The offset corresponding to the time before a gap.
438        ///
439        /// For example, given a time zone of `America/Los_Angeles`, the offset
440        /// for time immediately preceding `2020-03-08T02:00:00` is `-08`.
441        before: Offset,
442        /// The offset corresponding to the later time in a gap.
443        ///
444        /// For example, given a time zone of `America/Los_Angeles`, the offset
445        /// for time immediately following `2020-03-08T02:59:59` is `-07`.
446        after: Offset,
447    },
448    /// The offset for a particular civil datetime and time zone is ambiguous
449    /// because there is a fold.
450    ///
451    /// This most commonly occurs when a civil datetime corresponds to an hour
452    /// that was "repeated" in a jump to standard time from DST (daylight
453    /// saving time).
454    Fold {
455        /// The offset corresponding to the earlier time in a fold.
456        ///
457        /// For example, given a time zone of `America/Los_Angeles`, the offset
458        /// for time on the first `2020-11-01T01:00:00` is `-07`.
459        before: Offset,
460        /// The offset corresponding to the earlier time in a fold.
461        ///
462        /// For example, given a time zone of `America/Los_Angeles`, the offset
463        /// for time on the second `2020-11-01T01:00:00` is `-08`.
464        after: Offset,
465    },
466}
467
468impl AmbiguousOffset {
469    #[inline]
470    pub(crate) const fn into_ambiguous_timestamp(
471        self,
472        dt: DateTime,
473    ) -> AmbiguousTimestamp {
474        AmbiguousTimestamp { dt, offset: self }
475    }
476}
477
478/// A possibly ambiguous [`Timestamp`].
479///
480/// While this is called an ambiguous _timestamp_, the thing that is
481/// actually ambiguous is the offset. That is, an ambiguous timestamp is
482/// actually a pair of a [`civil::DateTime`](crate::civil::DateTime) and an
483/// [`AmbiguousOffset`].
484#[derive(Clone, Copy, Debug, Eq, PartialEq)]
485#[cfg_attr(feature = "defmt", derive(defmt::Format))]
486pub struct AmbiguousTimestamp {
487    dt: DateTime,
488    offset: AmbiguousOffset,
489}
490
491impl AmbiguousTimestamp {
492    /// Returns the civil datetime that was used to create this ambiguous
493    /// timestamp.
494    ///
495    /// # Example
496    ///
497    /// ```
498    /// use jiff_core::{civil::date, tz::posix};
499    ///
500    /// let tz = posix::TimeZone::parse("EST5EDT,M3.2.0,M11.1.0").unwrap();
501    /// let dt = date(2024, 7, 10).at(17, 15, 0, 0);
502    /// let ts = tz.to_ambiguous_timestamp(dt);
503    /// assert_eq!(ts.datetime(), dt);
504    ///
505    /// # Ok::<(), Box<dyn std::error::Error>>(())
506    /// ```
507    #[inline]
508    pub const fn datetime(&self) -> DateTime {
509        self.dt
510    }
511
512    /// Returns the possibly ambiguous offset that is the ultimate source of
513    /// ambiguity.
514    ///
515    /// Most civil datetimes are not ambiguous, and thus, the offset will not
516    /// be ambiguous either. In this case, the offset returned will be the
517    /// [`AmbiguousOffset::Unambiguous`] variant.
518    ///
519    /// But, not all civil datetimes are unambiguous. There are exactly two
520    /// cases where a civil datetime can be ambiguous: when a civil datetime
521    /// does not exist (a gap) or when a civil datetime is repeated (a fold).
522    /// In both such cases, the _offset_ is the thing that is ambiguous as
523    /// there are two possible choices for the offset in both cases: the offset
524    /// before the transition (whether it's a gap or a fold) or the offset
525    /// after the transition.
526    ///
527    /// This type captures the fact that computing an offset from a civil
528    /// datetime in a particular time zone is in one of three possible states:
529    ///
530    /// 1. It is unambiguous.
531    /// 2. It is ambiguous because there is a gap in time.
532    /// 3. It is ambiguous because there is a fold in time.
533    ///
534    /// # Example
535    ///
536    /// ```
537    /// use jiff_core::{civil::date, tz::{self, posix, AmbiguousOffset}};
538    ///
539    /// let tz = posix::TimeZone::parse("EST5EDT,M3.2.0,M11.1.0").unwrap();
540    ///
541    /// // Not ambiguous.
542    /// let dt = date(2024, 7, 15).at(17, 30, 0, 0);
543    /// let ts = tz.to_ambiguous_timestamp(dt);
544    /// assert_eq!(ts.offset(), AmbiguousOffset::Unambiguous {
545    ///     offset: tz::offset(-4),
546    /// });
547    ///
548    /// // Ambiguous because of a gap.
549    /// let dt = date(2024, 3, 10).at(2, 30, 0, 0);
550    /// let ts = tz.to_ambiguous_timestamp(dt);
551    /// assert_eq!(ts.offset(), AmbiguousOffset::Gap {
552    ///     before: tz::offset(-5),
553    ///     after: tz::offset(-4),
554    /// });
555    ///
556    /// // Ambiguous because of a fold.
557    /// let dt = date(2024, 11, 3).at(1, 30, 0, 0);
558    /// let ts = tz.to_ambiguous_timestamp(dt);
559    /// assert_eq!(ts.offset(), AmbiguousOffset::Fold {
560    ///     before: tz::offset(-4),
561    ///     after: tz::offset(-5),
562    /// });
563    ///
564    /// # Ok::<(), Box<dyn std::error::Error>>(())
565    /// ```
566    #[inline]
567    pub const fn offset(&self) -> AmbiguousOffset {
568        self.offset
569    }
570
571    /// Returns true if and only if this possibly ambiguous timestamp is
572    /// actually ambiguous.
573    ///
574    /// This occurs precisely in cases when the offset is _not_
575    /// [`AmbiguousOffset::Unambiguous`].
576    ///
577    /// # Example
578    ///
579    /// ```
580    /// use jiff_core::{civil::date, tz::posix};
581    ///
582    /// let tz = posix::TimeZone::parse("EST5EDT,M3.2.0,M11.1.0").unwrap();
583    ///
584    /// // Not ambiguous.
585    /// let dt = date(2024, 7, 15).at(17, 30, 0, 0);
586    /// let ts = tz.to_ambiguous_timestamp(dt);
587    /// assert!(!ts.is_ambiguous());
588    ///
589    /// // Ambiguous because of a gap.
590    /// let dt = date(2024, 3, 10).at(2, 30, 0, 0);
591    /// let ts = tz.to_ambiguous_timestamp(dt);
592    /// assert!(ts.is_ambiguous());
593    ///
594    /// // Ambiguous because of a fold.
595    /// let dt = date(2024, 11, 3).at(1, 30, 0, 0);
596    /// let ts = tz.to_ambiguous_timestamp(dt);
597    /// assert!(ts.is_ambiguous());
598    ///
599    /// # Ok::<(), Box<dyn std::error::Error>>(())
600    /// ```
601    #[inline]
602    pub const fn is_ambiguous(&self) -> bool {
603        !matches!(self.offset(), AmbiguousOffset::Unambiguous { .. })
604    }
605
606    /// Disambiguates this timestamp according to the "compatible" strategy.
607    ///
608    /// If this timestamp is unambiguous, then this is a no-op.
609    ///
610    /// The "compatible" strategy selects the offset corresponding to the civil
611    /// time after a gap, and the offset corresponding to the civil time before
612    /// a fold. This is what is specified in [RFC 5545].
613    ///
614    /// [RFC 5545]: https://datatracker.ietf.org/doc/html/rfc5545
615    ///
616    /// # Errors
617    ///
618    /// This returns an error when the combination of the civil datetime
619    /// and offset would lead to a `Timestamp` outside of the
620    /// [`Timestamp::MIN`] and [`Timestamp::MAX`] limits. This only occurs
621    /// when the civil datetime is "close" to its own [`DateTime::MIN`]
622    /// and [`DateTime::MAX`] limits.
623    #[inline]
624    pub const fn compatible(self) -> Result<Timestamp, RangeError> {
625        let offset = match self.offset() {
626            AmbiguousOffset::Unambiguous { offset } => offset,
627            AmbiguousOffset::Gap { before, .. } => before,
628            AmbiguousOffset::Fold { before, .. } => before,
629        };
630        offset.to_timestamp(self.dt)
631    }
632
633    /// Disambiguates this timestamp according to the "earlier" strategy.
634    ///
635    /// If this timestamp is unambiguous, then this is a no-op.
636    ///
637    /// The "earlier" strategy selects the offset corresponding to the civil
638    /// time before a gap, and the offset corresponding to the civil time
639    /// before a fold.
640    ///
641    /// # Errors
642    ///
643    /// This returns an error when the combination of the civil datetime
644    /// and offset would lead to a `Timestamp` outside of the
645    /// [`Timestamp::MIN`] and [`Timestamp::MAX`] limits. This only occurs
646    /// when the civil datetime is "close" to its own [`DateTime::MIN`]
647    /// and [`DateTime::MAX`] limits.
648    #[inline]
649    pub const fn earlier(self) -> Result<Timestamp, RangeError> {
650        let offset = match self.offset() {
651            AmbiguousOffset::Unambiguous { offset } => offset,
652            AmbiguousOffset::Gap { after, .. } => after,
653            AmbiguousOffset::Fold { before, .. } => before,
654        };
655        offset.to_timestamp(self.dt)
656    }
657
658    /// Disambiguates this timestamp according to the "later" strategy.
659    ///
660    /// If this timestamp is unambiguous, then this is a no-op.
661    ///
662    /// The "later" strategy selects the offset corresponding to the civil
663    /// time after a gap, and the offset corresponding to the civil time
664    /// after a fold.
665    ///
666    /// # Errors
667    ///
668    /// This returns an error when the combination of the civil datetime
669    /// and offset would lead to a `Timestamp` outside of the
670    /// [`Timestamp::MIN`] and [`Timestamp::MAX`] limits. This only occurs
671    /// when the civil datetime is "close" to its own [`DateTime::MIN`]
672    /// and [`DateTime::MAX`] limits.
673    #[inline]
674    pub const fn later(self) -> Result<Timestamp, RangeError> {
675        let offset = match self.offset() {
676            AmbiguousOffset::Unambiguous { offset } => offset,
677            AmbiguousOffset::Gap { before, .. } => before,
678            AmbiguousOffset::Fold { after, .. } => after,
679        };
680        offset.to_timestamp(self.dt)
681    }
682
683    /// Disambiguates this timestamp according to the "reject" strategy.
684    ///
685    /// If this timestamp is unambiguous, then this is a no-op.
686    ///
687    /// The "reject" strategy always returns an error when the timestamp
688    /// is ambiguous.
689    ///
690    /// # Errors
691    ///
692    /// This returns an error when the combination of the civil datetime
693    /// and offset would lead to a `Timestamp` outside of the
694    /// [`Timestamp::MIN`] and [`Timestamp::MAX`] limits. This only occurs
695    /// when the civil datetime is "close" to its own [`DateTime::MIN`]
696    /// and [`DateTime::MAX`] limits.
697    ///
698    /// This also returns an error when the timestamp is ambiguous.
699    ///
700    /// # Example
701    ///
702    /// ```
703    /// use jiff_core::{civil::date, tz::{posix, Offset}};
704    ///
705    /// let tz = posix::TimeZone::parse("EST5EDT,M3.2.0,M11.1.0").unwrap();
706    ///
707    /// // Not ambiguous.
708    /// let dt = date(2024, 7, 15).at(17, 30, 0, 0);
709    /// let ts = tz.to_ambiguous_timestamp(dt);
710    /// assert_eq!(
711    ///     ts.later().unwrap().to_datetime(Offset::UTC),
712    ///     date(2024, 7, 15).at(21, 30, 0, 0),
713    /// );
714    ///
715    /// // Ambiguous because of a gap.
716    /// let dt = date(2024, 3, 10).at(2, 30, 0, 0);
717    /// let ts = tz.to_ambiguous_timestamp(dt);
718    /// assert!(ts.unambiguous().is_err());
719    ///
720    /// // Ambiguous because of a fold.
721    /// let dt = date(2024, 11, 3).at(1, 30, 0, 0);
722    /// let ts = tz.to_ambiguous_timestamp(dt);
723    /// assert!(ts.unambiguous().is_err());
724    /// ```
725    #[inline]
726    pub const fn unambiguous(self) -> Result<Timestamp, AmbiguousError> {
727        let offset = match self.offset() {
728            AmbiguousOffset::Unambiguous { offset } => offset,
729            AmbiguousOffset::Gap { before, after } => {
730                return Err(AmbiguousError {
731                    kind: AmbiguousErrorKind::BecauseGap { before, after },
732                });
733            }
734            AmbiguousOffset::Fold { before, after } => {
735                return Err(AmbiguousError {
736                    kind: AmbiguousErrorKind::BecauseFold { before, after },
737                });
738            }
739        };
740        match offset.to_timestamp(self.dt) {
741            Ok(timestamp) => Ok(timestamp),
742            Err(range_error) => Err(AmbiguousError {
743                kind: AmbiguousErrorKind::Range(range_error),
744            }),
745        }
746    }
747}
748
749/// An error that occurs when an unmabiguous civil datetime is demanded.
750///
751/// This surfaces via the [`AmbiguousTimestamp::unambiguous`] API.
752#[derive(Clone, Debug)]
753#[cfg_attr(feature = "defmt", derive(defmt::Format))]
754pub struct AmbiguousError {
755    kind: AmbiguousErrorKind,
756}
757
758#[derive(Clone, Debug)]
759#[cfg_attr(feature = "defmt", derive(defmt::Format))]
760enum AmbiguousErrorKind {
761    Range(RangeError),
762    BecauseFold { before: Offset, after: Offset },
763    BecauseGap { before: Offset, after: Offset },
764}
765
766impl core::fmt::Display for AmbiguousError {
767    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
768        use self::AmbiguousErrorKind::*;
769
770        match self.kind {
771            Range(ref err) => core::fmt::Display::fmt(err, f),
772            BecauseFold { before, after } => write!(
773                f,
774                "datetime is ambiguous since it falls into a \
775                 fold between offsets {before} and {after}",
776            ),
777            BecauseGap { before, after } => write!(
778                f,
779                "datetime is ambiguous since it falls into a \
780                 gap between offsets {before} and {after}",
781            ),
782        }
783    }
784}
785
786#[cfg(feature = "std")]
787impl std::error::Error for AmbiguousError {}
788
789#[cfg(test)]
790mod tests {
791    use super::Offset;
792
793    #[test]
794    fn checked_subtracts_positive_and_negative_seconds() {
795        assert_eq!(
796            Offset::UTC.checked_sub(1).unwrap(),
797            Offset::constant_seconds(-1),
798        );
799        assert_eq!(
800            Offset::UTC.checked_sub(-1).unwrap(),
801            Offset::constant_seconds(1),
802        );
803    }
804
805    #[test]
806    fn checked_sub_rejects_overflow() {
807        assert!(Offset::MIN.checked_sub(1).is_err());
808        assert!(Offset::MAX.checked_sub(-1).is_err());
809        assert!(Offset::UTC.checked_sub(i32::MIN).is_err());
810    }
811
812    #[test]
813    fn subtraction_operators_subtract_seconds() {
814        assert_eq!(Offset::UTC - 1, Offset::constant_seconds(-1));
815
816        let mut offset = Offset::UTC;
817        offset -= 1;
818        assert_eq!(offset, Offset::constant_seconds(-1));
819    }
820}