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}