jiff/tz/ambiguous.rs
1use jcore::tz::AmbiguousOffset as JAmbiguousOffset;
2
3use crate::{
4 civil::DateTime,
5 error::{tz::ambiguous::Error as E, Error, ErrorContext},
6 tz::{Offset, TimeZone},
7 Timestamp, Zoned,
8};
9
10/// Configuration for resolving ambiguous datetimes in a particular time zone.
11///
12/// This is useful for specifying how to disambiguate ambiguous datetimes at
13/// runtime. For example, as configuration for parsing [`Zoned`] values via
14/// [`fmt::temporal::DateTimeParser::disambiguation`](crate::fmt::temporal::DateTimeParser::disambiguation).
15///
16/// Note that there is no difference in using
17/// `Disambiguation::Compatible.disambiguate(ambiguous_timestamp)` and
18/// `ambiguous_timestamp.compatible()`. They are equivalent. The purpose of
19/// this enum is to expose the disambiguation strategy as a runtime value for
20/// configuration purposes.
21///
22/// The default value is `Disambiguation::Compatible`, which matches the
23/// behavior specified in [RFC 5545 (iCalendar)]. Namely, when an ambiguous
24/// datetime is found in a fold (the clocks are rolled back), then the earlier
25/// time is selected. And when an ambiguous datetime is found in a gap (the
26/// clocks are skipped forward), then the later time is selected.
27///
28/// This enum is non-exhaustive so that other forms of disambiguation may be
29/// added in semver compatible releases.
30///
31/// [RFC 5545 (iCalendar)]: https://datatracker.ietf.org/doc/html/rfc5545
32///
33/// # Example
34///
35/// This example shows the default disambiguation mode ("compatible") when
36/// given a datetime that falls in a "gap" (i.e., a forwards DST transition).
37///
38/// ```
39/// use jiff::{civil::date, tz};
40///
41/// let newyork = tz::db().get("America/New_York")?;
42/// let ambiguous = newyork.to_ambiguous_zoned(date(2024, 3, 10).at(2, 30, 0, 0));
43///
44/// // NOTE: This is identical to `ambiguous.compatible()`.
45/// let zdt = ambiguous.disambiguate(tz::Disambiguation::Compatible)?;
46/// assert_eq!(zdt.datetime(), date(2024, 3, 10).at(3, 30, 0, 0));
47/// // In compatible mode, forward transitions select the later
48/// // time. In the EST->EDT transition, that's the -04 (EDT) offset.
49/// assert_eq!(zdt.offset(), tz::offset(-4));
50///
51/// # Ok::<(), Box<dyn std::error::Error>>(())
52/// ```
53///
54/// # Example: parsing
55///
56/// This example shows how to set the disambiguation configuration while
57/// parsing a [`Zoned`] datetime. In this example, we always prefer the earlier
58/// time.
59///
60/// ```
61/// use jiff::{civil::date, fmt::temporal::DateTimeParser, tz};
62///
63/// static PARSER: DateTimeParser = DateTimeParser::new()
64/// .disambiguation(tz::Disambiguation::Earlier);
65///
66/// let zdt = PARSER.parse_zoned("2024-03-10T02:30[America/New_York]")?;
67/// // In earlier mode, forward transitions select the earlier time, unlike
68/// // in compatible mode. In this case, that's the pre-DST offset of -05.
69/// assert_eq!(zdt.datetime(), date(2024, 3, 10).at(1, 30, 0, 0));
70/// assert_eq!(zdt.offset(), tz::offset(-5));
71///
72/// # Ok::<(), Box<dyn std::error::Error>>(())
73/// ```
74#[derive(Clone, Copy, Debug, Default)]
75#[cfg_attr(feature = "defmt", derive(defmt::Format))]
76#[non_exhaustive]
77pub enum Disambiguation {
78 /// In a backward transition, the earlier time is selected. In forward
79 /// transition, the later time is selected.
80 ///
81 /// This is equivalent to [`AmbiguousTimestamp::compatible`] and
82 /// [`AmbiguousZoned::compatible`].
83 #[default]
84 Compatible,
85 /// The earlier time is always selected.
86 ///
87 /// This is equivalent to [`AmbiguousTimestamp::earlier`] and
88 /// [`AmbiguousZoned::earlier`].
89 Earlier,
90 /// The later time is always selected.
91 ///
92 /// This is equivalent to [`AmbiguousTimestamp::later`] and
93 /// [`AmbiguousZoned::later`].
94 Later,
95 /// When an ambiguous datetime is encountered, this strategy will always
96 /// result in an error. This is useful if you need to require datetimes
97 /// from users to unambiguously refer to a specific instant.
98 ///
99 /// This is equivalent to [`AmbiguousTimestamp::unambiguous`] and
100 /// [`AmbiguousZoned::unambiguous`].
101 Reject,
102}
103
104/// A possibly ambiguous [`Offset`].
105///
106/// An `AmbiguousOffset` is part of both [`AmbiguousTimestamp`] and
107/// [`AmbiguousZoned`], which are created by
108/// [`TimeZone::to_ambiguous_timestamp`] and
109/// [`TimeZone::to_ambiguous_zoned`], respectively.
110///
111/// When converting a civil datetime in a particular time zone to a precise
112/// instant in time (that is, either `Timestamp` or `Zoned`), then the primary
113/// thing needed to form a precise instant in time is an [`Offset`]. The
114/// problem is that some civil datetimes are ambiguous. That is, some do not
115/// exist (because they fall into a gap, where some civil time is skipped),
116/// or some are repeated (because they fall into a fold, where some civil time
117/// is repeated).
118///
119/// The purpose of this type is to represent that ambiguity when it occurs.
120/// The ambiguity is manifest through the offset choice: it is either the
121/// offset _before_ the transition or the offset _after_ the transition. This
122/// is true regardless of whether the ambiguity occurs as a result of a gap
123/// or a fold.
124///
125/// It is generally considered very rare to need to inspect values of this
126/// type directly. Instead, higher level routines like
127/// [`AmbiguousZoned::compatible`] or [`AmbiguousZoned::unambiguous`] will
128/// implement a strategy for you.
129///
130/// # Example
131///
132/// This example shows how the "compatible" disambiguation strategy is
133/// implemented. Recall that the "compatible" strategy chooses the offset
134/// corresponding to the civil datetime after a gap, and the offset
135/// corresponding to the civil datetime before a gap.
136///
137/// ```
138/// use jiff::{civil::date, tz::{self, AmbiguousOffset}};
139///
140/// let tz = tz::db().get("America/New_York")?;
141/// let dt = date(2024, 3, 10).at(2, 30, 0, 0);
142/// let offset = match tz.to_ambiguous_timestamp(dt).offset() {
143/// AmbiguousOffset::Unambiguous { offset } => offset,
144/// // This is counter-intuitive, but in order to get the civil datetime
145/// // *after* the gap, we need to select the offset from *before* the
146/// // gap.
147/// AmbiguousOffset::Gap { before, .. } => before,
148/// AmbiguousOffset::Fold { before, .. } => before,
149/// };
150/// assert_eq!(offset.to_timestamp(dt)?.to_string(), "2024-03-10T07:30:00Z");
151///
152/// # Ok::<(), Box<dyn std::error::Error>>(())
153/// ```
154#[derive(Clone, Copy, Debug, Eq, PartialEq)]
155#[cfg_attr(feature = "defmt", derive(defmt::Format))]
156pub enum AmbiguousOffset {
157 /// The offset for a particular civil datetime and time zone is
158 /// unambiguous.
159 ///
160 /// This is the overwhelmingly common case. In general, the only time this
161 /// case does not occur is when there is a transition to a different time
162 /// zone (rare) or to/from daylight saving time (occurs for 1 hour twice
163 /// in year in many geographic locations).
164 Unambiguous {
165 /// The offset from UTC for the corresponding civil datetime given. The
166 /// offset is determined via the relevant time zone data, and in this
167 /// case, there is only one possible offset that could be applied to
168 /// the given civil datetime.
169 offset: Offset,
170 },
171 /// The offset for a particular civil datetime and time zone is ambiguous
172 /// because there is a gap.
173 ///
174 /// This most commonly occurs when a civil datetime corresponds to an hour
175 /// that was "skipped" in a jump to DST (daylight saving time).
176 Gap {
177 /// The offset corresponding to the time before a gap.
178 ///
179 /// For example, given a time zone of `America/Los_Angeles`, the offset
180 /// for time immediately preceding `2020-03-08T02:00:00` is `-08`.
181 before: Offset,
182 /// The offset corresponding to the later time in a gap.
183 ///
184 /// For example, given a time zone of `America/Los_Angeles`, the offset
185 /// for time immediately following `2020-03-08T02:59:59` is `-07`.
186 after: Offset,
187 },
188 /// The offset for a particular civil datetime and time zone is ambiguous
189 /// because there is a fold.
190 ///
191 /// This most commonly occurs when a civil datetime corresponds to an hour
192 /// that was "repeated" in a jump to standard time from DST (daylight
193 /// saving time).
194 Fold {
195 /// The offset corresponding to the earlier time in a fold.
196 ///
197 /// For example, given a time zone of `America/Los_Angeles`, the offset
198 /// for time on the first `2020-11-01T01:00:00` is `-07`.
199 before: Offset,
200 /// The offset corresponding to the earlier time in a fold.
201 ///
202 /// For example, given a time zone of `America/Los_Angeles`, the offset
203 /// for time on the second `2020-11-01T01:00:00` is `-08`.
204 after: Offset,
205 },
206}
207
208impl AmbiguousOffset {
209 #[inline]
210 pub(crate) const fn from_jcore(
211 offset: JAmbiguousOffset,
212 ) -> AmbiguousOffset {
213 match offset {
214 JAmbiguousOffset::Unambiguous { offset } => {
215 let offset = Offset::from_jcore(offset);
216 AmbiguousOffset::Unambiguous { offset }
217 }
218 JAmbiguousOffset::Gap { before, after } => {
219 let before = Offset::from_jcore(before);
220 let after = Offset::from_jcore(after);
221 AmbiguousOffset::Gap { before, after }
222 }
223 JAmbiguousOffset::Fold { before, after } => {
224 let before = Offset::from_jcore(before);
225 let after = Offset::from_jcore(after);
226 AmbiguousOffset::Fold { before, after }
227 }
228 }
229 }
230}
231
232/// A possibly ambiguous [`Timestamp`], created by
233/// [`TimeZone::to_ambiguous_timestamp`].
234///
235/// While this is called an ambiguous _timestamp_, the thing that is
236/// actually ambiguous is the offset. That is, an ambiguous timestamp is
237/// actually a pair of a [`civil::DateTime`](crate::civil::DateTime) and an
238/// [`AmbiguousOffset`].
239///
240/// When the offset is ambiguous, it either represents a gap (civil time is
241/// skipped) or a fold (civil time is repeated). In both cases, there are, by
242/// construction, two different offsets to choose from: the offset from before
243/// the transition and the offset from after the transition.
244///
245/// The purpose of this type is to represent that ambiguity (when it occurs)
246/// and enable callers to make a choice about how to resolve that ambiguity.
247/// In some cases, you might want to reject ambiguity altogether, which is
248/// supported by the [`AmbiguousTimestamp::unambiguous`] routine.
249///
250/// This type provides four different out-of-the-box disambiguation strategies:
251///
252/// * [`AmbiguousTimestamp::compatible`] implements the
253/// [`Disambiguation::Compatible`] strategy. In the case of a gap, the offset
254/// after the gap is selected. In the case of a fold, the offset before the
255/// fold occurs is selected.
256/// * [`AmbiguousTimestamp::earlier`] implements the
257/// [`Disambiguation::Earlier`] strategy. This always selects the "earlier"
258/// offset.
259/// * [`AmbiguousTimestamp::later`] implements the
260/// [`Disambiguation::Later`] strategy. This always selects the "later"
261/// offset.
262/// * [`AmbiguousTimestamp::unambiguous`] implements the
263/// [`Disambiguation::Reject`] strategy. It acts as an assertion that the
264/// offset is unambiguous. If it is ambiguous, then an appropriate error is
265/// returned.
266///
267/// The [`AmbiguousTimestamp::disambiguate`] method can be used with the
268/// [`Disambiguation`] enum when the disambiguation strategy isn't known until
269/// runtime.
270///
271/// Note also that these aren't the only disambiguation strategies. The
272/// [`AmbiguousOffset`] type, accessible via [`AmbiguousTimestamp::offset`],
273/// exposes the full details of the ambiguity. So any strategy can be
274/// implemented.
275///
276/// # Example
277///
278/// This example shows how the "compatible" disambiguation strategy is
279/// implemented. Recall that the "compatible" strategy chooses the offset
280/// corresponding to the civil datetime after a gap, and the offset
281/// corresponding to the civil datetime before a gap.
282///
283/// ```
284/// use jiff::{civil::date, tz::{self, AmbiguousOffset}};
285///
286/// let tz = tz::db().get("America/New_York")?;
287/// let dt = date(2024, 3, 10).at(2, 30, 0, 0);
288/// let offset = match tz.to_ambiguous_timestamp(dt).offset() {
289/// AmbiguousOffset::Unambiguous { offset } => offset,
290/// // This is counter-intuitive, but in order to get the civil datetime
291/// // *after* the gap, we need to select the offset from *before* the
292/// // gap.
293/// AmbiguousOffset::Gap { before, .. } => before,
294/// AmbiguousOffset::Fold { before, .. } => before,
295/// };
296/// assert_eq!(offset.to_timestamp(dt)?.to_string(), "2024-03-10T07:30:00Z");
297///
298/// # Ok::<(), Box<dyn std::error::Error>>(())
299/// ```
300#[derive(Clone, Copy, Debug, Eq, PartialEq)]
301#[cfg_attr(feature = "defmt", derive(defmt::Format))]
302pub struct AmbiguousTimestamp {
303 dt: DateTime,
304 offset: AmbiguousOffset,
305}
306
307impl AmbiguousTimestamp {
308 #[inline]
309 pub(crate) fn new(
310 dt: DateTime,
311 kind: AmbiguousOffset,
312 ) -> AmbiguousTimestamp {
313 AmbiguousTimestamp { dt, offset: kind }
314 }
315
316 /// Returns the civil datetime that was used to create this ambiguous
317 /// timestamp.
318 ///
319 /// # Example
320 ///
321 /// ```
322 /// use jiff::{civil::date, tz};
323 ///
324 /// let tz = tz::db().get("America/New_York")?;
325 /// let dt = date(2024, 7, 10).at(17, 15, 0, 0);
326 /// let ts = tz.to_ambiguous_timestamp(dt);
327 /// assert_eq!(ts.datetime(), dt);
328 ///
329 /// # Ok::<(), Box<dyn std::error::Error>>(())
330 /// ```
331 #[inline]
332 pub fn datetime(&self) -> DateTime {
333 self.dt
334 }
335
336 /// Returns the possibly ambiguous offset that is the ultimate source of
337 /// ambiguity.
338 ///
339 /// Most civil datetimes are not ambiguous, and thus, the offset will not
340 /// be ambiguous either. In this case, the offset returned will be the
341 /// [`AmbiguousOffset::Unambiguous`] variant.
342 ///
343 /// But, not all civil datetimes are unambiguous. There are exactly two
344 /// cases where a civil datetime can be ambiguous: when a civil datetime
345 /// does not exist (a gap) or when a civil datetime is repeated (a fold).
346 /// In both such cases, the _offset_ is the thing that is ambiguous as
347 /// there are two possible choices for the offset in both cases: the offset
348 /// before the transition (whether it's a gap or a fold) or the offset
349 /// after the transition.
350 ///
351 /// This type captures the fact that computing an offset from a civil
352 /// datetime in a particular time zone is in one of three possible states:
353 ///
354 /// 1. It is unambiguous.
355 /// 2. It is ambiguous because there is a gap in time.
356 /// 3. It is ambiguous because there is a fold in time.
357 ///
358 /// # Example
359 ///
360 /// ```
361 /// use jiff::{civil::date, tz::{self, AmbiguousOffset}};
362 ///
363 /// let tz = tz::db().get("America/New_York")?;
364 ///
365 /// // Not ambiguous.
366 /// let dt = date(2024, 7, 15).at(17, 30, 0, 0);
367 /// let ts = tz.to_ambiguous_timestamp(dt);
368 /// assert_eq!(ts.offset(), AmbiguousOffset::Unambiguous {
369 /// offset: tz::offset(-4),
370 /// });
371 ///
372 /// // Ambiguous because of a gap.
373 /// let dt = date(2024, 3, 10).at(2, 30, 0, 0);
374 /// let ts = tz.to_ambiguous_timestamp(dt);
375 /// assert_eq!(ts.offset(), AmbiguousOffset::Gap {
376 /// before: tz::offset(-5),
377 /// after: tz::offset(-4),
378 /// });
379 ///
380 /// // Ambiguous because of a fold.
381 /// let dt = date(2024, 11, 3).at(1, 30, 0, 0);
382 /// let ts = tz.to_ambiguous_timestamp(dt);
383 /// assert_eq!(ts.offset(), AmbiguousOffset::Fold {
384 /// before: tz::offset(-4),
385 /// after: tz::offset(-5),
386 /// });
387 ///
388 /// # Ok::<(), Box<dyn std::error::Error>>(())
389 /// ```
390 #[inline]
391 pub fn offset(&self) -> AmbiguousOffset {
392 self.offset
393 }
394
395 /// Returns true if and only if this possibly ambiguous timestamp is
396 /// actually ambiguous.
397 ///
398 /// This occurs precisely in cases when the offset is _not_
399 /// [`AmbiguousOffset::Unambiguous`].
400 ///
401 /// # Example
402 ///
403 /// ```
404 /// use jiff::{civil::date, tz};
405 ///
406 /// let tz = tz::db().get("America/New_York")?;
407 ///
408 /// // Not ambiguous.
409 /// let dt = date(2024, 7, 15).at(17, 30, 0, 0);
410 /// let ts = tz.to_ambiguous_timestamp(dt);
411 /// assert!(!ts.is_ambiguous());
412 ///
413 /// // Ambiguous because of a gap.
414 /// let dt = date(2024, 3, 10).at(2, 30, 0, 0);
415 /// let ts = tz.to_ambiguous_timestamp(dt);
416 /// assert!(ts.is_ambiguous());
417 ///
418 /// // Ambiguous because of a fold.
419 /// let dt = date(2024, 11, 3).at(1, 30, 0, 0);
420 /// let ts = tz.to_ambiguous_timestamp(dt);
421 /// assert!(ts.is_ambiguous());
422 ///
423 /// # Ok::<(), Box<dyn std::error::Error>>(())
424 /// ```
425 #[inline]
426 pub fn is_ambiguous(&self) -> bool {
427 !matches!(self.offset(), AmbiguousOffset::Unambiguous { .. })
428 }
429
430 /// Disambiguates this timestamp according to the
431 /// [`Disambiguation::Compatible`] strategy.
432 ///
433 /// If this timestamp is unambiguous, then this is a no-op.
434 ///
435 /// The "compatible" strategy selects the offset corresponding to the civil
436 /// time after a gap, and the offset corresponding to the civil time before
437 /// a fold. This is what is specified in [RFC 5545].
438 ///
439 /// [RFC 5545]: https://datatracker.ietf.org/doc/html/rfc5545
440 ///
441 /// # Errors
442 ///
443 /// This returns an error when the combination of the civil datetime
444 /// and offset would lead to a `Timestamp` outside of the
445 /// [`Timestamp::MIN`] and [`Timestamp::MAX`] limits. This only occurs
446 /// when the civil datetime is "close" to its own [`DateTime::MIN`]
447 /// and [`DateTime::MAX`] limits.
448 ///
449 /// # Example
450 ///
451 /// ```
452 /// use jiff::{civil::date, tz};
453 ///
454 /// let tz = tz::db().get("America/New_York")?;
455 ///
456 /// // Not ambiguous.
457 /// let dt = date(2024, 7, 15).at(17, 30, 0, 0);
458 /// let ts = tz.to_ambiguous_timestamp(dt);
459 /// assert_eq!(
460 /// ts.compatible()?.to_string(),
461 /// "2024-07-15T21:30:00Z",
462 /// );
463 ///
464 /// // Ambiguous because of a gap.
465 /// let dt = date(2024, 3, 10).at(2, 30, 0, 0);
466 /// let ts = tz.to_ambiguous_timestamp(dt);
467 /// assert_eq!(
468 /// ts.compatible()?.to_string(),
469 /// "2024-03-10T07:30:00Z",
470 /// );
471 ///
472 /// // Ambiguous because of a fold.
473 /// let dt = date(2024, 11, 3).at(1, 30, 0, 0);
474 /// let ts = tz.to_ambiguous_timestamp(dt);
475 /// assert_eq!(
476 /// ts.compatible()?.to_string(),
477 /// "2024-11-03T05:30:00Z",
478 /// );
479 ///
480 /// # Ok::<(), Box<dyn std::error::Error>>(())
481 /// ```
482 #[inline]
483 pub fn compatible(self) -> Result<Timestamp, Error> {
484 let offset = match self.offset() {
485 AmbiguousOffset::Unambiguous { offset } => offset,
486 AmbiguousOffset::Gap { before, .. } => before,
487 AmbiguousOffset::Fold { before, .. } => before,
488 };
489 offset.to_timestamp(self.dt)
490 }
491
492 /// Disambiguates this timestamp according to the
493 /// [`Disambiguation::Earlier`] strategy.
494 ///
495 /// If this timestamp is unambiguous, then this is a no-op.
496 ///
497 /// The "earlier" strategy selects the offset corresponding to the civil
498 /// time before a gap, and the offset corresponding to the civil time
499 /// before a fold.
500 ///
501 /// # Errors
502 ///
503 /// This returns an error when the combination of the civil datetime
504 /// and offset would lead to a `Timestamp` outside of the
505 /// [`Timestamp::MIN`] and [`Timestamp::MAX`] limits. This only occurs
506 /// when the civil datetime is "close" to its own [`DateTime::MIN`]
507 /// and [`DateTime::MAX`] limits.
508 ///
509 /// # Example
510 ///
511 /// ```
512 /// use jiff::{civil::date, tz};
513 ///
514 /// let tz = tz::db().get("America/New_York")?;
515 ///
516 /// // Not ambiguous.
517 /// let dt = date(2024, 7, 15).at(17, 30, 0, 0);
518 /// let ts = tz.to_ambiguous_timestamp(dt);
519 /// assert_eq!(
520 /// ts.earlier()?.to_string(),
521 /// "2024-07-15T21:30:00Z",
522 /// );
523 ///
524 /// // Ambiguous because of a gap.
525 /// let dt = date(2024, 3, 10).at(2, 30, 0, 0);
526 /// let ts = tz.to_ambiguous_timestamp(dt);
527 /// assert_eq!(
528 /// ts.earlier()?.to_string(),
529 /// "2024-03-10T06:30:00Z",
530 /// );
531 ///
532 /// // Ambiguous because of a fold.
533 /// let dt = date(2024, 11, 3).at(1, 30, 0, 0);
534 /// let ts = tz.to_ambiguous_timestamp(dt);
535 /// assert_eq!(
536 /// ts.earlier()?.to_string(),
537 /// "2024-11-03T05:30:00Z",
538 /// );
539 ///
540 /// # Ok::<(), Box<dyn std::error::Error>>(())
541 /// ```
542 #[inline]
543 pub fn earlier(self) -> Result<Timestamp, Error> {
544 let offset = match self.offset() {
545 AmbiguousOffset::Unambiguous { offset } => offset,
546 AmbiguousOffset::Gap { after, .. } => after,
547 AmbiguousOffset::Fold { before, .. } => before,
548 };
549 offset.to_timestamp(self.dt)
550 }
551
552 /// Disambiguates this timestamp according to the
553 /// [`Disambiguation::Later`] strategy.
554 ///
555 /// If this timestamp is unambiguous, then this is a no-op.
556 ///
557 /// The "later" strategy selects the offset corresponding to the civil
558 /// time after a gap, and the offset corresponding to the civil time
559 /// after a fold.
560 ///
561 /// # Errors
562 ///
563 /// This returns an error when the combination of the civil datetime
564 /// and offset would lead to a `Timestamp` outside of the
565 /// [`Timestamp::MIN`] and [`Timestamp::MAX`] limits. This only occurs
566 /// when the civil datetime is "close" to its own [`DateTime::MIN`]
567 /// and [`DateTime::MAX`] limits.
568 ///
569 /// # Example
570 ///
571 /// ```
572 /// use jiff::{civil::date, tz};
573 ///
574 /// let tz = tz::db().get("America/New_York")?;
575 ///
576 /// // Not ambiguous.
577 /// let dt = date(2024, 7, 15).at(17, 30, 0, 0);
578 /// let ts = tz.to_ambiguous_timestamp(dt);
579 /// assert_eq!(
580 /// ts.later()?.to_string(),
581 /// "2024-07-15T21:30:00Z",
582 /// );
583 ///
584 /// // Ambiguous because of a gap.
585 /// let dt = date(2024, 3, 10).at(2, 30, 0, 0);
586 /// let ts = tz.to_ambiguous_timestamp(dt);
587 /// assert_eq!(
588 /// ts.later()?.to_string(),
589 /// "2024-03-10T07:30:00Z",
590 /// );
591 ///
592 /// // Ambiguous because of a fold.
593 /// let dt = date(2024, 11, 3).at(1, 30, 0, 0);
594 /// let ts = tz.to_ambiguous_timestamp(dt);
595 /// assert_eq!(
596 /// ts.later()?.to_string(),
597 /// "2024-11-03T06:30:00Z",
598 /// );
599 ///
600 /// # Ok::<(), Box<dyn std::error::Error>>(())
601 /// ```
602 #[inline]
603 pub fn later(self) -> Result<Timestamp, Error> {
604 let offset = match self.offset() {
605 AmbiguousOffset::Unambiguous { offset } => offset,
606 AmbiguousOffset::Gap { before, .. } => before,
607 AmbiguousOffset::Fold { after, .. } => after,
608 };
609 offset.to_timestamp(self.dt)
610 }
611
612 /// Disambiguates this timestamp according to the
613 /// [`Disambiguation::Reject`] strategy.
614 ///
615 /// If this timestamp is unambiguous, then this is a no-op.
616 ///
617 /// The "reject" strategy always returns an error when the timestamp
618 /// is ambiguous.
619 ///
620 /// # Errors
621 ///
622 /// This returns an error when the combination of the civil datetime
623 /// and offset would lead to a `Timestamp` outside of the
624 /// [`Timestamp::MIN`] and [`Timestamp::MAX`] limits. This only occurs
625 /// when the civil datetime is "close" to its own [`DateTime::MIN`]
626 /// and [`DateTime::MAX`] limits.
627 ///
628 /// This also returns an error when the timestamp is ambiguous.
629 ///
630 /// # Example
631 ///
632 /// ```
633 /// use jiff::{civil::date, tz};
634 ///
635 /// let tz = tz::db().get("America/New_York")?;
636 ///
637 /// // Not ambiguous.
638 /// let dt = date(2024, 7, 15).at(17, 30, 0, 0);
639 /// let ts = tz.to_ambiguous_timestamp(dt);
640 /// assert_eq!(
641 /// ts.later()?.to_string(),
642 /// "2024-07-15T21:30:00Z",
643 /// );
644 ///
645 /// // Ambiguous because of a gap.
646 /// let dt = date(2024, 3, 10).at(2, 30, 0, 0);
647 /// let ts = tz.to_ambiguous_timestamp(dt);
648 /// assert!(ts.unambiguous().is_err());
649 ///
650 /// // Ambiguous because of a fold.
651 /// let dt = date(2024, 11, 3).at(1, 30, 0, 0);
652 /// let ts = tz.to_ambiguous_timestamp(dt);
653 /// assert!(ts.unambiguous().is_err());
654 ///
655 /// # Ok::<(), Box<dyn std::error::Error>>(())
656 /// ```
657 #[inline]
658 pub fn unambiguous(self) -> Result<Timestamp, Error> {
659 let offset = match self.offset() {
660 AmbiguousOffset::Unambiguous { offset } => offset,
661 AmbiguousOffset::Gap { before, after } => {
662 return Err(Error::from(E::BecauseGap { before, after }));
663 }
664 AmbiguousOffset::Fold { before, after } => {
665 return Err(Error::from(E::BecauseFold { before, after }));
666 }
667 };
668 offset.to_timestamp(self.dt)
669 }
670
671 /// Disambiguates this (possibly ambiguous) timestamp into a specific
672 /// timestamp.
673 ///
674 /// This is the same as calling one of the disambiguation methods, but
675 /// the method chosen is indicated by the option given. This is useful
676 /// when the disambiguation option needs to be chosen at runtime.
677 ///
678 /// # Errors
679 ///
680 /// This returns an error if this would have returned a timestamp
681 /// outside of its minimum and maximum values.
682 ///
683 /// This can also return an error when using the [`Disambiguation::Reject`]
684 /// strategy. Namely, when using the `Reject` strategy, any ambiguous
685 /// timestamp always results in an error.
686 ///
687 /// # Example
688 ///
689 /// This example shows the various disambiguation modes when given a
690 /// datetime that falls in a "fold" (i.e., a backwards DST transition).
691 ///
692 /// ```
693 /// use jiff::{civil::date, tz::{self, Disambiguation}};
694 ///
695 /// let newyork = tz::db().get("America/New_York")?;
696 /// let dt = date(2024, 11, 3).at(1, 30, 0, 0);
697 /// let ambiguous = newyork.to_ambiguous_timestamp(dt);
698 ///
699 /// // In compatible mode, backward transitions select the earlier
700 /// // time. In the EDT->EST transition, that's the -04 (EDT) offset.
701 /// let ts = ambiguous.clone().disambiguate(Disambiguation::Compatible)?;
702 /// assert_eq!(ts.to_string(), "2024-11-03T05:30:00Z");
703 ///
704 /// // The earlier time in the EDT->EST transition is the -04 (EDT) offset.
705 /// let ts = ambiguous.clone().disambiguate(Disambiguation::Earlier)?;
706 /// assert_eq!(ts.to_string(), "2024-11-03T05:30:00Z");
707 ///
708 /// // The later time in the EDT->EST transition is the -05 (EST) offset.
709 /// let ts = ambiguous.clone().disambiguate(Disambiguation::Later)?;
710 /// assert_eq!(ts.to_string(), "2024-11-03T06:30:00Z");
711 ///
712 /// // Since our datetime is ambiguous, the 'reject' strategy errors.
713 /// assert!(ambiguous.disambiguate(Disambiguation::Reject).is_err());
714 ///
715 /// # Ok::<(), Box<dyn std::error::Error>>(())
716 /// ```
717 #[inline]
718 pub fn disambiguate(
719 self,
720 option: Disambiguation,
721 ) -> Result<Timestamp, Error> {
722 match option {
723 Disambiguation::Compatible => self.compatible(),
724 Disambiguation::Earlier => self.earlier(),
725 Disambiguation::Later => self.later(),
726 Disambiguation::Reject => self.unambiguous(),
727 }
728 }
729
730 /// Convert this ambiguous timestamp into an ambiguous zoned date time by
731 /// attaching a time zone.
732 ///
733 /// This is useful when you have a [`civil::DateTime`], [`TimeZone`] and
734 /// want to convert it to an instant while applying a particular
735 /// disambiguation strategy without an extra clone of the `TimeZone`.
736 ///
737 /// This isn't currently exposed because I believe use cases for crate
738 /// users can be satisfied via [`TimeZone::into_ambiguous_zoned`] (which
739 /// is implemented via this routine).
740 #[inline]
741 pub(crate) fn into_ambiguous_zoned(self, tz: TimeZone) -> AmbiguousZoned {
742 AmbiguousZoned::new(self, tz)
743 }
744}
745
746/// A possibly ambiguous [`Zoned`], created by
747/// [`TimeZone::to_ambiguous_zoned`].
748///
749/// While this is called an ambiguous zoned datetime, the thing that is
750/// actually ambiguous is the offset. That is, an ambiguous zoned datetime
751/// is actually a triple of a [`civil::DateTime`](crate::civil::DateTime), a
752/// [`TimeZone`] and an [`AmbiguousOffset`].
753///
754/// When the offset is ambiguous, it either represents a gap (civil time is
755/// skipped) or a fold (civil time is repeated). In both cases, there are, by
756/// construction, two different offsets to choose from: the offset from before
757/// the transition and the offset from after the transition.
758///
759/// The purpose of this type is to represent that ambiguity (when it occurs)
760/// and enable callers to make a choice about how to resolve that ambiguity.
761/// In some cases, you might want to reject ambiguity altogether, which is
762/// supported by the [`AmbiguousZoned::unambiguous`] routine.
763///
764/// This type provides four different out-of-the-box disambiguation strategies:
765///
766/// * [`AmbiguousZoned::compatible`] implements the
767/// [`Disambiguation::Compatible`] strategy. In the case of a gap, the offset
768/// after the gap is selected. In the case of a fold, the offset before the
769/// fold occurs is selected.
770/// * [`AmbiguousZoned::earlier`] implements the
771/// [`Disambiguation::Earlier`] strategy. This always selects the "earlier"
772/// offset.
773/// * [`AmbiguousZoned::later`] implements the
774/// [`Disambiguation::Later`] strategy. This always selects the "later"
775/// offset.
776/// * [`AmbiguousZoned::unambiguous`] implements the
777/// [`Disambiguation::Reject`] strategy. It acts as an assertion that the
778/// offset is unambiguous. If it is ambiguous, then an appropriate error is
779/// returned.
780///
781/// The [`AmbiguousZoned::disambiguate`] method can be used with the
782/// [`Disambiguation`] enum when the disambiguation strategy isn't known until
783/// runtime.
784///
785/// Note also that these aren't the only disambiguation strategies. The
786/// [`AmbiguousOffset`] type, accessible via [`AmbiguousZoned::offset`],
787/// exposes the full details of the ambiguity. So any strategy can be
788/// implemented.
789///
790/// # Example
791///
792/// This example shows how the "compatible" disambiguation strategy is
793/// implemented. Recall that the "compatible" strategy chooses the offset
794/// corresponding to the civil datetime after a gap, and the offset
795/// corresponding to the civil datetime before a gap.
796///
797/// ```
798/// use jiff::{civil::date, tz::{self, AmbiguousOffset}};
799///
800/// let tz = tz::db().get("America/New_York")?;
801/// let dt = date(2024, 3, 10).at(2, 30, 0, 0);
802/// let ambiguous = tz.to_ambiguous_zoned(dt);
803/// let offset = match ambiguous.offset() {
804/// AmbiguousOffset::Unambiguous { offset } => offset,
805/// // This is counter-intuitive, but in order to get the civil datetime
806/// // *after* the gap, we need to select the offset from *before* the
807/// // gap.
808/// AmbiguousOffset::Gap { before, .. } => before,
809/// AmbiguousOffset::Fold { before, .. } => before,
810/// };
811/// let zdt = offset.to_timestamp(dt)?.to_zoned(ambiguous.into_time_zone());
812/// assert_eq!(zdt.to_string(), "2024-03-10T03:30:00-04:00[America/New_York]");
813///
814/// # Ok::<(), Box<dyn std::error::Error>>(())
815/// ```
816#[derive(Clone, Debug, Eq, PartialEq)]
817#[cfg_attr(feature = "defmt", derive(defmt::Format))]
818pub struct AmbiguousZoned {
819 ts: AmbiguousTimestamp,
820 tz: TimeZone,
821}
822
823impl AmbiguousZoned {
824 #[inline]
825 fn new(ts: AmbiguousTimestamp, tz: TimeZone) -> AmbiguousZoned {
826 AmbiguousZoned { ts, tz }
827 }
828
829 /// Returns a reference to the time zone that was used to create this
830 /// ambiguous zoned datetime.
831 ///
832 /// # Example
833 ///
834 /// ```
835 /// use jiff::{civil::date, tz};
836 ///
837 /// let tz = tz::db().get("America/New_York")?;
838 /// let dt = date(2024, 7, 10).at(17, 15, 0, 0);
839 /// let zdt = tz.to_ambiguous_zoned(dt);
840 /// assert_eq!(&tz, zdt.time_zone());
841 ///
842 /// # Ok::<(), Box<dyn std::error::Error>>(())
843 /// ```
844 #[inline]
845 pub fn time_zone(&self) -> &TimeZone {
846 &self.tz
847 }
848
849 /// Consumes this ambiguous zoned datetime and returns the underlying
850 /// `TimeZone`. This is useful if you no longer need the ambiguous zoned
851 /// datetime and want its `TimeZone` without cloning it. (Cloning a
852 /// `TimeZone` is cheap but not free.)
853 ///
854 /// # Example
855 ///
856 /// ```
857 /// use jiff::{civil::date, tz};
858 ///
859 /// let tz = tz::db().get("America/New_York")?;
860 /// let dt = date(2024, 7, 10).at(17, 15, 0, 0);
861 /// let zdt = tz.to_ambiguous_zoned(dt);
862 /// assert_eq!(tz, zdt.into_time_zone());
863 ///
864 /// # Ok::<(), Box<dyn std::error::Error>>(())
865 /// ```
866 #[inline]
867 pub fn into_time_zone(self) -> TimeZone {
868 self.tz
869 }
870
871 /// Returns the civil datetime that was used to create this ambiguous
872 /// zoned datetime.
873 ///
874 /// # Example
875 ///
876 /// ```
877 /// use jiff::{civil::date, tz};
878 ///
879 /// let tz = tz::db().get("America/New_York")?;
880 /// let dt = date(2024, 7, 10).at(17, 15, 0, 0);
881 /// let zdt = tz.to_ambiguous_zoned(dt);
882 /// assert_eq!(zdt.datetime(), dt);
883 ///
884 /// # Ok::<(), Box<dyn std::error::Error>>(())
885 /// ```
886 #[inline]
887 pub fn datetime(&self) -> DateTime {
888 self.ts.datetime()
889 }
890
891 /// Returns the possibly ambiguous offset that is the ultimate source of
892 /// ambiguity.
893 ///
894 /// Most civil datetimes are not ambiguous, and thus, the offset will not
895 /// be ambiguous either. In this case, the offset returned will be the
896 /// [`AmbiguousOffset::Unambiguous`] variant.
897 ///
898 /// But, not all civil datetimes are unambiguous. There are exactly two
899 /// cases where a civil datetime can be ambiguous: when a civil datetime
900 /// does not exist (a gap) or when a civil datetime is repeated (a fold).
901 /// In both such cases, the _offset_ is the thing that is ambiguous as
902 /// there are two possible choices for the offset in both cases: the offset
903 /// before the transition (whether it's a gap or a fold) or the offset
904 /// after the transition.
905 ///
906 /// This type captures the fact that computing an offset from a civil
907 /// datetime in a particular time zone is in one of three possible states:
908 ///
909 /// 1. It is unambiguous.
910 /// 2. It is ambiguous because there is a gap in time.
911 /// 3. It is ambiguous because there is a fold in time.
912 ///
913 /// # Example
914 ///
915 /// ```
916 /// use jiff::{civil::date, tz::{self, AmbiguousOffset}};
917 ///
918 /// let tz = tz::db().get("America/New_York")?;
919 ///
920 /// // Not ambiguous.
921 /// let dt = date(2024, 7, 15).at(17, 30, 0, 0);
922 /// let zdt = tz.to_ambiguous_zoned(dt);
923 /// assert_eq!(zdt.offset(), AmbiguousOffset::Unambiguous {
924 /// offset: tz::offset(-4),
925 /// });
926 ///
927 /// // Ambiguous because of a gap.
928 /// let dt = date(2024, 3, 10).at(2, 30, 0, 0);
929 /// let zdt = tz.to_ambiguous_zoned(dt);
930 /// assert_eq!(zdt.offset(), AmbiguousOffset::Gap {
931 /// before: tz::offset(-5),
932 /// after: tz::offset(-4),
933 /// });
934 ///
935 /// // Ambiguous because of a fold.
936 /// let dt = date(2024, 11, 3).at(1, 30, 0, 0);
937 /// let zdt = tz.to_ambiguous_zoned(dt);
938 /// assert_eq!(zdt.offset(), AmbiguousOffset::Fold {
939 /// before: tz::offset(-4),
940 /// after: tz::offset(-5),
941 /// });
942 ///
943 /// # Ok::<(), Box<dyn std::error::Error>>(())
944 /// ```
945 #[inline]
946 pub fn offset(&self) -> AmbiguousOffset {
947 self.ts.offset
948 }
949
950 /// Returns true if and only if this possibly ambiguous zoned datetime is
951 /// actually ambiguous.
952 ///
953 /// This occurs precisely in cases when the offset is _not_
954 /// [`AmbiguousOffset::Unambiguous`].
955 ///
956 /// # Example
957 ///
958 /// ```
959 /// use jiff::{civil::date, tz};
960 ///
961 /// let tz = tz::db().get("America/New_York")?;
962 ///
963 /// // Not ambiguous.
964 /// let dt = date(2024, 7, 15).at(17, 30, 0, 0);
965 /// let zdt = tz.to_ambiguous_zoned(dt);
966 /// assert!(!zdt.is_ambiguous());
967 ///
968 /// // Ambiguous because of a gap.
969 /// let dt = date(2024, 3, 10).at(2, 30, 0, 0);
970 /// let zdt = tz.to_ambiguous_zoned(dt);
971 /// assert!(zdt.is_ambiguous());
972 ///
973 /// // Ambiguous because of a fold.
974 /// let dt = date(2024, 11, 3).at(1, 30, 0, 0);
975 /// let zdt = tz.to_ambiguous_zoned(dt);
976 /// assert!(zdt.is_ambiguous());
977 ///
978 /// # Ok::<(), Box<dyn std::error::Error>>(())
979 /// ```
980 #[inline]
981 pub fn is_ambiguous(&self) -> bool {
982 !matches!(self.offset(), AmbiguousOffset::Unambiguous { .. })
983 }
984
985 /// Disambiguates this zoned datetime according to the
986 /// [`Disambiguation::Compatible`] strategy.
987 ///
988 /// If this zoned datetime is unambiguous, then this is a no-op.
989 ///
990 /// The "compatible" strategy selects the offset corresponding to the civil
991 /// time after a gap, and the offset corresponding to the civil time before
992 /// a fold. This is what is specified in [RFC 5545].
993 ///
994 /// [RFC 5545]: https://datatracker.ietf.org/doc/html/rfc5545
995 ///
996 /// # Errors
997 ///
998 /// This returns an error when the combination of the civil datetime
999 /// and offset would lead to a `Zoned` with a timestamp outside of the
1000 /// [`Timestamp::MIN`] and [`Timestamp::MAX`] limits. This only occurs
1001 /// when the civil datetime is "close" to its own [`DateTime::MIN`]
1002 /// and [`DateTime::MAX`] limits.
1003 ///
1004 /// # Example
1005 ///
1006 /// ```
1007 /// use jiff::{civil::date, tz};
1008 ///
1009 /// let tz = tz::db().get("America/New_York")?;
1010 ///
1011 /// // Not ambiguous.
1012 /// let dt = date(2024, 7, 15).at(17, 30, 0, 0);
1013 /// let zdt = tz.to_ambiguous_zoned(dt);
1014 /// assert_eq!(
1015 /// zdt.compatible()?.to_string(),
1016 /// "2024-07-15T17:30:00-04:00[America/New_York]",
1017 /// );
1018 ///
1019 /// // Ambiguous because of a gap.
1020 /// let dt = date(2024, 3, 10).at(2, 30, 0, 0);
1021 /// let zdt = tz.to_ambiguous_zoned(dt);
1022 /// assert_eq!(
1023 /// zdt.compatible()?.to_string(),
1024 /// "2024-03-10T03:30:00-04:00[America/New_York]",
1025 /// );
1026 ///
1027 /// // Ambiguous because of a fold.
1028 /// let dt = date(2024, 11, 3).at(1, 30, 0, 0);
1029 /// let zdt = tz.to_ambiguous_zoned(dt);
1030 /// assert_eq!(
1031 /// zdt.compatible()?.to_string(),
1032 /// "2024-11-03T01:30:00-04:00[America/New_York]",
1033 /// );
1034 ///
1035 /// # Ok::<(), Box<dyn std::error::Error>>(())
1036 /// ```
1037 #[inline]
1038 pub fn compatible(self) -> Result<Zoned, Error> {
1039 let ts = self
1040 .ts
1041 .compatible()
1042 .with_context(|| E::InTimeZone { tz: self.time_zone().clone() })?;
1043 Ok(ts.to_zoned(self.tz))
1044 }
1045
1046 /// Disambiguates this zoned datetime according to the
1047 /// [`Disambiguation::Earlier`] strategy.
1048 ///
1049 /// If this zoned datetime is unambiguous, then this is a no-op.
1050 ///
1051 /// The "earlier" strategy selects the offset corresponding to the civil
1052 /// time before a gap, and the offset corresponding to the civil time
1053 /// before a fold.
1054 ///
1055 /// # Errors
1056 ///
1057 /// This returns an error when the combination of the civil datetime
1058 /// and offset would lead to a `Zoned` with a timestamp outside of the
1059 /// [`Timestamp::MIN`] and [`Timestamp::MAX`] limits. This only occurs
1060 /// when the civil datetime is "close" to its own [`DateTime::MIN`]
1061 /// and [`DateTime::MAX`] limits.
1062 ///
1063 /// # Example
1064 ///
1065 /// ```
1066 /// use jiff::{civil::date, tz};
1067 ///
1068 /// let tz = tz::db().get("America/New_York")?;
1069 ///
1070 /// // Not ambiguous.
1071 /// let dt = date(2024, 7, 15).at(17, 30, 0, 0);
1072 /// let zdt = tz.to_ambiguous_zoned(dt);
1073 /// assert_eq!(
1074 /// zdt.earlier()?.to_string(),
1075 /// "2024-07-15T17:30:00-04:00[America/New_York]",
1076 /// );
1077 ///
1078 /// // Ambiguous because of a gap.
1079 /// let dt = date(2024, 3, 10).at(2, 30, 0, 0);
1080 /// let zdt = tz.to_ambiguous_zoned(dt);
1081 /// assert_eq!(
1082 /// zdt.earlier()?.to_string(),
1083 /// "2024-03-10T01:30:00-05:00[America/New_York]",
1084 /// );
1085 ///
1086 /// // Ambiguous because of a fold.
1087 /// let dt = date(2024, 11, 3).at(1, 30, 0, 0);
1088 /// let zdt = tz.to_ambiguous_zoned(dt);
1089 /// assert_eq!(
1090 /// zdt.earlier()?.to_string(),
1091 /// "2024-11-03T01:30:00-04:00[America/New_York]",
1092 /// );
1093 ///
1094 /// # Ok::<(), Box<dyn std::error::Error>>(())
1095 /// ```
1096 #[inline]
1097 pub fn earlier(self) -> Result<Zoned, Error> {
1098 let ts = self
1099 .ts
1100 .earlier()
1101 .with_context(|| E::InTimeZone { tz: self.time_zone().clone() })?;
1102 Ok(ts.to_zoned(self.tz))
1103 }
1104
1105 /// Disambiguates this zoned datetime according to the
1106 /// [`Disambiguation::Later`] strategy.
1107 ///
1108 /// If this zoned datetime is unambiguous, then this is a no-op.
1109 ///
1110 /// The "later" strategy selects the offset corresponding to the civil
1111 /// time after a gap, and the offset corresponding to the civil time
1112 /// after a fold.
1113 ///
1114 /// # Errors
1115 ///
1116 /// This returns an error when the combination of the civil datetime
1117 /// and offset would lead to a `Zoned` with a timestamp outside of the
1118 /// [`Timestamp::MIN`] and [`Timestamp::MAX`] limits. This only occurs
1119 /// when the civil datetime is "close" to its own [`DateTime::MIN`]
1120 /// and [`DateTime::MAX`] limits.
1121 ///
1122 /// # Example
1123 ///
1124 /// ```
1125 /// use jiff::{civil::date, tz};
1126 ///
1127 /// let tz = tz::db().get("America/New_York")?;
1128 ///
1129 /// // Not ambiguous.
1130 /// let dt = date(2024, 7, 15).at(17, 30, 0, 0);
1131 /// let zdt = tz.to_ambiguous_zoned(dt);
1132 /// assert_eq!(
1133 /// zdt.later()?.to_string(),
1134 /// "2024-07-15T17:30:00-04:00[America/New_York]",
1135 /// );
1136 ///
1137 /// // Ambiguous because of a gap.
1138 /// let dt = date(2024, 3, 10).at(2, 30, 0, 0);
1139 /// let zdt = tz.to_ambiguous_zoned(dt);
1140 /// assert_eq!(
1141 /// zdt.later()?.to_string(),
1142 /// "2024-03-10T03:30:00-04:00[America/New_York]",
1143 /// );
1144 ///
1145 /// // Ambiguous because of a fold.
1146 /// let dt = date(2024, 11, 3).at(1, 30, 0, 0);
1147 /// let zdt = tz.to_ambiguous_zoned(dt);
1148 /// assert_eq!(
1149 /// zdt.later()?.to_string(),
1150 /// "2024-11-03T01:30:00-05:00[America/New_York]",
1151 /// );
1152 ///
1153 /// # Ok::<(), Box<dyn std::error::Error>>(())
1154 /// ```
1155 #[inline]
1156 pub fn later(self) -> Result<Zoned, Error> {
1157 let ts = self
1158 .ts
1159 .later()
1160 .with_context(|| E::InTimeZone { tz: self.time_zone().clone() })?;
1161 Ok(ts.to_zoned(self.tz))
1162 }
1163
1164 /// Disambiguates this zoned datetime according to the
1165 /// [`Disambiguation::Reject`] strategy.
1166 ///
1167 /// If this zoned datetime is unambiguous, then this is a no-op.
1168 ///
1169 /// The "reject" strategy always returns an error when the zoned datetime
1170 /// is ambiguous.
1171 ///
1172 /// # Errors
1173 ///
1174 /// This returns an error when the combination of the civil datetime
1175 /// and offset would lead to a `Zoned` with a timestamp outside of the
1176 /// [`Timestamp::MIN`] and [`Timestamp::MAX`] limits. This only occurs
1177 /// when the civil datetime is "close" to its own [`DateTime::MIN`]
1178 /// and [`DateTime::MAX`] limits.
1179 ///
1180 /// This also returns an error when the timestamp is ambiguous.
1181 ///
1182 /// # Example
1183 ///
1184 /// ```
1185 /// use jiff::{civil::date, tz};
1186 ///
1187 /// let tz = tz::db().get("America/New_York")?;
1188 ///
1189 /// // Not ambiguous.
1190 /// let dt = date(2024, 7, 15).at(17, 30, 0, 0);
1191 /// let zdt = tz.to_ambiguous_zoned(dt);
1192 /// assert_eq!(
1193 /// zdt.later()?.to_string(),
1194 /// "2024-07-15T17:30:00-04:00[America/New_York]",
1195 /// );
1196 ///
1197 /// // Ambiguous because of a gap.
1198 /// let dt = date(2024, 3, 10).at(2, 30, 0, 0);
1199 /// let zdt = tz.to_ambiguous_zoned(dt);
1200 /// assert!(zdt.unambiguous().is_err());
1201 ///
1202 /// // Ambiguous because of a fold.
1203 /// let dt = date(2024, 11, 3).at(1, 30, 0, 0);
1204 /// let zdt = tz.to_ambiguous_zoned(dt);
1205 /// assert!(zdt.unambiguous().is_err());
1206 ///
1207 /// # Ok::<(), Box<dyn std::error::Error>>(())
1208 /// ```
1209 #[inline]
1210 pub fn unambiguous(self) -> Result<Zoned, Error> {
1211 let ts = self
1212 .ts
1213 .unambiguous()
1214 .with_context(|| E::InTimeZone { tz: self.time_zone().clone() })?;
1215 Ok(ts.to_zoned(self.tz))
1216 }
1217
1218 /// Disambiguates this (possibly ambiguous) timestamp into a concrete
1219 /// time zone aware timestamp.
1220 ///
1221 /// This is the same as calling one of the disambiguation methods, but
1222 /// the method chosen is indicated by the option given. This is useful
1223 /// when the disambiguation option needs to be chosen at runtime.
1224 ///
1225 /// # Errors
1226 ///
1227 /// This returns an error if this would have returned a zoned datetime
1228 /// outside of its minimum and maximum values.
1229 ///
1230 /// This can also return an error when using the [`Disambiguation::Reject`]
1231 /// strategy. Namely, when using the `Reject` strategy, any ambiguous
1232 /// timestamp always results in an error.
1233 ///
1234 /// # Example
1235 ///
1236 /// This example shows the various disambiguation modes when given a
1237 /// datetime that falls in a "fold" (i.e., a backwards DST transition).
1238 ///
1239 /// ```
1240 /// use jiff::{civil::date, tz::{self, Disambiguation}};
1241 ///
1242 /// let newyork = tz::db().get("America/New_York")?;
1243 /// let dt = date(2024, 11, 3).at(1, 30, 0, 0);
1244 /// let ambiguous = newyork.to_ambiguous_zoned(dt);
1245 ///
1246 /// // In compatible mode, backward transitions select the earlier
1247 /// // time. In the EDT->EST transition, that's the -04 (EDT) offset.
1248 /// let zdt = ambiguous.clone().disambiguate(Disambiguation::Compatible)?;
1249 /// assert_eq!(
1250 /// zdt.to_string(),
1251 /// "2024-11-03T01:30:00-04:00[America/New_York]",
1252 /// );
1253 ///
1254 /// // The earlier time in the EDT->EST transition is the -04 (EDT) offset.
1255 /// let zdt = ambiguous.clone().disambiguate(Disambiguation::Earlier)?;
1256 /// assert_eq!(
1257 /// zdt.to_string(),
1258 /// "2024-11-03T01:30:00-04:00[America/New_York]",
1259 /// );
1260 ///
1261 /// // The later time in the EDT->EST transition is the -05 (EST) offset.
1262 /// let zdt = ambiguous.clone().disambiguate(Disambiguation::Later)?;
1263 /// assert_eq!(
1264 /// zdt.to_string(),
1265 /// "2024-11-03T01:30:00-05:00[America/New_York]",
1266 /// );
1267 ///
1268 /// // Since our datetime is ambiguous, the 'reject' strategy errors.
1269 /// assert!(ambiguous.disambiguate(Disambiguation::Reject).is_err());
1270 ///
1271 /// # Ok::<(), Box<dyn std::error::Error>>(())
1272 /// ```
1273 #[inline]
1274 pub fn disambiguate(self, option: Disambiguation) -> Result<Zoned, Error> {
1275 match option {
1276 Disambiguation::Compatible => self.compatible(),
1277 Disambiguation::Earlier => self.earlier(),
1278 Disambiguation::Later => self.later(),
1279 Disambiguation::Reject => self.unambiguous(),
1280 }
1281 }
1282}