/rust/registry/src/index.crates.io-1949cf8c6b5b557f/jiff-0.2.28/src/tz/ambiguous.rs
Line | Count | Source |
1 | | use crate::{ |
2 | | civil::DateTime, |
3 | | error::{tz::ambiguous::Error as E, 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 | 0 | pub(crate) const fn from_iambiguous_offset_const( |
208 | 0 | iaoff: IAmbiguousOffset, |
209 | 0 | ) -> AmbiguousOffset { |
210 | 0 | match iaoff { |
211 | 0 | IAmbiguousOffset::Unambiguous { offset } => { |
212 | 0 | let offset = Offset::from_ioffset_const(offset); |
213 | 0 | AmbiguousOffset::Unambiguous { offset } |
214 | | } |
215 | 0 | IAmbiguousOffset::Gap { before, after } => { |
216 | 0 | let before = Offset::from_ioffset_const(before); |
217 | 0 | let after = Offset::from_ioffset_const(after); |
218 | 0 | AmbiguousOffset::Gap { before, after } |
219 | | } |
220 | 0 | IAmbiguousOffset::Fold { before, after } => { |
221 | 0 | let before = Offset::from_ioffset_const(before); |
222 | 0 | let after = Offset::from_ioffset_const(after); |
223 | 0 | AmbiguousOffset::Fold { before, after } |
224 | | } |
225 | | } |
226 | 0 | } |
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 | 0 | pub(crate) fn new( |
306 | 0 | dt: DateTime, |
307 | 0 | kind: AmbiguousOffset, |
308 | 0 | ) -> AmbiguousTimestamp { |
309 | 0 | AmbiguousTimestamp { dt, offset: kind } |
310 | 0 | } |
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 | 0 | pub fn datetime(&self) -> DateTime { |
329 | 0 | self.dt |
330 | 0 | } |
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 | 0 | pub fn offset(&self) -> AmbiguousOffset { |
388 | 0 | self.offset |
389 | 0 | } |
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}; |
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 | 0 | pub fn is_ambiguous(&self) -> bool { |
423 | 0 | !matches!(self.offset(), AmbiguousOffset::Unambiguous { .. }) |
424 | 0 | } |
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 | 0 | pub fn compatible(self) -> Result<Timestamp, Error> { |
480 | 0 | let offset = match self.offset() { |
481 | 0 | AmbiguousOffset::Unambiguous { offset } => offset, |
482 | 0 | AmbiguousOffset::Gap { before, .. } => before, |
483 | 0 | AmbiguousOffset::Fold { before, .. } => before, |
484 | | }; |
485 | 0 | offset.to_timestamp(self.dt) |
486 | 0 | } |
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 | 0 | pub fn earlier(self) -> Result<Timestamp, Error> { |
540 | 0 | let offset = match self.offset() { |
541 | 0 | AmbiguousOffset::Unambiguous { offset } => offset, |
542 | 0 | AmbiguousOffset::Gap { after, .. } => after, |
543 | 0 | AmbiguousOffset::Fold { before, .. } => before, |
544 | | }; |
545 | 0 | offset.to_timestamp(self.dt) |
546 | 0 | } |
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 | 0 | pub fn later(self) -> Result<Timestamp, Error> { |
600 | 0 | let offset = match self.offset() { |
601 | 0 | AmbiguousOffset::Unambiguous { offset } => offset, |
602 | 0 | AmbiguousOffset::Gap { before, .. } => before, |
603 | 0 | AmbiguousOffset::Fold { after, .. } => after, |
604 | | }; |
605 | 0 | offset.to_timestamp(self.dt) |
606 | 0 | } |
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 | 0 | pub fn unambiguous(self) -> Result<Timestamp, Error> { |
655 | 0 | let offset = match self.offset() { |
656 | 0 | AmbiguousOffset::Unambiguous { offset } => offset, |
657 | 0 | AmbiguousOffset::Gap { before, after } => { |
658 | 0 | return Err(Error::from(E::BecauseGap { before, after })); |
659 | | } |
660 | 0 | AmbiguousOffset::Fold { before, after } => { |
661 | 0 | return Err(Error::from(E::BecauseFold { before, after })); |
662 | | } |
663 | | }; |
664 | 0 | offset.to_timestamp(self.dt) |
665 | 0 | } |
666 | | |
667 | | /// Disambiguates this (possibly ambiguous) timestamp into a specific |
668 | | /// timestamp. |
669 | | /// |
670 | | /// This is the same as calling one of the disambiguation methods, but |
671 | | /// the method chosen is indicated by the option given. This is useful |
672 | | /// when the disambiguation option needs to be chosen at runtime. |
673 | | /// |
674 | | /// # Errors |
675 | | /// |
676 | | /// This returns an error if this would have returned a timestamp |
677 | | /// outside of its minimum and maximum values. |
678 | | /// |
679 | | /// This can also return an error when using the [`Disambiguation::Reject`] |
680 | | /// strategy. Namely, when using the `Reject` strategy, any ambiguous |
681 | | /// timestamp always results in an error. |
682 | | /// |
683 | | /// # Example |
684 | | /// |
685 | | /// This example shows the various disambiguation modes when given a |
686 | | /// datetime that falls in a "fold" (i.e., a backwards DST transition). |
687 | | /// |
688 | | /// ``` |
689 | | /// use jiff::{civil::date, tz::{self, Disambiguation}}; |
690 | | /// |
691 | | /// let newyork = tz::db().get("America/New_York")?; |
692 | | /// let dt = date(2024, 11, 3).at(1, 30, 0, 0); |
693 | | /// let ambiguous = newyork.to_ambiguous_timestamp(dt); |
694 | | /// |
695 | | /// // In compatible mode, backward transitions select the earlier |
696 | | /// // time. In the EDT->EST transition, that's the -04 (EDT) offset. |
697 | | /// let ts = ambiguous.clone().disambiguate(Disambiguation::Compatible)?; |
698 | | /// assert_eq!(ts.to_string(), "2024-11-03T05:30:00Z"); |
699 | | /// |
700 | | /// // The earlier time in the EDT->EST transition is the -04 (EDT) offset. |
701 | | /// let ts = ambiguous.clone().disambiguate(Disambiguation::Earlier)?; |
702 | | /// assert_eq!(ts.to_string(), "2024-11-03T05:30:00Z"); |
703 | | /// |
704 | | /// // The later time in the EDT->EST transition is the -05 (EST) offset. |
705 | | /// let ts = ambiguous.clone().disambiguate(Disambiguation::Later)?; |
706 | | /// assert_eq!(ts.to_string(), "2024-11-03T06:30:00Z"); |
707 | | /// |
708 | | /// // Since our datetime is ambiguous, the 'reject' strategy errors. |
709 | | /// assert!(ambiguous.disambiguate(Disambiguation::Reject).is_err()); |
710 | | /// |
711 | | /// # Ok::<(), Box<dyn std::error::Error>>(()) |
712 | | /// ``` |
713 | | #[inline] |
714 | 0 | pub fn disambiguate( |
715 | 0 | self, |
716 | 0 | option: Disambiguation, |
717 | 0 | ) -> Result<Timestamp, Error> { |
718 | 0 | match option { |
719 | 0 | Disambiguation::Compatible => self.compatible(), |
720 | 0 | Disambiguation::Earlier => self.earlier(), |
721 | 0 | Disambiguation::Later => self.later(), |
722 | 0 | Disambiguation::Reject => self.unambiguous(), |
723 | | } |
724 | 0 | } |
725 | | |
726 | | /// Convert this ambiguous timestamp into an ambiguous zoned date time by |
727 | | /// attaching a time zone. |
728 | | /// |
729 | | /// This is useful when you have a [`civil::DateTime`], [`TimeZone`] and |
730 | | /// want to convert it to an instant while applying a particular |
731 | | /// disambiguation strategy without an extra clone of the `TimeZone`. |
732 | | /// |
733 | | /// This isn't currently exposed because I believe use cases for crate |
734 | | /// users can be satisfied via [`TimeZone::into_ambiguous_zoned`] (which |
735 | | /// is implemented via this routine). |
736 | | #[inline] |
737 | 0 | pub(crate) fn into_ambiguous_zoned(self, tz: TimeZone) -> AmbiguousZoned { |
738 | 0 | AmbiguousZoned::new(self, tz) |
739 | 0 | } |
740 | | } |
741 | | |
742 | | /// A possibly ambiguous [`Zoned`], created by |
743 | | /// [`TimeZone::to_ambiguous_zoned`]. |
744 | | /// |
745 | | /// While this is called an ambiguous zoned datetime, the thing that is |
746 | | /// actually ambiguous is the offset. That is, an ambiguous zoned datetime |
747 | | /// is actually a triple of a [`civil::DateTime`](crate::civil::DateTime), a |
748 | | /// [`TimeZone`] and an [`AmbiguousOffset`]. |
749 | | /// |
750 | | /// When the offset is ambiguous, it either represents a gap (civil time is |
751 | | /// skipped) or a fold (civil time is repeated). In both cases, there are, by |
752 | | /// construction, two different offsets to choose from: the offset from before |
753 | | /// the transition and the offset from after the transition. |
754 | | /// |
755 | | /// The purpose of this type is to represent that ambiguity (when it occurs) |
756 | | /// and enable callers to make a choice about how to resolve that ambiguity. |
757 | | /// In some cases, you might want to reject ambiguity altogether, which is |
758 | | /// supported by the [`AmbiguousZoned::unambiguous`] routine. |
759 | | /// |
760 | | /// This type provides four different out-of-the-box disambiguation strategies: |
761 | | /// |
762 | | /// * [`AmbiguousZoned::compatible`] implements the |
763 | | /// [`Disambiguation::Compatible`] strategy. In the case of a gap, the offset |
764 | | /// after the gap is selected. In the case of a fold, the offset before the |
765 | | /// fold occurs is selected. |
766 | | /// * [`AmbiguousZoned::earlier`] implements the |
767 | | /// [`Disambiguation::Earlier`] strategy. This always selects the "earlier" |
768 | | /// offset. |
769 | | /// * [`AmbiguousZoned::later`] implements the |
770 | | /// [`Disambiguation::Later`] strategy. This always selects the "later" |
771 | | /// offset. |
772 | | /// * [`AmbiguousZoned::unambiguous`] implements the |
773 | | /// [`Disambiguation::Reject`] strategy. It acts as an assertion that the |
774 | | /// offset is unambiguous. If it is ambiguous, then an appropriate error is |
775 | | /// returned. |
776 | | /// |
777 | | /// The [`AmbiguousZoned::disambiguate`] method can be used with the |
778 | | /// [`Disambiguation`] enum when the disambiguation strategy isn't known until |
779 | | /// runtime. |
780 | | /// |
781 | | /// Note also that these aren't the only disambiguation strategies. The |
782 | | /// [`AmbiguousOffset`] type, accessible via [`AmbiguousZoned::offset`], |
783 | | /// exposes the full details of the ambiguity. So any strategy can be |
784 | | /// implemented. |
785 | | /// |
786 | | /// # Example |
787 | | /// |
788 | | /// This example shows how the "compatible" disambiguation strategy is |
789 | | /// implemented. Recall that the "compatible" strategy chooses the offset |
790 | | /// corresponding to the civil datetime after a gap, and the offset |
791 | | /// corresponding to the civil datetime before a gap. |
792 | | /// |
793 | | /// ``` |
794 | | /// use jiff::{civil::date, tz::{self, AmbiguousOffset}}; |
795 | | /// |
796 | | /// let tz = tz::db().get("America/New_York")?; |
797 | | /// let dt = date(2024, 3, 10).at(2, 30, 0, 0); |
798 | | /// let ambiguous = tz.to_ambiguous_zoned(dt); |
799 | | /// let offset = match ambiguous.offset() { |
800 | | /// AmbiguousOffset::Unambiguous { offset } => offset, |
801 | | /// // This is counter-intuitive, but in order to get the civil datetime |
802 | | /// // *after* the gap, we need to select the offset from *before* the |
803 | | /// // gap. |
804 | | /// AmbiguousOffset::Gap { before, .. } => before, |
805 | | /// AmbiguousOffset::Fold { before, .. } => before, |
806 | | /// }; |
807 | | /// let zdt = offset.to_timestamp(dt)?.to_zoned(ambiguous.into_time_zone()); |
808 | | /// assert_eq!(zdt.to_string(), "2024-03-10T03:30:00-04:00[America/New_York]"); |
809 | | /// |
810 | | /// # Ok::<(), Box<dyn std::error::Error>>(()) |
811 | | /// ``` |
812 | | #[derive(Clone, Debug, Eq, PartialEq)] |
813 | | pub struct AmbiguousZoned { |
814 | | ts: AmbiguousTimestamp, |
815 | | tz: TimeZone, |
816 | | } |
817 | | |
818 | | impl AmbiguousZoned { |
819 | | #[inline] |
820 | 0 | fn new(ts: AmbiguousTimestamp, tz: TimeZone) -> AmbiguousZoned { |
821 | 0 | AmbiguousZoned { ts, tz } |
822 | 0 | } |
823 | | |
824 | | /// Returns a reference to the time zone that was used to create this |
825 | | /// ambiguous zoned datetime. |
826 | | /// |
827 | | /// # Example |
828 | | /// |
829 | | /// ``` |
830 | | /// use jiff::{civil::date, tz}; |
831 | | /// |
832 | | /// let tz = tz::db().get("America/New_York")?; |
833 | | /// let dt = date(2024, 7, 10).at(17, 15, 0, 0); |
834 | | /// let zdt = tz.to_ambiguous_zoned(dt); |
835 | | /// assert_eq!(&tz, zdt.time_zone()); |
836 | | /// |
837 | | /// # Ok::<(), Box<dyn std::error::Error>>(()) |
838 | | /// ``` |
839 | | #[inline] |
840 | 0 | pub fn time_zone(&self) -> &TimeZone { |
841 | 0 | &self.tz |
842 | 0 | } |
843 | | |
844 | | /// Consumes this ambiguous zoned datetime and returns the underlying |
845 | | /// `TimeZone`. This is useful if you no longer need the ambiguous zoned |
846 | | /// datetime and want its `TimeZone` without cloning it. (Cloning a |
847 | | /// `TimeZone` is cheap but not free.) |
848 | | /// |
849 | | /// # Example |
850 | | /// |
851 | | /// ``` |
852 | | /// use jiff::{civil::date, tz}; |
853 | | /// |
854 | | /// let tz = tz::db().get("America/New_York")?; |
855 | | /// let dt = date(2024, 7, 10).at(17, 15, 0, 0); |
856 | | /// let zdt = tz.to_ambiguous_zoned(dt); |
857 | | /// assert_eq!(tz, zdt.into_time_zone()); |
858 | | /// |
859 | | /// # Ok::<(), Box<dyn std::error::Error>>(()) |
860 | | /// ``` |
861 | | #[inline] |
862 | 0 | pub fn into_time_zone(self) -> TimeZone { |
863 | 0 | self.tz |
864 | 0 | } |
865 | | |
866 | | /// Returns the civil datetime that was used to create this ambiguous |
867 | | /// zoned datetime. |
868 | | /// |
869 | | /// # Example |
870 | | /// |
871 | | /// ``` |
872 | | /// use jiff::{civil::date, tz}; |
873 | | /// |
874 | | /// let tz = tz::db().get("America/New_York")?; |
875 | | /// let dt = date(2024, 7, 10).at(17, 15, 0, 0); |
876 | | /// let zdt = tz.to_ambiguous_zoned(dt); |
877 | | /// assert_eq!(zdt.datetime(), dt); |
878 | | /// |
879 | | /// # Ok::<(), Box<dyn std::error::Error>>(()) |
880 | | /// ``` |
881 | | #[inline] |
882 | 0 | pub fn datetime(&self) -> DateTime { |
883 | 0 | self.ts.datetime() |
884 | 0 | } |
885 | | |
886 | | /// Returns the possibly ambiguous offset that is the ultimate source of |
887 | | /// ambiguity. |
888 | | /// |
889 | | /// Most civil datetimes are not ambiguous, and thus, the offset will not |
890 | | /// be ambiguous either. In this case, the offset returned will be the |
891 | | /// [`AmbiguousOffset::Unambiguous`] variant. |
892 | | /// |
893 | | /// But, not all civil datetimes are unambiguous. There are exactly two |
894 | | /// cases where a civil datetime can be ambiguous: when a civil datetime |
895 | | /// does not exist (a gap) or when a civil datetime is repeated (a fold). |
896 | | /// In both such cases, the _offset_ is the thing that is ambiguous as |
897 | | /// there are two possible choices for the offset in both cases: the offset |
898 | | /// before the transition (whether it's a gap or a fold) or the offset |
899 | | /// after the transition. |
900 | | /// |
901 | | /// This type captures the fact that computing an offset from a civil |
902 | | /// datetime in a particular time zone is in one of three possible states: |
903 | | /// |
904 | | /// 1. It is unambiguous. |
905 | | /// 2. It is ambiguous because there is a gap in time. |
906 | | /// 3. It is ambiguous because there is a fold in time. |
907 | | /// |
908 | | /// # Example |
909 | | /// |
910 | | /// ``` |
911 | | /// use jiff::{civil::date, tz::{self, AmbiguousOffset}}; |
912 | | /// |
913 | | /// let tz = tz::db().get("America/New_York")?; |
914 | | /// |
915 | | /// // Not ambiguous. |
916 | | /// let dt = date(2024, 7, 15).at(17, 30, 0, 0); |
917 | | /// let zdt = tz.to_ambiguous_zoned(dt); |
918 | | /// assert_eq!(zdt.offset(), AmbiguousOffset::Unambiguous { |
919 | | /// offset: tz::offset(-4), |
920 | | /// }); |
921 | | /// |
922 | | /// // Ambiguous because of a gap. |
923 | | /// let dt = date(2024, 3, 10).at(2, 30, 0, 0); |
924 | | /// let zdt = tz.to_ambiguous_zoned(dt); |
925 | | /// assert_eq!(zdt.offset(), AmbiguousOffset::Gap { |
926 | | /// before: tz::offset(-5), |
927 | | /// after: tz::offset(-4), |
928 | | /// }); |
929 | | /// |
930 | | /// // Ambiguous because of a fold. |
931 | | /// let dt = date(2024, 11, 3).at(1, 30, 0, 0); |
932 | | /// let zdt = tz.to_ambiguous_zoned(dt); |
933 | | /// assert_eq!(zdt.offset(), AmbiguousOffset::Fold { |
934 | | /// before: tz::offset(-4), |
935 | | /// after: tz::offset(-5), |
936 | | /// }); |
937 | | /// |
938 | | /// # Ok::<(), Box<dyn std::error::Error>>(()) |
939 | | /// ``` |
940 | | #[inline] |
941 | 0 | pub fn offset(&self) -> AmbiguousOffset { |
942 | 0 | self.ts.offset |
943 | 0 | } |
944 | | |
945 | | /// Returns true if and only if this possibly ambiguous zoned datetime is |
946 | | /// actually ambiguous. |
947 | | /// |
948 | | /// This occurs precisely in cases when the offset is _not_ |
949 | | /// [`AmbiguousOffset::Unambiguous`]. |
950 | | /// |
951 | | /// # Example |
952 | | /// |
953 | | /// ``` |
954 | | /// use jiff::{civil::date, tz}; |
955 | | /// |
956 | | /// let tz = tz::db().get("America/New_York")?; |
957 | | /// |
958 | | /// // Not ambiguous. |
959 | | /// let dt = date(2024, 7, 15).at(17, 30, 0, 0); |
960 | | /// let zdt = tz.to_ambiguous_zoned(dt); |
961 | | /// assert!(!zdt.is_ambiguous()); |
962 | | /// |
963 | | /// // Ambiguous because of a gap. |
964 | | /// let dt = date(2024, 3, 10).at(2, 30, 0, 0); |
965 | | /// let zdt = tz.to_ambiguous_zoned(dt); |
966 | | /// assert!(zdt.is_ambiguous()); |
967 | | /// |
968 | | /// // Ambiguous because of a fold. |
969 | | /// let dt = date(2024, 11, 3).at(1, 30, 0, 0); |
970 | | /// let zdt = tz.to_ambiguous_zoned(dt); |
971 | | /// assert!(zdt.is_ambiguous()); |
972 | | /// |
973 | | /// # Ok::<(), Box<dyn std::error::Error>>(()) |
974 | | /// ``` |
975 | | #[inline] |
976 | 0 | pub fn is_ambiguous(&self) -> bool { |
977 | 0 | !matches!(self.offset(), AmbiguousOffset::Unambiguous { .. }) |
978 | 0 | } |
979 | | |
980 | | /// Disambiguates this zoned datetime according to the |
981 | | /// [`Disambiguation::Compatible`] strategy. |
982 | | /// |
983 | | /// If this zoned datetime is unambiguous, then this is a no-op. |
984 | | /// |
985 | | /// The "compatible" strategy selects the offset corresponding to the civil |
986 | | /// time after a gap, and the offset corresponding to the civil time before |
987 | | /// a fold. This is what is specified in [RFC 5545]. |
988 | | /// |
989 | | /// [RFC 5545]: https://datatracker.ietf.org/doc/html/rfc5545 |
990 | | /// |
991 | | /// # Errors |
992 | | /// |
993 | | /// This returns an error when the combination of the civil datetime |
994 | | /// and offset would lead to a `Zoned` with a timestamp outside of the |
995 | | /// [`Timestamp::MIN`] and [`Timestamp::MAX`] limits. This only occurs |
996 | | /// when the civil datetime is "close" to its own [`DateTime::MIN`] |
997 | | /// and [`DateTime::MAX`] limits. |
998 | | /// |
999 | | /// # Example |
1000 | | /// |
1001 | | /// ``` |
1002 | | /// use jiff::{civil::date, tz}; |
1003 | | /// |
1004 | | /// let tz = tz::db().get("America/New_York")?; |
1005 | | /// |
1006 | | /// // Not ambiguous. |
1007 | | /// let dt = date(2024, 7, 15).at(17, 30, 0, 0); |
1008 | | /// let zdt = tz.to_ambiguous_zoned(dt); |
1009 | | /// assert_eq!( |
1010 | | /// zdt.compatible()?.to_string(), |
1011 | | /// "2024-07-15T17:30:00-04:00[America/New_York]", |
1012 | | /// ); |
1013 | | /// |
1014 | | /// // Ambiguous because of a gap. |
1015 | | /// let dt = date(2024, 3, 10).at(2, 30, 0, 0); |
1016 | | /// let zdt = tz.to_ambiguous_zoned(dt); |
1017 | | /// assert_eq!( |
1018 | | /// zdt.compatible()?.to_string(), |
1019 | | /// "2024-03-10T03:30:00-04:00[America/New_York]", |
1020 | | /// ); |
1021 | | /// |
1022 | | /// // Ambiguous because of a fold. |
1023 | | /// let dt = date(2024, 11, 3).at(1, 30, 0, 0); |
1024 | | /// let zdt = tz.to_ambiguous_zoned(dt); |
1025 | | /// assert_eq!( |
1026 | | /// zdt.compatible()?.to_string(), |
1027 | | /// "2024-11-03T01:30:00-04:00[America/New_York]", |
1028 | | /// ); |
1029 | | /// |
1030 | | /// # Ok::<(), Box<dyn std::error::Error>>(()) |
1031 | | /// ``` |
1032 | | #[inline] |
1033 | 0 | pub fn compatible(self) -> Result<Zoned, Error> { |
1034 | 0 | let ts = self |
1035 | 0 | .ts |
1036 | 0 | .compatible() |
1037 | 0 | .with_context(|| E::InTimeZone { tz: self.time_zone().clone() })?; |
1038 | 0 | Ok(ts.to_zoned(self.tz)) |
1039 | 0 | } |
1040 | | |
1041 | | /// Disambiguates this zoned datetime according to the |
1042 | | /// [`Disambiguation::Earlier`] strategy. |
1043 | | /// |
1044 | | /// If this zoned datetime is unambiguous, then this is a no-op. |
1045 | | /// |
1046 | | /// The "earlier" strategy selects the offset corresponding to the civil |
1047 | | /// time before a gap, and the offset corresponding to the civil time |
1048 | | /// before a fold. |
1049 | | /// |
1050 | | /// # Errors |
1051 | | /// |
1052 | | /// This returns an error when the combination of the civil datetime |
1053 | | /// and offset would lead to a `Zoned` with a timestamp outside of the |
1054 | | /// [`Timestamp::MIN`] and [`Timestamp::MAX`] limits. This only occurs |
1055 | | /// when the civil datetime is "close" to its own [`DateTime::MIN`] |
1056 | | /// and [`DateTime::MAX`] limits. |
1057 | | /// |
1058 | | /// # Example |
1059 | | /// |
1060 | | /// ``` |
1061 | | /// use jiff::{civil::date, tz}; |
1062 | | /// |
1063 | | /// let tz = tz::db().get("America/New_York")?; |
1064 | | /// |
1065 | | /// // Not ambiguous. |
1066 | | /// let dt = date(2024, 7, 15).at(17, 30, 0, 0); |
1067 | | /// let zdt = tz.to_ambiguous_zoned(dt); |
1068 | | /// assert_eq!( |
1069 | | /// zdt.earlier()?.to_string(), |
1070 | | /// "2024-07-15T17:30:00-04:00[America/New_York]", |
1071 | | /// ); |
1072 | | /// |
1073 | | /// // Ambiguous because of a gap. |
1074 | | /// let dt = date(2024, 3, 10).at(2, 30, 0, 0); |
1075 | | /// let zdt = tz.to_ambiguous_zoned(dt); |
1076 | | /// assert_eq!( |
1077 | | /// zdt.earlier()?.to_string(), |
1078 | | /// "2024-03-10T01:30:00-05:00[America/New_York]", |
1079 | | /// ); |
1080 | | /// |
1081 | | /// // Ambiguous because of a fold. |
1082 | | /// let dt = date(2024, 11, 3).at(1, 30, 0, 0); |
1083 | | /// let zdt = tz.to_ambiguous_zoned(dt); |
1084 | | /// assert_eq!( |
1085 | | /// zdt.earlier()?.to_string(), |
1086 | | /// "2024-11-03T01:30:00-04:00[America/New_York]", |
1087 | | /// ); |
1088 | | /// |
1089 | | /// # Ok::<(), Box<dyn std::error::Error>>(()) |
1090 | | /// ``` |
1091 | | #[inline] |
1092 | 0 | pub fn earlier(self) -> Result<Zoned, Error> { |
1093 | 0 | let ts = self |
1094 | 0 | .ts |
1095 | 0 | .earlier() |
1096 | 0 | .with_context(|| E::InTimeZone { tz: self.time_zone().clone() })?; |
1097 | 0 | Ok(ts.to_zoned(self.tz)) |
1098 | 0 | } |
1099 | | |
1100 | | /// Disambiguates this zoned datetime according to the |
1101 | | /// [`Disambiguation::Later`] strategy. |
1102 | | /// |
1103 | | /// If this zoned datetime is unambiguous, then this is a no-op. |
1104 | | /// |
1105 | | /// The "later" strategy selects the offset corresponding to the civil |
1106 | | /// time after a gap, and the offset corresponding to the civil time |
1107 | | /// after a fold. |
1108 | | /// |
1109 | | /// # Errors |
1110 | | /// |
1111 | | /// This returns an error when the combination of the civil datetime |
1112 | | /// and offset would lead to a `Zoned` with a timestamp outside of the |
1113 | | /// [`Timestamp::MIN`] and [`Timestamp::MAX`] limits. This only occurs |
1114 | | /// when the civil datetime is "close" to its own [`DateTime::MIN`] |
1115 | | /// and [`DateTime::MAX`] limits. |
1116 | | /// |
1117 | | /// # Example |
1118 | | /// |
1119 | | /// ``` |
1120 | | /// use jiff::{civil::date, tz}; |
1121 | | /// |
1122 | | /// let tz = tz::db().get("America/New_York")?; |
1123 | | /// |
1124 | | /// // Not ambiguous. |
1125 | | /// let dt = date(2024, 7, 15).at(17, 30, 0, 0); |
1126 | | /// let zdt = tz.to_ambiguous_zoned(dt); |
1127 | | /// assert_eq!( |
1128 | | /// zdt.later()?.to_string(), |
1129 | | /// "2024-07-15T17:30:00-04:00[America/New_York]", |
1130 | | /// ); |
1131 | | /// |
1132 | | /// // Ambiguous because of a gap. |
1133 | | /// let dt = date(2024, 3, 10).at(2, 30, 0, 0); |
1134 | | /// let zdt = tz.to_ambiguous_zoned(dt); |
1135 | | /// assert_eq!( |
1136 | | /// zdt.later()?.to_string(), |
1137 | | /// "2024-03-10T03:30:00-04:00[America/New_York]", |
1138 | | /// ); |
1139 | | /// |
1140 | | /// // Ambiguous because of a fold. |
1141 | | /// let dt = date(2024, 11, 3).at(1, 30, 0, 0); |
1142 | | /// let zdt = tz.to_ambiguous_zoned(dt); |
1143 | | /// assert_eq!( |
1144 | | /// zdt.later()?.to_string(), |
1145 | | /// "2024-11-03T01:30:00-05:00[America/New_York]", |
1146 | | /// ); |
1147 | | /// |
1148 | | /// # Ok::<(), Box<dyn std::error::Error>>(()) |
1149 | | /// ``` |
1150 | | #[inline] |
1151 | 0 | pub fn later(self) -> Result<Zoned, Error> { |
1152 | 0 | let ts = self |
1153 | 0 | .ts |
1154 | 0 | .later() |
1155 | 0 | .with_context(|| E::InTimeZone { tz: self.time_zone().clone() })?; |
1156 | 0 | Ok(ts.to_zoned(self.tz)) |
1157 | 0 | } |
1158 | | |
1159 | | /// Disambiguates this zoned datetime according to the |
1160 | | /// [`Disambiguation::Reject`] strategy. |
1161 | | /// |
1162 | | /// If this zoned datetime is unambiguous, then this is a no-op. |
1163 | | /// |
1164 | | /// The "reject" strategy always returns an error when the zoned datetime |
1165 | | /// is ambiguous. |
1166 | | /// |
1167 | | /// # Errors |
1168 | | /// |
1169 | | /// This returns an error when the combination of the civil datetime |
1170 | | /// and offset would lead to a `Zoned` with a timestamp outside of the |
1171 | | /// [`Timestamp::MIN`] and [`Timestamp::MAX`] limits. This only occurs |
1172 | | /// when the civil datetime is "close" to its own [`DateTime::MIN`] |
1173 | | /// and [`DateTime::MAX`] limits. |
1174 | | /// |
1175 | | /// This also returns an error when the timestamp is ambiguous. |
1176 | | /// |
1177 | | /// # Example |
1178 | | /// |
1179 | | /// ``` |
1180 | | /// use jiff::{civil::date, tz}; |
1181 | | /// |
1182 | | /// let tz = tz::db().get("America/New_York")?; |
1183 | | /// |
1184 | | /// // Not ambiguous. |
1185 | | /// let dt = date(2024, 7, 15).at(17, 30, 0, 0); |
1186 | | /// let zdt = tz.to_ambiguous_zoned(dt); |
1187 | | /// assert_eq!( |
1188 | | /// zdt.later()?.to_string(), |
1189 | | /// "2024-07-15T17:30:00-04:00[America/New_York]", |
1190 | | /// ); |
1191 | | /// |
1192 | | /// // Ambiguous because of a gap. |
1193 | | /// let dt = date(2024, 3, 10).at(2, 30, 0, 0); |
1194 | | /// let zdt = tz.to_ambiguous_zoned(dt); |
1195 | | /// assert!(zdt.unambiguous().is_err()); |
1196 | | /// |
1197 | | /// // Ambiguous because of a fold. |
1198 | | /// let dt = date(2024, 11, 3).at(1, 30, 0, 0); |
1199 | | /// let zdt = tz.to_ambiguous_zoned(dt); |
1200 | | /// assert!(zdt.unambiguous().is_err()); |
1201 | | /// |
1202 | | /// # Ok::<(), Box<dyn std::error::Error>>(()) |
1203 | | /// ``` |
1204 | | #[inline] |
1205 | 0 | pub fn unambiguous(self) -> Result<Zoned, Error> { |
1206 | 0 | let ts = self |
1207 | 0 | .ts |
1208 | 0 | .unambiguous() |
1209 | 0 | .with_context(|| E::InTimeZone { tz: self.time_zone().clone() })?; |
1210 | 0 | Ok(ts.to_zoned(self.tz)) |
1211 | 0 | } |
1212 | | |
1213 | | /// Disambiguates this (possibly ambiguous) timestamp into a concrete |
1214 | | /// time zone aware timestamp. |
1215 | | /// |
1216 | | /// This is the same as calling one of the disambiguation methods, but |
1217 | | /// the method chosen is indicated by the option given. This is useful |
1218 | | /// when the disambiguation option needs to be chosen at runtime. |
1219 | | /// |
1220 | | /// # Errors |
1221 | | /// |
1222 | | /// This returns an error if this would have returned a zoned datetime |
1223 | | /// outside of its minimum and maximum values. |
1224 | | /// |
1225 | | /// This can also return an error when using the [`Disambiguation::Reject`] |
1226 | | /// strategy. Namely, when using the `Reject` strategy, any ambiguous |
1227 | | /// timestamp always results in an error. |
1228 | | /// |
1229 | | /// # Example |
1230 | | /// |
1231 | | /// This example shows the various disambiguation modes when given a |
1232 | | /// datetime that falls in a "fold" (i.e., a backwards DST transition). |
1233 | | /// |
1234 | | /// ``` |
1235 | | /// use jiff::{civil::date, tz::{self, Disambiguation}}; |
1236 | | /// |
1237 | | /// let newyork = tz::db().get("America/New_York")?; |
1238 | | /// let dt = date(2024, 11, 3).at(1, 30, 0, 0); |
1239 | | /// let ambiguous = newyork.to_ambiguous_zoned(dt); |
1240 | | /// |
1241 | | /// // In compatible mode, backward transitions select the earlier |
1242 | | /// // time. In the EDT->EST transition, that's the -04 (EDT) offset. |
1243 | | /// let zdt = ambiguous.clone().disambiguate(Disambiguation::Compatible)?; |
1244 | | /// assert_eq!( |
1245 | | /// zdt.to_string(), |
1246 | | /// "2024-11-03T01:30:00-04:00[America/New_York]", |
1247 | | /// ); |
1248 | | /// |
1249 | | /// // The earlier time in the EDT->EST transition is the -04 (EDT) offset. |
1250 | | /// let zdt = ambiguous.clone().disambiguate(Disambiguation::Earlier)?; |
1251 | | /// assert_eq!( |
1252 | | /// zdt.to_string(), |
1253 | | /// "2024-11-03T01:30:00-04:00[America/New_York]", |
1254 | | /// ); |
1255 | | /// |
1256 | | /// // The later time in the EDT->EST transition is the -05 (EST) offset. |
1257 | | /// let zdt = ambiguous.clone().disambiguate(Disambiguation::Later)?; |
1258 | | /// assert_eq!( |
1259 | | /// zdt.to_string(), |
1260 | | /// "2024-11-03T01:30:00-05:00[America/New_York]", |
1261 | | /// ); |
1262 | | /// |
1263 | | /// // Since our datetime is ambiguous, the 'reject' strategy errors. |
1264 | | /// assert!(ambiguous.disambiguate(Disambiguation::Reject).is_err()); |
1265 | | /// |
1266 | | /// # Ok::<(), Box<dyn std::error::Error>>(()) |
1267 | | /// ``` |
1268 | | #[inline] |
1269 | 0 | pub fn disambiguate(self, option: Disambiguation) -> Result<Zoned, Error> { |
1270 | 0 | match option { |
1271 | 0 | Disambiguation::Compatible => self.compatible(), |
1272 | 0 | Disambiguation::Earlier => self.earlier(), |
1273 | 0 | Disambiguation::Later => self.later(), |
1274 | 0 | Disambiguation::Reject => self.unambiguous(), |
1275 | | } |
1276 | 0 | } |
1277 | | } |