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