1 | use 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 ] |
75 | pub 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)] |
153 | pub 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 | |
205 | impl AmbiguousOffset { |
206 | #[inline ] |
207 | pub(crate) const fn from_iambiguous_offset_const( |
208 | iaoff: IAmbiguousOffset, |
209 | ) -> AmbiguousOffset { |
210 | match iaoff { |
211 | IAmbiguousOffset::Unambiguous { offset: IOffset } => { |
212 | let offset: Offset = Offset::from_ioffset_const(ioff:offset); |
213 | AmbiguousOffset::Unambiguous { offset } |
214 | } |
215 | IAmbiguousOffset::Gap { before: IOffset, after: IOffset } => { |
216 | let before: Offset = Offset::from_ioffset_const(ioff:before); |
217 | let after: Offset = Offset::from_ioffset_const(ioff:after); |
218 | AmbiguousOffset::Gap { before, after } |
219 | } |
220 | IAmbiguousOffset::Fold { before: IOffset, after: IOffset } => { |
221 | let before: Offset = Offset::from_ioffset_const(ioff:before); |
222 | let after: Offset = Offset::from_ioffset_const(ioff: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)] |
298 | pub struct AmbiguousTimestamp { |
299 | dt: DateTime, |
300 | offset: AmbiguousOffset, |
301 | } |
302 | |
303 | impl 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)] |
821 | pub struct AmbiguousZoned { |
822 | ts: AmbiguousTimestamp, |
823 | tz: TimeZone, |
824 | } |
825 | |
826 | impl 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 | } |
1298 | |