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 {}