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