1 | use crate::{ |
2 | civil::{Date, DateTime, Time}, |
3 | error::Error, |
4 | tz::{Offset, TimeZone, TimeZoneDatabase}, |
5 | util::borrow::StringCow, |
6 | Timestamp, Zoned, |
7 | }; |
8 | |
9 | /// A low level representation of a parsed Temporal ISO 8601 datetime string. |
10 | /// |
11 | /// Most users should not need to use or care about this type. Its purpose is |
12 | /// to represent the individual components of a datetime string for more |
13 | /// flexible parsing when use cases call for it. |
14 | /// |
15 | /// One can parse into `Pieces` via [`Pieces::parse`]. Its date, time |
16 | /// (optional), offset (optional) and time zone annotation (optional) can be |
17 | /// queried independently. Each component corresponds to the following in a |
18 | /// datetime string: |
19 | /// |
20 | /// ```text |
21 | /// {date}T{time}{offset}[{time-zone-annotation}] |
22 | /// ``` |
23 | /// |
24 | /// For example: |
25 | /// |
26 | /// ```text |
27 | /// 2025-01-03T19:54-05[America/New_York] |
28 | /// ``` |
29 | /// |
30 | /// A date is the only required component. |
31 | /// |
32 | /// A `Pieces` can also be constructed from structured values via its `From` |
33 | /// trait implementations. The `From` trait has the following implementations |
34 | /// available: |
35 | /// |
36 | /// * `From<Date>` creates a `Pieces` with just a civil [`Date`]. All other |
37 | /// components are left empty. |
38 | /// * `From<DateTime>` creates a `Pieces` with a civil [`Date`] and [`Time`]. |
39 | /// The offset and time zone annotation are left empty. |
40 | /// * `From<Timestamp>` creates a `Pieces` from a [`Timestamp`] using |
41 | /// a Zulu offset. This signifies that the precise instant is known, but the |
42 | /// local time's offset from UTC is unknown. The [`Date`] and [`Time`] are |
43 | /// determined via `Offset::UTC.to_datetime(timestamp)`. The time zone |
44 | /// annotation is left empty. |
45 | /// * `From<(Timestamp, Offset)>` creates a `Pieces` from a [`Timestamp`] and |
46 | /// an [`Offset`]. The [`Date`] and [`Time`] are determined via |
47 | /// `offset.to_datetime(timestamp)`. The time zone annotation is left empty. |
48 | /// * `From<&Zoned>` creates a `Pieces` from a [`Zoned`]. This populates all |
49 | /// fields of a `Pieces`. |
50 | /// |
51 | /// A `Pieces` can be converted to a Temporal ISO 8601 string via its `Display` |
52 | /// trait implementation. |
53 | /// |
54 | /// # Example: distinguishing between `Z`, `+00:00` and `-00:00` |
55 | /// |
56 | /// With `Pieces`, it's possible to parse a datetime string and inspect the |
57 | /// "type" of its offset when it is zero. This makes use of the |
58 | /// [`PiecesOffset`] and [`PiecesNumericOffset`] auxiliary types. |
59 | /// |
60 | /// ``` |
61 | /// use jiff::{ |
62 | /// fmt::temporal::{Pieces, PiecesNumericOffset, PiecesOffset}, |
63 | /// tz::Offset, |
64 | /// }; |
65 | /// |
66 | /// let pieces = Pieces::parse("1970-01-01T00:00:00Z" )?; |
67 | /// let off = pieces.offset().unwrap(); |
68 | /// // Parsed as Zulu. |
69 | /// assert_eq!(off, PiecesOffset::Zulu); |
70 | /// // Gets converted from Zulu to UTC, i.e., just zero. |
71 | /// assert_eq!(off.to_numeric_offset(), Offset::UTC); |
72 | /// |
73 | /// let pieces = Pieces::parse("1970-01-01T00:00:00-00:00" )?; |
74 | /// let off = pieces.offset().unwrap(); |
75 | /// // Parsed as a negative zero. |
76 | /// assert_eq!(off, PiecesOffset::from( |
77 | /// PiecesNumericOffset::from(Offset::UTC).with_negative_zero(), |
78 | /// )); |
79 | /// // Gets converted from -00:00 to UTC, i.e., just zero. |
80 | /// assert_eq!(off.to_numeric_offset(), Offset::UTC); |
81 | /// |
82 | /// let pieces = Pieces::parse("1970-01-01T00:00:00+00:00" )?; |
83 | /// let off = pieces.offset().unwrap(); |
84 | /// // Parsed as a positive zero. |
85 | /// assert_eq!(off, PiecesOffset::from( |
86 | /// PiecesNumericOffset::from(Offset::UTC), |
87 | /// )); |
88 | /// // Gets converted from -00:00 to UTC, i.e., just zero. |
89 | /// assert_eq!(off.to_numeric_offset(), Offset::UTC); |
90 | /// |
91 | /// # Ok::<(), Box<dyn std::error::Error>>(()) |
92 | /// ``` |
93 | /// |
94 | /// It's rare to need to care about these differences, but the above example |
95 | /// demonstrates that `Pieces` doesn't try to do any automatic translation for |
96 | /// you. |
97 | /// |
98 | /// # Example: it is very easy to misuse `Pieces` |
99 | /// |
100 | /// This example shows how easily you can shoot yourself in the foot with |
101 | /// `Pieces`: |
102 | /// |
103 | /// ``` |
104 | /// use jiff::{fmt::temporal::{Pieces, TimeZoneAnnotation}, tz}; |
105 | /// |
106 | /// let mut pieces = Pieces::parse("2025-01-03T07:55+02[Africa/Cairo]" )?; |
107 | /// pieces = pieces.with_offset(tz::offset(-10)); |
108 | /// // This is nonsense because the offset isn't compatible with the time zone! |
109 | /// // Moreover, the actual instant that this timestamp represents has changed. |
110 | /// assert_eq!(pieces.to_string(), "2025-01-03T07:55:00-10:00[Africa/Cairo]" ); |
111 | /// |
112 | /// # Ok::<(), Box<dyn std::error::Error>>(()) |
113 | /// ``` |
114 | /// |
115 | /// In the above example, we take a parsed `Pieces`, change its offset and |
116 | /// then format it back into a string. There are no speed bumps or errors. |
117 | /// A `Pieces` will just blindly follow your instruction, even if it produces |
118 | /// a nonsense result. Nonsense results are still parsable back into `Pieces`: |
119 | /// |
120 | /// ``` |
121 | /// use jiff::{civil, fmt::temporal::Pieces, tz::{TimeZone, offset}}; |
122 | /// |
123 | /// let pieces = Pieces::parse("2025-01-03T07:55:00-10:00[Africa/Cairo]" )?; |
124 | /// assert_eq!(pieces.date(), civil::date(2025, 1, 3)); |
125 | /// assert_eq!(pieces.time(), Some(civil::time(7, 55, 0, 0))); |
126 | /// assert_eq!(pieces.to_numeric_offset(), Some(offset(-10))); |
127 | /// assert_eq!(pieces.to_time_zone()?, Some(TimeZone::get("Africa/Cairo" )?)); |
128 | /// |
129 | /// # Ok::<(), Box<dyn std::error::Error>>(()) |
130 | /// ``` |
131 | /// |
132 | /// This exemplifies that `Pieces` is a mostly "dumb" type that passes |
133 | /// through the data it contains, even if it doesn't make sense. |
134 | /// |
135 | /// # Case study: how to parse `2025-01-03T17:28-05` into `Zoned` |
136 | /// |
137 | /// One thing in particular that `Pieces` enables callers to do is side-step |
138 | /// some of the stricter requirements placed on the higher level parsing |
139 | /// functions (such as `Zoned`'s `FromStr` trait implementation). For example, |
140 | /// parsing a datetime string into a `Zoned` _requires_ that the string contain |
141 | /// a time zone annotation. Namely, parsing `2025-01-03T17:28-05` into a |
142 | /// `Zoned` will fail: |
143 | /// |
144 | /// ``` |
145 | /// use jiff::Zoned; |
146 | /// |
147 | /// assert_eq!( |
148 | /// "2025-01-03T17:28-05" .parse::<Zoned>().unwrap_err().to_string(), |
149 | /// "failed to find time zone in square brackets in \ |
150 | /// \"2025-01-03T17:28-05 \", which is required for \ |
151 | /// parsing a zoned instant" , |
152 | /// ); |
153 | /// ``` |
154 | /// |
155 | /// The above fails because an RFC 3339 timestamp only contains an offset, |
156 | /// not a time zone, and thus the resulting `Zoned` could never do time zone |
157 | /// aware arithmetic. |
158 | /// |
159 | /// However, in some cases, you might want to bypass these protections and |
160 | /// creat a `Zoned` value with a fixed offset time zone anyway. For example, |
161 | /// perhaps your use cases don't need time zone aware arithmetic, but want to |
162 | /// preserve the offset anyway. This can be accomplished with `Pieces`: |
163 | /// |
164 | /// ``` |
165 | /// use jiff::{fmt::temporal::Pieces, tz::TimeZone}; |
166 | /// |
167 | /// let pieces = Pieces::parse("2025-01-03T17:28-05" )?; |
168 | /// let time = pieces.time().unwrap_or_else(jiff::civil::Time::midnight); |
169 | /// let dt = pieces.date().to_datetime(time); |
170 | /// let Some(offset) = pieces.to_numeric_offset() else { |
171 | /// let msg = format!( |
172 | /// "datetime string has no offset, \ |
173 | /// and thus cannot be parsed into an instant" , |
174 | /// ); |
175 | /// return Err(msg.into()); |
176 | /// }; |
177 | /// let zdt = TimeZone::fixed(offset).to_zoned(dt)?; |
178 | /// assert_eq!(zdt.to_string(), "2025-01-03T17:28:00-05:00[-05:00]" ); |
179 | /// |
180 | /// # Ok::<(), Box<dyn std::error::Error>>(()) |
181 | /// ``` |
182 | /// |
183 | /// One problem with the above code snippet is that it completely ignores if |
184 | /// a time zone annotation is present. If it is, it probably makes sense to use |
185 | /// it, but "fall back" to a fixed offset time zone if it isn't (which the |
186 | /// higher level `Zoned` parsing function won't do for you): |
187 | /// |
188 | /// ``` |
189 | /// use jiff::{fmt::temporal::Pieces, tz::TimeZone}; |
190 | /// |
191 | /// let timestamp = "2025-01-02T15:13-05" ; |
192 | /// |
193 | /// let pieces = Pieces::parse(timestamp)?; |
194 | /// let time = pieces.time().unwrap_or_else(jiff::civil::Time::midnight); |
195 | /// let dt = pieces.date().to_datetime(time); |
196 | /// let tz = match pieces.to_time_zone()? { |
197 | /// Some(tz) => tz, |
198 | /// None => { |
199 | /// let Some(offset) = pieces.to_numeric_offset() else { |
200 | /// let msg = format!( |
201 | /// "timestamp `{timestamp}` has no time zone \ |
202 | /// or offset, and thus cannot be parsed into \ |
203 | /// an instant" , |
204 | /// ); |
205 | /// return Err(msg.into()); |
206 | /// }; |
207 | /// TimeZone::fixed(offset) |
208 | /// } |
209 | /// }; |
210 | /// // We don't bother with offset conflict resolution. And note that |
211 | /// // this uses automatic "compatible" disambiguation in the case of |
212 | /// // discontinuities. Of course, this is all moot if `TimeZone` is |
213 | /// // fixed. The above code handles the case where it isn't! |
214 | /// let zdt = tz.to_zoned(dt)?; |
215 | /// assert_eq!(zdt.to_string(), "2025-01-02T15:13:00-05:00[-05:00]" ); |
216 | /// |
217 | /// # Ok::<(), Box<dyn std::error::Error>>(()) |
218 | /// ``` |
219 | /// |
220 | /// This is mostly the same as above, but if an annotation is present, we use |
221 | /// a `TimeZone` derived from that over the offset present. |
222 | /// |
223 | /// However, this still doesn't quite capture what happens when parsing into a |
224 | /// `Zoned` value. In particular, parsing into a `Zoned` is _also_ doing offset |
225 | /// conflict resolution for you. An offset conflict occurs when there is a |
226 | /// mismatch between the offset in an RFC 3339 timestamp and the time zone in |
227 | /// an RFC 9557 time zone annotation. |
228 | /// |
229 | /// For example, `2024-06-14T17:30-05[America/New_York]` has a mismatch |
230 | /// since the date is in daylight saving time, but the offset, `-05`, is the |
231 | /// offset for standard time in `America/New_York`. If this datetime were |
232 | /// fed to the above code, then the `-05` offset would be completely ignored |
233 | /// and `America/New_York` would resolve the datetime based on its rules. In |
234 | /// this case, you'd get `2024-06-14T17:30-04`, which is a different instant |
235 | /// than the original datetime! |
236 | /// |
237 | /// You can either implement your own conflict resolution or use |
238 | /// [`tz::OffsetConflict`](crate::tz::OffsetConflict) to do it for you. |
239 | /// |
240 | /// ``` |
241 | /// use jiff::{fmt::temporal::Pieces, tz::{OffsetConflict, TimeZone}}; |
242 | /// |
243 | /// let timestamp = "2024-06-14T17:30-05[America/New_York]" ; |
244 | /// // The default for conflict resolution when parsing into a `Zoned` is |
245 | /// // actually `Reject`, but we use `AlwaysOffset` here to show a different |
246 | /// // strategy. You'll want to pick the conflict resolution that suits your |
247 | /// // needs. The `Reject` strategy is what you should pick if you aren't |
248 | /// // sure. |
249 | /// let conflict_resolution = OffsetConflict::AlwaysOffset; |
250 | /// |
251 | /// let pieces = Pieces::parse(timestamp)?; |
252 | /// let time = pieces.time().unwrap_or_else(jiff::civil::Time::midnight); |
253 | /// let dt = pieces.date().to_datetime(time); |
254 | /// let ambiguous_zdt = match pieces.to_time_zone()? { |
255 | /// Some(tz) => { |
256 | /// match pieces.to_numeric_offset() { |
257 | /// None => tz.into_ambiguous_zoned(dt), |
258 | /// Some(offset) => { |
259 | /// conflict_resolution.resolve(dt, offset, tz)? |
260 | /// } |
261 | /// } |
262 | /// } |
263 | /// None => { |
264 | /// let Some(offset) = pieces.to_numeric_offset() else { |
265 | /// let msg = format!( |
266 | /// "timestamp `{timestamp}` has no time zone \ |
267 | /// or offset, and thus cannot be parsed into \ |
268 | /// an instant" , |
269 | /// ); |
270 | /// return Err(msg.into()); |
271 | /// }; |
272 | /// // Won't even be ambiguous, but gets us the same |
273 | /// // type as the branch above. |
274 | /// TimeZone::fixed(offset).into_ambiguous_zoned(dt) |
275 | /// } |
276 | /// }; |
277 | /// // We do compatible disambiguation here like we do in the previous |
278 | /// // examples, but you could choose any strategy. As with offset conflict |
279 | /// // resolution, if you aren't sure what to pick, a safe choice here would |
280 | /// // be `ambiguous_zdt.unambiguous()`, which will return an error if the |
281 | /// // datetime is ambiguous in any way. Then, if you ever hit an error, you |
282 | /// // can examine the case to see if it should be handled in a different way. |
283 | /// let zdt = ambiguous_zdt.compatible()?; |
284 | /// // Notice that we now have a different civil time and offset, but the |
285 | /// // instant it corresponds to is the same as the one we started with. |
286 | /// assert_eq!(zdt.to_string(), "2024-06-14T18:30:00-04:00[America/New_York]" ); |
287 | /// |
288 | /// # Ok::<(), Box<dyn std::error::Error>>(()) |
289 | /// ``` |
290 | /// |
291 | /// The above has effectively completely rebuilt the higher level `Zoned` |
292 | /// parsing routine, but with a fallback to a fixed time zone when a time zone |
293 | /// annotation is not present. |
294 | /// |
295 | /// # Case study: inferring the time zone of RFC 3339 timestamps |
296 | /// |
297 | /// As [one real world use case details][infer-time-zone], it might be |
298 | /// desirable to try and infer the time zone of RFC 3339 timestamps with |
299 | /// varying offsets. This might be applicable when: |
300 | /// |
301 | /// * You have out-of-band information, possibly contextual, that indicates |
302 | /// the timestamps have to come from a fixed set of time zones. |
303 | /// * The time zones have different standard offsets. |
304 | /// * You have a specific desire or need to use a [`Zoned`] value for its |
305 | /// ergonomics and time zone aware handling. After all, in this case, you |
306 | /// believe the timestamps to actually be generated from a specific time zone, |
307 | /// but the interchange format doesn't support carrying that information. Or |
308 | /// the source data simply omits it. |
309 | /// |
310 | /// In other words, you might be trying to make the best of a bad situation. |
311 | /// |
312 | /// A `Pieces` can help you accomplish this because it gives you access to each |
313 | /// component of a parsed datetime, and thus lets you implement arbitrary logic |
314 | /// for how to translate that into a `Zoned`. In this case, there is |
315 | /// contextual information that Jiff can't possibly know about. |
316 | /// |
317 | /// The general approach we take here is to make use of |
318 | /// [`tz::OffsetConflict`](crate::tz::OffsetConflict) to query whether a |
319 | /// timestamp has a fixed offset compatible with a particular time zone. And if |
320 | /// so, we can _probably_ assume it comes from that time zone. One hitch is |
321 | /// that it's possible for the timestamp to be valid for multiple time zones, |
322 | /// so we check that as well. |
323 | /// |
324 | /// In the use case linked above, we have fixed offset timestamps from |
325 | /// `America/Chicago` and `America/New_York`. So let's try implementing the |
326 | /// above strategy. Note that we assume our inputs are RFC 3339 fixed offset |
327 | /// timestamps and error otherwise. This is just to keep things simple. To |
328 | /// handle data that is more varied, see the previous case study where we |
329 | /// respect a time zone annotation if it's present, and fall back to a fixed |
330 | /// offset time zone if it isn't. |
331 | /// |
332 | /// ``` |
333 | /// use jiff::{fmt::temporal::Pieces, tz::{OffsetConflict, TimeZone}, Zoned}; |
334 | /// |
335 | /// // The time zones we're allowed to choose from. |
336 | /// let tzs = &[ |
337 | /// TimeZone::get("America/New_York" )?, |
338 | /// TimeZone::get("America/Chicago" )?, |
339 | /// ]; |
340 | /// |
341 | /// // Here's our data that lacks time zones. The task is to assign a time zone |
342 | /// // from `tzs` to each below and convert it to a `Zoned`. If we fail on any |
343 | /// // one, then we substitute `None`. |
344 | /// let data = &[ |
345 | /// "2024-01-13T10:33-05" , |
346 | /// "2024-01-25T12:15-06" , |
347 | /// "2024-03-10T02:30-05" , |
348 | /// "2024-06-08T14:01-05" , |
349 | /// "2024-06-12T11:46-04" , |
350 | /// "2024-11-03T01:30-05" , |
351 | /// ]; |
352 | /// // Our answers. |
353 | /// let mut zdts: Vec<Option<Zoned>> = vec![]; |
354 | /// for string in data { |
355 | /// // Parse and gather up the data that we can from the input. |
356 | /// // In this case, that's a civil datetime and an offset from UTC. |
357 | /// let pieces = Pieces::parse(string)?; |
358 | /// let time = pieces.time().unwrap_or_else(jiff::civil::Time::midnight); |
359 | /// let dt = pieces.date().to_datetime(time); |
360 | /// let Some(offset) = pieces.to_numeric_offset() else { |
361 | /// // A robust implementation should use a TZ annotation if present. |
362 | /// return Err("missing offset" .into()); |
363 | /// }; |
364 | /// // Now collect all time zones that are valid for this timestamp. |
365 | /// let mut candidates = vec![]; |
366 | /// for tz in tzs { |
367 | /// let result = OffsetConflict::Reject.resolve(dt, offset, tz.clone()); |
368 | /// // The parsed offset isn't valid for this time zone, so reject it. |
369 | /// let Ok(ambiguous_zdt) = result else { continue }; |
370 | /// // This can never fail because we used the "reject" conflict |
371 | /// // resolution strategy. It will never return an ambiguous |
372 | /// // `Zoned` since we always have a valid offset that does |
373 | /// // disambiguation for us. |
374 | /// let zdt = ambiguous_zdt.unambiguous().unwrap(); |
375 | /// candidates.push(zdt); |
376 | /// } |
377 | /// if candidates.len() == 1 { |
378 | /// zdts.push(Some(candidates.pop().unwrap())); |
379 | /// } else { |
380 | /// zdts.push(None); |
381 | /// } |
382 | /// } |
383 | /// assert_eq!(zdts, vec![ |
384 | /// Some("2024-01-13T10:33-05[America/New_York]" .parse()?), |
385 | /// Some("2024-01-25T12:15-06[America/Chicago]" .parse()?), |
386 | /// // Failed because the clock time falls in a gap in the |
387 | /// // transition to daylight saving time, and it could be |
388 | /// // valid for either America/New_York or America/Chicago. |
389 | /// None, |
390 | /// Some("2024-06-08T14:01-05[America/Chicago]" .parse()?), |
391 | /// Some("2024-06-12T11:46-04[America/New_York]" .parse()?), |
392 | /// // Failed because the clock time falls in a fold in the |
393 | /// // transition out of daylight saving time, and it could be |
394 | /// // valid for either America/New_York or America/Chicago. |
395 | /// None, |
396 | /// ]); |
397 | /// |
398 | /// # Ok::<(), Box<dyn std::error::Error>>(()) |
399 | /// ``` |
400 | /// |
401 | /// The one hitch here is that if the time zones are close to each |
402 | /// geographically and both have daylight saving time, then there are some |
403 | /// RFC 3339 timestamps that are truly ambiguous. For example, |
404 | /// `2024-11-03T01:30-05` is perfectly valid for both `America/New_York` and |
405 | /// `America/Chicago`. In this case, there is no way to tell which time zone |
406 | /// the timestamp belongs to. It might be reasonable to return an error in |
407 | /// this case or omit the timestamp. It depends on what you need to do. |
408 | /// |
409 | /// With more effort, it would also be possible to optimize the above routine |
410 | /// by utilizing [`TimeZone::preceding`] and [`TimeZone::following`] to get |
411 | /// the exact boundaries of each time zone transition. Then you could use an |
412 | /// offset lookup table for each range to determine the appropriate time zone. |
413 | /// |
414 | /// [infer-time-zone]: https://github.com/BurntSushi/jiff/discussions/181#discussioncomment-11729435 |
415 | #[derive (Clone, Debug, Eq, Hash, PartialEq)] |
416 | pub struct Pieces<'n> { |
417 | date: Date, |
418 | time: Option<Time>, |
419 | offset: Option<PiecesOffset>, |
420 | time_zone_annotation: Option<TimeZoneAnnotation<'n>>, |
421 | } |
422 | |
423 | impl<'n> Pieces<'n> { |
424 | /// Parses a Temporal ISO 8601 datetime string into a `Pieces`. |
425 | /// |
426 | /// This is a convenience routine for |
427 | /// [`DateTimeParser::parses_pieces`](crate::fmt::temporal::DateTimeParser::parse_pieces). |
428 | /// |
429 | /// Note that the `Pieces` returned is parameterized by the lifetime of |
430 | /// `input`. This is because it might borrow a sub-slice of `input` for |
431 | /// a time zone annotation name. For example, |
432 | /// `Canada/Yukon` in `2025-01-03T16:42-07[Canada/Yukon]`. |
433 | /// |
434 | /// # Example |
435 | /// |
436 | /// ``` |
437 | /// use jiff::{civil, fmt::temporal::Pieces, tz::TimeZone}; |
438 | /// |
439 | /// let pieces = Pieces::parse("2025-01-03T16:42[Canada/Yukon]" )?; |
440 | /// assert_eq!(pieces.date(), civil::date(2025, 1, 3)); |
441 | /// assert_eq!(pieces.time(), Some(civil::time(16, 42, 0, 0))); |
442 | /// assert_eq!(pieces.to_numeric_offset(), None); |
443 | /// assert_eq!(pieces.to_time_zone()?, Some(TimeZone::get("Canada/Yukon" )?)); |
444 | /// |
445 | /// # Ok::<(), Box<dyn std::error::Error>>(()) |
446 | /// ``` |
447 | #[inline ] |
448 | pub fn parse<I: ?Sized + AsRef<[u8]> + 'n>( |
449 | input: &'n I, |
450 | ) -> Result<Pieces<'n>, Error> { |
451 | let input = input.as_ref(); |
452 | super::DEFAULT_DATETIME_PARSER.parse_pieces(input) |
453 | } |
454 | |
455 | /// Returns the civil date in this `Pieces`. |
456 | /// |
457 | /// Note that every `Pieces` value is guaranteed to have a `Date`. |
458 | /// |
459 | /// # Example |
460 | /// |
461 | /// ``` |
462 | /// use jiff::{civil, fmt::temporal::Pieces}; |
463 | /// |
464 | /// let pieces = Pieces::parse("2025-01-03" )?; |
465 | /// assert_eq!(pieces.date(), civil::date(2025, 1, 3)); |
466 | /// |
467 | /// # Ok::<(), Box<dyn std::error::Error>>(()) |
468 | /// ``` |
469 | #[inline ] |
470 | pub fn date(&self) -> Date { |
471 | self.date |
472 | } |
473 | |
474 | /// Returns the civil time in this `Pieces`. |
475 | /// |
476 | /// The time component is optional. In |
477 | /// [`DateTimeParser`](crate::fmt::temporal::DateTimeParser), parsing |
478 | /// into types that require a time (like [`DateTime`]) when a time is |
479 | /// missing automatically set the time to midnight. (Or, more precisely, |
480 | /// the first instant of the day.) |
481 | /// |
482 | /// # Example |
483 | /// |
484 | /// ``` |
485 | /// use jiff::{civil, fmt::temporal::Pieces, Zoned}; |
486 | /// |
487 | /// let pieces = Pieces::parse("2025-01-03T14:49:01" )?; |
488 | /// assert_eq!(pieces.date(), civil::date(2025, 1, 3)); |
489 | /// assert_eq!(pieces.time(), Some(civil::time(14, 49, 1, 0))); |
490 | /// |
491 | /// // tricksy tricksy, the first instant of 2015-10-18 in Sao Paulo is |
492 | /// // not midnight! |
493 | /// let pieces = Pieces::parse("2015-10-18[America/Sao_Paulo]" )?; |
494 | /// // Parsing into pieces just gives us the component parts, so no time: |
495 | /// assert_eq!(pieces.time(), None); |
496 | /// |
497 | /// // But if this uses higher level routines to parse into a `Zoned`, |
498 | /// // then we can see that the missing time implies the first instant |
499 | /// // of the day: |
500 | /// let zdt: Zoned = "2015-10-18[America/Sao_Paulo]" .parse()?; |
501 | /// assert_eq!(zdt.time(), jiff::civil::time(1, 0, 0, 0)); |
502 | /// |
503 | /// # Ok::<(), Box<dyn std::error::Error>>(()) |
504 | /// ``` |
505 | #[inline ] |
506 | pub fn time(&self) -> Option<Time> { |
507 | self.time |
508 | } |
509 | |
510 | /// Returns the offset in this `Pieces`. |
511 | /// |
512 | /// The offset returned can be infallibly converted to a numeric offset, |
513 | /// i.e., [`Offset`]. But it also includes extra data to indicate whether |
514 | /// a `Z` or a `-00:00` was parsed. (Neither of which are representable by |
515 | /// an `Offset`, which doesn't distinguish between Zulu and UTC and doesn't |
516 | /// represent negative and positive zero differently.) |
517 | /// |
518 | /// # Example |
519 | /// |
520 | /// This example shows how different flavors of `Offset::UTC` can be parsed |
521 | /// and inspected. |
522 | /// |
523 | /// ``` |
524 | /// use jiff::{ |
525 | /// fmt::temporal::{Pieces, PiecesNumericOffset, PiecesOffset}, |
526 | /// tz::Offset, |
527 | /// }; |
528 | /// |
529 | /// let pieces = Pieces::parse("1970-01-01T00:00:00Z" )?; |
530 | /// let off = pieces.offset().unwrap(); |
531 | /// // Parsed as Zulu. |
532 | /// assert_eq!(off, PiecesOffset::Zulu); |
533 | /// // Gets converted from Zulu to UTC, i.e., just zero. |
534 | /// assert_eq!(off.to_numeric_offset(), Offset::UTC); |
535 | /// |
536 | /// let pieces = Pieces::parse("1970-01-01T00:00:00-00:00" )?; |
537 | /// let off = pieces.offset().unwrap(); |
538 | /// // Parsed as a negative zero. |
539 | /// assert_eq!(off, PiecesOffset::from( |
540 | /// PiecesNumericOffset::from(Offset::UTC).with_negative_zero(), |
541 | /// )); |
542 | /// // Gets converted from -00:00 to UTC, i.e., just zero. |
543 | /// assert_eq!(off.to_numeric_offset(), Offset::UTC); |
544 | /// |
545 | /// let pieces = Pieces::parse("1970-01-01T00:00:00+00:00" )?; |
546 | /// let off = pieces.offset().unwrap(); |
547 | /// // Parsed as a positive zero. |
548 | /// assert_eq!(off, PiecesOffset::from( |
549 | /// PiecesNumericOffset::from(Offset::UTC), |
550 | /// )); |
551 | /// // Gets converted from -00:00 to UTC, i.e., just zero. |
552 | /// assert_eq!(off.to_numeric_offset(), Offset::UTC); |
553 | /// |
554 | /// # Ok::<(), Box<dyn std::error::Error>>(()) |
555 | /// ``` |
556 | #[inline ] |
557 | pub fn offset(&self) -> Option<PiecesOffset> { |
558 | self.offset |
559 | } |
560 | |
561 | /// Returns the time zone annotation in this `Pieces`. |
562 | /// |
563 | /// A time zone annotation is optional. The higher level |
564 | /// [`DateTimeParser`](crate::fmt::temporal::DateTimeParser) |
565 | /// requires a time zone annotation when parsing into a [`Zoned`]. |
566 | /// |
567 | /// A time zone annotation is either an offset, or more commonly, an IANA |
568 | /// time zone identifier. |
569 | /// |
570 | /// # Example |
571 | /// |
572 | /// ``` |
573 | /// use jiff::{fmt::temporal::{Pieces, TimeZoneAnnotation}, tz::offset}; |
574 | /// |
575 | /// // A time zone annotation from a name: |
576 | /// let pieces = Pieces::parse("2025-01-02T16:47-05[America/New_York]" )?; |
577 | /// assert_eq!( |
578 | /// pieces.time_zone_annotation().unwrap(), |
579 | /// &TimeZoneAnnotation::from("America/New_York" ), |
580 | /// ); |
581 | /// |
582 | /// // A time zone annotation from an offset: |
583 | /// let pieces = Pieces::parse("2025-01-02T16:47-05[-05:00]" )?; |
584 | /// assert_eq!( |
585 | /// pieces.time_zone_annotation().unwrap(), |
586 | /// &TimeZoneAnnotation::from(offset(-5)), |
587 | /// ); |
588 | /// |
589 | /// # Ok::<(), Box<dyn std::error::Error>>(()) |
590 | /// ``` |
591 | #[inline ] |
592 | pub fn time_zone_annotation(&self) -> Option<&TimeZoneAnnotation<'n>> { |
593 | self.time_zone_annotation.as_ref() |
594 | } |
595 | |
596 | /// A convenience routine for converting an offset on this `Pieces`, |
597 | /// if present, to a numeric [`Offset`]. |
598 | /// |
599 | /// This collapses the offsets `Z`, `-00:00` and `+00:00` all to |
600 | /// [`Offset::UTC`]. If you need to distinguish between them, then use |
601 | /// [`Pieces::offset`]. |
602 | /// |
603 | /// # Example |
604 | /// |
605 | /// This example shows how `Z`, `-00:00` and `+00:00` all map to the same |
606 | /// [`Offset`] value: |
607 | /// |
608 | /// ``` |
609 | /// use jiff::{fmt::temporal::Pieces, tz::Offset}; |
610 | /// |
611 | /// let pieces = Pieces::parse("1970-01-01T00:00:00Z" )?; |
612 | /// assert_eq!(pieces.to_numeric_offset(), Some(Offset::UTC)); |
613 | /// |
614 | /// let pieces = Pieces::parse("1970-01-01T00:00:00-00:00" )?; |
615 | /// assert_eq!(pieces.to_numeric_offset(), Some(Offset::UTC)); |
616 | /// |
617 | /// let pieces = Pieces::parse("1970-01-01T00:00:00+00:00" )?; |
618 | /// assert_eq!(pieces.to_numeric_offset(), Some(Offset::UTC)); |
619 | /// |
620 | /// # Ok::<(), Box<dyn std::error::Error>>(()) |
621 | /// ``` |
622 | #[inline ] |
623 | pub fn to_numeric_offset(&self) -> Option<Offset> { |
624 | self.offset().map(|poffset| poffset.to_numeric_offset()) |
625 | } |
626 | |
627 | /// A convenience routine for converting a time zone annotation, if |
628 | /// present, into a [`TimeZone`]. |
629 | /// |
630 | /// If no annotation is on this `Pieces`, then this returns `Ok(None)`. |
631 | /// |
632 | /// This may return an error if the time zone annotation is a name and it |
633 | /// couldn't be found in Jiff's global time zone database. |
634 | /// |
635 | /// # Example |
636 | /// |
637 | /// ``` |
638 | /// use jiff::{fmt::temporal::Pieces, tz::{TimeZone, offset}}; |
639 | /// |
640 | /// // No time zone annotations means you get `Ok(None)`: |
641 | /// let pieces = Pieces::parse("2025-01-03T17:13-05" )?; |
642 | /// assert_eq!(pieces.to_time_zone()?, None); |
643 | /// |
644 | /// // An offset time zone annotation gets you a fixed offset `TimeZone`: |
645 | /// let pieces = Pieces::parse("2025-01-03T17:13-05[-05]" )?; |
646 | /// assert_eq!(pieces.to_time_zone()?, Some(TimeZone::fixed(offset(-5)))); |
647 | /// |
648 | /// // A time zone annotation name gets you a IANA time zone: |
649 | /// let pieces = Pieces::parse("2025-01-03T17:13-05[America/New_York]" )?; |
650 | /// assert_eq!(pieces.to_time_zone()?, Some(TimeZone::get("America/New_York" )?)); |
651 | /// |
652 | /// // A time zone annotation name that doesn't exist gives you an error: |
653 | /// let pieces = Pieces::parse("2025-01-03T17:13-05[Australia/Bluey]" )?; |
654 | /// assert_eq!( |
655 | /// pieces.to_time_zone().unwrap_err().to_string(), |
656 | /// "failed to find time zone `Australia/Bluey` in time zone database" , |
657 | /// ); |
658 | /// |
659 | /// # Ok::<(), Box<dyn std::error::Error>>(()) |
660 | /// ``` |
661 | #[inline ] |
662 | pub fn to_time_zone(&self) -> Result<Option<TimeZone>, Error> { |
663 | self.to_time_zone_with(crate::tz::db()) |
664 | } |
665 | |
666 | /// A convenience routine for converting a time zone annotation, if |
667 | /// present, into a [`TimeZone`] using the given [`TimeZoneDatabase`]. |
668 | /// |
669 | /// If no annotation is on this `Pieces`, then this returns `Ok(None)`. |
670 | /// |
671 | /// This may return an error if the time zone annotation is a name and it |
672 | /// couldn't be found in Jiff's global time zone database. |
673 | /// |
674 | /// # Example |
675 | /// |
676 | /// ``` |
677 | /// use jiff::{fmt::temporal::Pieces, tz::TimeZone}; |
678 | /// |
679 | /// // A time zone annotation name gets you a IANA time zone: |
680 | /// let pieces = Pieces::parse("2025-01-03T17:13-05[America/New_York]" )?; |
681 | /// assert_eq!( |
682 | /// pieces.to_time_zone_with(jiff::tz::db())?, |
683 | /// Some(TimeZone::get("America/New_York" )?), |
684 | /// ); |
685 | /// |
686 | /// # Ok::<(), Box<dyn std::error::Error>>(()) |
687 | /// ``` |
688 | #[inline ] |
689 | pub fn to_time_zone_with( |
690 | &self, |
691 | db: &TimeZoneDatabase, |
692 | ) -> Result<Option<TimeZone>, Error> { |
693 | let Some(ann) = self.time_zone_annotation() else { return Ok(None) }; |
694 | ann.to_time_zone_with(db).map(Some) |
695 | } |
696 | |
697 | /// Set the date on this `Pieces` to the one given. |
698 | /// |
699 | /// A `Date` is the minimal piece of information necessary to create a |
700 | /// `Pieces`. This method will override any previous setting. |
701 | /// |
702 | /// # Example |
703 | /// |
704 | /// ``` |
705 | /// use jiff::{civil, fmt::temporal::Pieces, Timestamp}; |
706 | /// |
707 | /// let pieces = Pieces::from(civil::date(2025, 1, 3)); |
708 | /// assert_eq!(pieces.to_string(), "2025-01-03" ); |
709 | /// |
710 | /// // Alternatively, build a `Pieces` from another data type, and the |
711 | /// // date field will be automatically populated. |
712 | /// let pieces = Pieces::from(Timestamp::from_second(1735930208)?); |
713 | /// assert_eq!(pieces.date(), civil::date(2025, 1, 3)); |
714 | /// assert_eq!(pieces.to_string(), "2025-01-03T18:50:08Z" ); |
715 | /// |
716 | /// # Ok::<(), Box<dyn std::error::Error>>(()) |
717 | /// ``` |
718 | #[inline ] |
719 | pub fn with_date(self, date: Date) -> Pieces<'n> { |
720 | Pieces { date, ..self } |
721 | } |
722 | |
723 | /// Set the time on this `Pieces` to the one given. |
724 | /// |
725 | /// Setting a [`Time`] on `Pieces` is optional. When formatting a |
726 | /// `Pieces` to a string, a missing `Time` may be omitted from the datetime |
727 | /// string in some cases. See [`Pieces::with_offset`] for more details. |
728 | /// |
729 | /// # Example |
730 | /// |
731 | /// ``` |
732 | /// use jiff::{civil, fmt::temporal::Pieces}; |
733 | /// |
734 | /// let pieces = Pieces::from(civil::date(2025, 1, 3)) |
735 | /// .with_time(civil::time(13, 48, 0, 0)); |
736 | /// assert_eq!(pieces.to_string(), "2025-01-03T13:48:00" ); |
737 | /// // Alternatively, build a `Pieces` from a `DateTime` directly: |
738 | /// let pieces = Pieces::from(civil::date(2025, 1, 3).at(13, 48, 0, 0)); |
739 | /// assert_eq!(pieces.to_string(), "2025-01-03T13:48:00" ); |
740 | /// |
741 | /// # Ok::<(), Box<dyn std::error::Error>>(()) |
742 | /// ``` |
743 | #[inline ] |
744 | pub fn with_time(self, time: Time) -> Pieces<'n> { |
745 | Pieces { time: Some(time), ..self } |
746 | } |
747 | |
748 | /// Set the offset on this `Pieces` to the one given. |
749 | /// |
750 | /// Setting the offset on `Pieces` is optional. |
751 | /// |
752 | /// The type of offset is polymorphic, and includes anything that can be |
753 | /// infallibly converted into a [`PiecesOffset`]. This includes an |
754 | /// [`Offset`]. |
755 | /// |
756 | /// This refers to the offset in the [RFC 3339] component of a Temporal |
757 | /// ISO 8601 datetime string. |
758 | /// |
759 | /// Since a string like `2025-01-03+11` is not valid, if a `Pieces` has |
760 | /// an offset set but no [`Time`] set, then formatting the `Pieces` will |
761 | /// write an explicit `Time` set to midnight. |
762 | /// |
763 | /// Note that this is distinct from [`Pieces::with_time_zone_offset`]. |
764 | /// This routine sets the offset on the datetime, while |
765 | /// `Pieces::with_time_zone_offset` sets the offset inside the time zone |
766 | /// annotation. When the timestamp offset and the time zone annotation |
767 | /// offset are both present, then they must be equivalent or else the |
768 | /// datetime string is not a valid Temporal ISO 8601 string. However, a |
769 | /// `Pieces` will let you format a string with mismatching offsets. |
770 | /// |
771 | /// # Example |
772 | /// |
773 | /// This example shows how easily you can shoot yourself in the foot with |
774 | /// this routine: |
775 | /// |
776 | /// ``` |
777 | /// use jiff::{fmt::temporal::{Pieces, TimeZoneAnnotation}, tz}; |
778 | /// |
779 | /// let mut pieces = Pieces::parse("2025-01-03T07:55+02[+02]" )?; |
780 | /// pieces = pieces.with_offset(tz::offset(-10)); |
781 | /// // This is nonsense because the offsets don't match! |
782 | /// // And notice also that the instant that this timestamp refers to has |
783 | /// // changed. |
784 | /// assert_eq!(pieces.to_string(), "2025-01-03T07:55:00-10:00[+02:00]" ); |
785 | /// |
786 | /// # Ok::<(), Box<dyn std::error::Error>>(()) |
787 | /// ``` |
788 | /// |
789 | /// This exemplifies that `Pieces` is a mostly "dumb" type that passes |
790 | /// through the data it contains, even if it doesn't make sense. |
791 | /// |
792 | /// # Example: changing the offset can change the instant |
793 | /// |
794 | /// Consider this case where a `Pieces` is created directly from a |
795 | /// `Timestamp`, and then the offset is changed. |
796 | /// |
797 | /// ``` |
798 | /// use jiff::{fmt::temporal::Pieces, tz, Timestamp}; |
799 | /// |
800 | /// let pieces = Pieces::from(Timestamp::UNIX_EPOCH) |
801 | /// .with_offset(tz::offset(-5)); |
802 | /// assert_eq!(pieces.to_string(), "1970-01-01T00:00:00-05:00" ); |
803 | /// ``` |
804 | /// |
805 | /// You might do this naively as a way of printing the timestamp of the |
806 | /// Unix epoch with an offset of `-05` from UTC. But the above does not |
807 | /// correspond to the Unix epoch: |
808 | /// |
809 | /// ``` |
810 | /// use jiff::{Timestamp, ToSpan, Unit}; |
811 | /// |
812 | /// let ts: Timestamp = "1970-01-01T00:00:00-05:00" .parse()?; |
813 | /// assert_eq!( |
814 | /// ts.since((Unit::Hour, Timestamp::UNIX_EPOCH))?, |
815 | /// 5.hours().fieldwise(), |
816 | /// ); |
817 | /// |
818 | /// # Ok::<(), Box<dyn std::error::Error>>(()) |
819 | /// ``` |
820 | /// |
821 | /// This further exemplifies how `Pieces` is just a "dumb" type that |
822 | /// passes through the data it contains. |
823 | /// |
824 | /// This specific example is also why `Pieces` has a `From` trait |
825 | /// implementation for `(Timestamp, Offset)`, which correspond more to |
826 | /// what you want: |
827 | /// |
828 | /// ``` |
829 | /// use jiff::{fmt::temporal::Pieces, tz, Timestamp}; |
830 | /// |
831 | /// let pieces = Pieces::from((Timestamp::UNIX_EPOCH, tz::offset(-5))); |
832 | /// assert_eq!(pieces.to_string(), "1969-12-31T19:00:00-05:00" ); |
833 | /// ``` |
834 | /// |
835 | /// A decent mental model of `Pieces` is that setting fields on `Pieces` |
836 | /// can't change the values in memory of other fields. |
837 | /// |
838 | /// # Example: setting an offset forces a time to be written |
839 | /// |
840 | /// Consider these cases where formatting a `Pieces` won't write a |
841 | /// [`Time`]: |
842 | /// |
843 | /// ``` |
844 | /// use jiff::fmt::temporal::Pieces; |
845 | /// |
846 | /// let pieces = Pieces::from(jiff::civil::date(2025, 1, 3)); |
847 | /// assert_eq!(pieces.to_string(), "2025-01-03" ); |
848 | /// |
849 | /// let pieces = Pieces::from(jiff::civil::date(2025, 1, 3)) |
850 | /// .with_time_zone_name("Africa/Cairo" ); |
851 | /// assert_eq!(pieces.to_string(), "2025-01-03[Africa/Cairo]" ); |
852 | /// ``` |
853 | /// |
854 | /// This works because the resulting strings are valid. In particular, when |
855 | /// one parses a `2025-01-03[Africa/Cairo]` into a `Zoned`, it results in a |
856 | /// time component of midnight automatically (or more precisely, the first |
857 | /// instead of the corresponding day): |
858 | /// |
859 | /// ``` |
860 | /// use jiff::{civil::Time, Zoned}; |
861 | /// |
862 | /// let zdt: Zoned = "2025-01-03[Africa/Cairo]" .parse()?; |
863 | /// assert_eq!(zdt.time(), Time::midnight()); |
864 | /// |
865 | /// // tricksy tricksy, the first instant of 2015-10-18 in Sao Paulo is |
866 | /// // not midnight! |
867 | /// let zdt: Zoned = "2015-10-18[America/Sao_Paulo]" .parse()?; |
868 | /// assert_eq!(zdt.time(), jiff::civil::time(1, 0, 0, 0)); |
869 | /// // This happens because midnight didn't appear on the clocks in |
870 | /// // Sao Paulo on 2015-10-18. So if you try to parse a datetime with |
871 | /// // midnight, automatic disambiguation kicks in and chooses the time |
872 | /// // after the gap automatically: |
873 | /// let zdt: Zoned = "2015-10-18T00:00:00[America/Sao_Paulo]" .parse()?; |
874 | /// assert_eq!(zdt.time(), jiff::civil::time(1, 0, 0, 0)); |
875 | /// |
876 | /// # Ok::<(), Box<dyn std::error::Error>>(()) |
877 | /// ``` |
878 | /// |
879 | /// However, if you have a date and an offset, then since things like |
880 | /// `2025-01-03+10` aren't valid Temporal ISO 8601 datetime strings, the |
881 | /// default midnight time is automatically written: |
882 | /// |
883 | /// ``` |
884 | /// use jiff::{fmt::temporal::Pieces, tz}; |
885 | /// |
886 | /// let pieces = Pieces::from(jiff::civil::date(2025, 1, 3)) |
887 | /// .with_offset(tz::offset(-5)); |
888 | /// assert_eq!(pieces.to_string(), "2025-01-03T00:00:00-05:00" ); |
889 | /// |
890 | /// let pieces = Pieces::from(jiff::civil::date(2025, 1, 3)) |
891 | /// .with_offset(tz::offset(2)) |
892 | /// .with_time_zone_name("Africa/Cairo" ); |
893 | /// assert_eq!(pieces.to_string(), "2025-01-03T00:00:00+02:00[Africa/Cairo]" ); |
894 | /// ``` |
895 | /// |
896 | /// # Example: formatting a Zulu or `-00:00` offset |
897 | /// |
898 | /// A [`PiecesOffset`] encapsulates not just a numeric offset, but also |
899 | /// whether a `Z` or a signed zero are used. While it's uncommon to need |
900 | /// this, this permits one to format a `Pieces` using either of these |
901 | /// constructs: |
902 | /// |
903 | /// ``` |
904 | /// use jiff::{ |
905 | /// civil, |
906 | /// fmt::temporal::{Pieces, PiecesNumericOffset, PiecesOffset}, |
907 | /// tz::Offset, |
908 | /// }; |
909 | /// |
910 | /// let pieces = Pieces::from(civil::date(1970, 1, 1).at(0, 0, 0, 0)) |
911 | /// .with_offset(Offset::UTC); |
912 | /// assert_eq!(pieces.to_string(), "1970-01-01T00:00:00+00:00" ); |
913 | /// |
914 | /// let pieces = Pieces::from(civil::date(1970, 1, 1).at(0, 0, 0, 0)) |
915 | /// .with_offset(PiecesOffset::Zulu); |
916 | /// assert_eq!(pieces.to_string(), "1970-01-01T00:00:00Z" ); |
917 | /// |
918 | /// let pieces = Pieces::from(civil::date(1970, 1, 1).at(0, 0, 0, 0)) |
919 | /// .with_offset(PiecesNumericOffset::from(Offset::UTC).with_negative_zero()); |
920 | /// assert_eq!(pieces.to_string(), "1970-01-01T00:00:00-00:00" ); |
921 | /// ``` |
922 | /// |
923 | /// [RFC 3339]: https://www.rfc-editor.org/rfc/rfc3339 |
924 | #[inline ] |
925 | pub fn with_offset<T: Into<PiecesOffset>>(self, offset: T) -> Pieces<'n> { |
926 | Pieces { offset: Some(offset.into()), ..self } |
927 | } |
928 | |
929 | /// Sets the time zone annotation on this `Pieces` to the given time zone |
930 | /// name. |
931 | /// |
932 | /// Setting a time zone annotation on `Pieces` is optional. |
933 | /// |
934 | /// This is a convenience routine for using |
935 | /// [`Pieces::with_time_zone_annotation`] with an explicitly constructed |
936 | /// [`TimeZoneAnnotation`] for a time zone name. |
937 | /// |
938 | /// # Example |
939 | /// |
940 | /// This example shows how easily you can shoot yourself in the foot with |
941 | /// this routine: |
942 | /// |
943 | /// ``` |
944 | /// use jiff::fmt::temporal::{Pieces, TimeZoneAnnotation}; |
945 | /// |
946 | /// let mut pieces = Pieces::parse("2025-01-03T07:55+02[Africa/Cairo]" )?; |
947 | /// pieces = pieces.with_time_zone_name("Australia/Bluey" ); |
948 | /// // This is nonsense because `Australia/Bluey` isn't a valid time zone! |
949 | /// assert_eq!(pieces.to_string(), "2025-01-03T07:55:00+02:00[Australia/Bluey]" ); |
950 | /// |
951 | /// # Ok::<(), Box<dyn std::error::Error>>(()) |
952 | /// ``` |
953 | /// |
954 | /// This exemplifies that `Pieces` is a mostly "dumb" type that passes |
955 | /// through the data it contains, even if it doesn't make sense. |
956 | #[inline ] |
957 | pub fn with_time_zone_name<'a>(self, name: &'a str) -> Pieces<'a> { |
958 | self.with_time_zone_annotation(TimeZoneAnnotation::from(name)) |
959 | } |
960 | |
961 | /// Sets the time zone annotation on this `Pieces` to the given offset. |
962 | /// |
963 | /// Setting a time zone annotation on `Pieces` is optional. |
964 | /// |
965 | /// This is a convenience routine for using |
966 | /// [`Pieces::with_time_zone_annotation`] with an explicitly constructed |
967 | /// [`TimeZoneAnnotation`] for a time zone offset. |
968 | /// |
969 | /// Note that this is distinct from [`Pieces::with_offset`]. This |
970 | /// routine sets the offset inside the time zone annotation, while |
971 | /// `Pieces::with_offset` sets the offset on the timestamp itself. When the |
972 | /// timestamp offset and the time zone annotation offset are both present, |
973 | /// then they must be equivalent or else the datetime string is not a valid |
974 | /// Temporal ISO 8601 string. However, a `Pieces` will let you format a |
975 | /// string with mismatching offsets. |
976 | /// |
977 | /// # Example |
978 | /// |
979 | /// This example shows how easily you can shoot yourself in the foot with |
980 | /// this routine: |
981 | /// |
982 | /// ``` |
983 | /// use jiff::{fmt::temporal::{Pieces, TimeZoneAnnotation}, tz}; |
984 | /// |
985 | /// let mut pieces = Pieces::parse("2025-01-03T07:55+02[Africa/Cairo]" )?; |
986 | /// pieces = pieces.with_time_zone_offset(tz::offset(-7)); |
987 | /// // This is nonsense because the offset `+02` does not match `-07`. |
988 | /// assert_eq!(pieces.to_string(), "2025-01-03T07:55:00+02:00[-07:00]" ); |
989 | /// |
990 | /// # Ok::<(), Box<dyn std::error::Error>>(()) |
991 | /// ``` |
992 | /// |
993 | /// This exemplifies that `Pieces` is a mostly "dumb" type that passes |
994 | /// through the data it contains, even if it doesn't make sense. |
995 | #[inline ] |
996 | pub fn with_time_zone_offset(self, offset: Offset) -> Pieces<'static> { |
997 | self.with_time_zone_annotation(TimeZoneAnnotation::from(offset)) |
998 | } |
999 | |
1000 | /// Returns a new `Pieces` with the given time zone annotation. |
1001 | /// |
1002 | /// Setting a time zone annotation on `Pieces` is optional. |
1003 | /// |
1004 | /// You may find it more convenient to use |
1005 | /// [`Pieces::with_time_zone_name`] or [`Pieces::with_time_zone_offset`]. |
1006 | /// |
1007 | /// # Example |
1008 | /// |
1009 | /// This example shows how easily you can shoot yourself in the foot with |
1010 | /// this routine: |
1011 | /// |
1012 | /// ``` |
1013 | /// use jiff::fmt::temporal::{Pieces, TimeZoneAnnotation}; |
1014 | /// |
1015 | /// let mut pieces = Pieces::parse("2025-01-03T07:55+02[Africa/Cairo]" )?; |
1016 | /// pieces = pieces.with_time_zone_annotation( |
1017 | /// TimeZoneAnnotation::from("Canada/Yukon" ), |
1018 | /// ); |
1019 | /// // This is nonsense because the offset `+02` is never valid for the |
1020 | /// // `Canada/Yukon` time zone. |
1021 | /// assert_eq!(pieces.to_string(), "2025-01-03T07:55:00+02:00[Canada/Yukon]" ); |
1022 | /// |
1023 | /// # Ok::<(), Box<dyn std::error::Error>>(()) |
1024 | /// ``` |
1025 | /// |
1026 | /// This exemplifies that `Pieces` is a mostly "dumb" type that passes |
1027 | /// through the data it contains, even if it doesn't make sense. |
1028 | #[inline ] |
1029 | pub fn with_time_zone_annotation<'a>( |
1030 | self, |
1031 | ann: TimeZoneAnnotation<'a>, |
1032 | ) -> Pieces<'a> { |
1033 | Pieces { time_zone_annotation: Some(ann), ..self } |
1034 | } |
1035 | |
1036 | /// Converts this `Pieces` into an "owned" value whose lifetime is |
1037 | /// `'static`. |
1038 | /// |
1039 | /// Ths "owned" value in this context refers to the time zone annotation |
1040 | /// name, if present. For example, `Canada/Yukon` in |
1041 | /// `2025-01-03T07:55-07[Canada/Yukon]`. When parsing into a `Pieces`, |
1042 | /// the time zone annotation name is borrowed. But callers may find it more |
1043 | /// convenient to work with an owned value. By calling this method, the |
1044 | /// borrowed string internally will be copied into a new string heap |
1045 | /// allocation. |
1046 | /// |
1047 | /// If `Pieces` doesn't have a time zone annotation, is already owned or |
1048 | /// the time zone annotation is an offset, then this is a no-op. |
1049 | #[cfg (feature = "alloc" )] |
1050 | #[inline ] |
1051 | pub fn into_owned(self) -> Pieces<'static> { |
1052 | Pieces { |
1053 | date: self.date, |
1054 | time: self.time, |
1055 | offset: self.offset, |
1056 | time_zone_annotation: self |
1057 | .time_zone_annotation |
1058 | .map(|ann| ann.into_owned()), |
1059 | } |
1060 | } |
1061 | } |
1062 | |
1063 | impl From<Date> for Pieces<'static> { |
1064 | #[inline ] |
1065 | fn from(date: Date) -> Pieces<'static> { |
1066 | Pieces { date, time: None, offset: None, time_zone_annotation: None } |
1067 | } |
1068 | } |
1069 | |
1070 | impl From<DateTime> for Pieces<'static> { |
1071 | #[inline ] |
1072 | fn from(dt: DateTime) -> Pieces<'static> { |
1073 | Pieces::from(dt.date()).with_time(dt.time()) |
1074 | } |
1075 | } |
1076 | |
1077 | impl From<Timestamp> for Pieces<'static> { |
1078 | #[inline ] |
1079 | fn from(ts: Timestamp) -> Pieces<'static> { |
1080 | let dt: DateTime = Offset::UTC.to_datetime(timestamp:ts); |
1081 | Pieces::from(dt).with_offset(PiecesOffset::Zulu) |
1082 | } |
1083 | } |
1084 | |
1085 | impl From<(Timestamp, Offset)> for Pieces<'static> { |
1086 | #[inline ] |
1087 | fn from((ts: Timestamp, offset: Offset): (Timestamp, Offset)) -> Pieces<'static> { |
1088 | Pieces::from(offset.to_datetime(timestamp:ts)).with_offset(offset) |
1089 | } |
1090 | } |
1091 | |
1092 | impl<'a> From<&'a Zoned> for Pieces<'a> { |
1093 | #[inline ] |
1094 | fn from(zdt: &'a Zoned) -> Pieces<'a> { |
1095 | let mut pieces: Pieces<'_> = |
1096 | Pieces::from(zdt.datetime()).with_offset(zdt.offset()); |
1097 | if let Some(name: &str) = zdt.time_zone().iana_name() { |
1098 | pieces = pieces.with_time_zone_name(name); |
1099 | } else { |
1100 | pieces = pieces.with_time_zone_offset(zdt.offset()); |
1101 | } |
1102 | pieces |
1103 | } |
1104 | } |
1105 | |
1106 | impl<'n> core::fmt::Display for Pieces<'n> { |
1107 | fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { |
1108 | use crate::fmt::StdFmtWrite; |
1109 | |
1110 | let precision: Option = |
1111 | f.precision().map(|p: usize| u8::try_from(p).unwrap_or(default:u8::MAX)); |
1112 | super::DateTimePrinter::new() |
1113 | .precision(precision) |
1114 | .print_pieces(self, StdFmtWrite(f)) |
1115 | .map_err(|_| core::fmt::Error) |
1116 | } |
1117 | } |
1118 | |
1119 | /// An offset parsed from a Temporal ISO 8601 datetime string, for use with |
1120 | /// [`Pieces`]. |
1121 | /// |
1122 | /// One can almost think of this as effectively equivalent to an `Offset`. And |
1123 | /// indeed, all `PiecesOffset` values can be convert to an `Offset`. However, |
1124 | /// some offsets in a datetime string have a different connotation that can't |
1125 | /// be captured by an `Offset`. |
1126 | /// |
1127 | /// For example, the offsets `Z`, `-00:00` and `+00:00` all map to |
1128 | /// [`Offset::UTC`] after parsing. However, `Z` and `-00:00` generally |
1129 | /// indicate that the offset from local time is unknown, where as `+00:00` |
1130 | /// indicates that the offset from local is _known_ and is zero. This type |
1131 | /// permits callers to inspect what offset was actually written. |
1132 | /// |
1133 | /// # Example |
1134 | /// |
1135 | /// This example shows how one can create Temporal ISO 8601 datetime strings |
1136 | /// with `+00:00`, `-00:00` or `Z` offsets. |
1137 | /// |
1138 | /// ``` |
1139 | /// use jiff::{ |
1140 | /// fmt::temporal::{Pieces, PiecesNumericOffset}, |
1141 | /// tz::Offset, |
1142 | /// Timestamp, |
1143 | /// }; |
1144 | /// |
1145 | /// // If you create a `Pieces` from a `Timestamp` with a UTC offset, |
1146 | /// // then this is interpreted as "the offset from UTC is known and is |
1147 | /// // zero." |
1148 | /// let pieces = Pieces::from((Timestamp::UNIX_EPOCH, Offset::UTC)); |
1149 | /// assert_eq!(pieces.to_string(), "1970-01-01T00:00:00+00:00" ); |
1150 | /// |
1151 | /// // Otherwise, if you create a `Pieces` from just a `Timestamp` with |
1152 | /// // no offset, then it is interpreted as "the offset from UTC is not |
1153 | /// // known." Typically, this is rendered with `Z` for "Zulu": |
1154 | /// let pieces = Pieces::from(Timestamp::UNIX_EPOCH); |
1155 | /// assert_eq!(pieces.to_string(), "1970-01-01T00:00:00Z" ); |
1156 | /// |
1157 | /// // But it might be the case that you want to use `-00:00` instead, |
1158 | /// // perhaps to conform to some existing convention or legacy |
1159 | /// // applications that require it: |
1160 | /// let pieces = Pieces::from(Timestamp::UNIX_EPOCH) |
1161 | /// .with_offset( |
1162 | /// PiecesNumericOffset::from(Offset::UTC).with_negative_zero(), |
1163 | /// ); |
1164 | /// assert_eq!(pieces.to_string(), "1970-01-01T00:00:00-00:00" ); |
1165 | /// ``` |
1166 | /// |
1167 | /// Without `Pieces`, it's not otherwise possible to emit a `-00:00` offset. |
1168 | /// For example, |
1169 | /// [`DateTimePrinter::print_timestamp`](crate::fmt::temporal::DateTimePrinter::print_timestamp) |
1170 | /// will always emit `Z`, which is consider semantically identical to `-00:00` |
1171 | /// by [RFC 9557]. There's no specific use case where it's expected that you |
1172 | /// should need to write `-00:00` instead of `Z`, but it's conceivable legacy |
1173 | /// or otherwise inflexible applications might want it. Or perhaps, in some |
1174 | /// systems, there is a distinction to draw between `Z` and `-00:00`. |
1175 | /// |
1176 | /// [RFC 9557]: https://www.rfc-editor.org/rfc/rfc9557.html |
1177 | #[derive (Clone, Copy, Debug, Eq, Hash, PartialEq)] |
1178 | #[non_exhaustive ] |
1179 | pub enum PiecesOffset { |
1180 | /// The "Zulu" offset, corresponding to UTC in a context where the offset |
1181 | /// for civil time is unknown or unavailable. |
1182 | /// |
1183 | /// [RFC 9557] defines this as equivalent in semantic meaning to `-00:00`: |
1184 | /// |
1185 | /// > If the time in UTC is known, but the offset to local time is unknown, |
1186 | /// > this can be represented with an offset of `Z`. (The original version |
1187 | /// > of this specification provided `-00:00` for this purpose, which is |
1188 | /// > not allowed by ISO-8601:2000 and therefore is less interoperable; |
1189 | /// > Section 3.3 of RFC 5322 describes a related convention for email, |
1190 | /// > which does not have this problem). This differs semantically from an |
1191 | /// > offset of `+00:00`, which implies that UTC is the preferred reference |
1192 | /// > point for the specified time. |
1193 | /// |
1194 | /// [RFC 9557]: https://www.rfc-editor.org/rfc/rfc9557 |
1195 | Zulu, |
1196 | /// A specific numeric offset, including whether the parsed sign is |
1197 | /// negative. |
1198 | /// |
1199 | /// The sign is usually redundant, since an `Offset` is itself signed. But |
1200 | /// it can be used to distinguish between `+00:00` (`+00` is the preferred |
1201 | /// offset) and `-00:00` (`+00` is what should be used, but only because |
1202 | /// the offset to local time is not known). Generally speaking, one should |
1203 | /// regard `-00:00` as equivalent to `Z`, per RFC 9557. |
1204 | Numeric(PiecesNumericOffset), |
1205 | } |
1206 | |
1207 | impl PiecesOffset { |
1208 | /// Converts this offset to a concrete numeric offset in all cases. |
1209 | /// |
1210 | /// If this was a `Z` or a `-00:00` offset, then `Offset::UTC` is returned. |
1211 | /// In all other cases, the underlying numeric offset is returned as-is. |
1212 | /// |
1213 | /// # Example |
1214 | /// |
1215 | /// ``` |
1216 | /// use jiff::{ |
1217 | /// fmt::temporal::{Pieces, PiecesNumericOffset, PiecesOffset}, |
1218 | /// tz::Offset, |
1219 | /// }; |
1220 | /// |
1221 | /// let pieces = Pieces::parse("1970-01-01T00:00:00Z" )?; |
1222 | /// let off = pieces.offset().unwrap(); |
1223 | /// // Parsed as Zulu. |
1224 | /// assert_eq!(off, PiecesOffset::Zulu); |
1225 | /// // Gets converted from Zulu to UTC, i.e., just zero. |
1226 | /// assert_eq!(off.to_numeric_offset(), Offset::UTC); |
1227 | /// |
1228 | /// let pieces = Pieces::parse("1970-01-01T00:00:00-00:00" )?; |
1229 | /// let off = pieces.offset().unwrap(); |
1230 | /// // Parsed as a negative zero. |
1231 | /// assert_eq!(off, PiecesOffset::from( |
1232 | /// PiecesNumericOffset::from(Offset::UTC).with_negative_zero(), |
1233 | /// )); |
1234 | /// // Gets converted from -00:00 to UTC, i.e., just zero. |
1235 | /// assert_eq!(off.to_numeric_offset(), Offset::UTC); |
1236 | /// |
1237 | /// let pieces = Pieces::parse("1970-01-01T00:00:00+00:00" )?; |
1238 | /// let off = pieces.offset().unwrap(); |
1239 | /// // Parsed as a positive zero. |
1240 | /// assert_eq!(off, PiecesOffset::from( |
1241 | /// PiecesNumericOffset::from(Offset::UTC), |
1242 | /// )); |
1243 | /// // Gets converted from -00:00 to UTC, i.e., just zero. |
1244 | /// assert_eq!(off.to_numeric_offset(), Offset::UTC); |
1245 | /// |
1246 | /// # Ok::<(), Box<dyn std::error::Error>>(()) |
1247 | /// ``` |
1248 | #[inline ] |
1249 | pub fn to_numeric_offset(&self) -> Offset { |
1250 | match *self { |
1251 | PiecesOffset::Zulu => Offset::UTC, |
1252 | // -00:00 and +00:00 both collapse to zero here. |
1253 | PiecesOffset::Numeric(ref noffset) => noffset.offset(), |
1254 | } |
1255 | } |
1256 | } |
1257 | |
1258 | impl From<Offset> for PiecesOffset { |
1259 | #[inline ] |
1260 | fn from(offset: Offset) -> PiecesOffset { |
1261 | PiecesOffset::from(PiecesNumericOffset::from(offset)) |
1262 | } |
1263 | } |
1264 | |
1265 | impl From<PiecesNumericOffset> for PiecesOffset { |
1266 | #[inline ] |
1267 | fn from(offset: PiecesNumericOffset) -> PiecesOffset { |
1268 | PiecesOffset::Numeric(offset) |
1269 | } |
1270 | } |
1271 | |
1272 | /// A specific numeric offset, including the sign of the offset, for use with |
1273 | /// [`Pieces`]. |
1274 | /// |
1275 | /// # Signedness |
1276 | /// |
1277 | /// The sign attached to this type is usually redundant, since the underlying |
1278 | /// [`Offset`] is itself signed. But it can be used to distinguish between |
1279 | /// `+00:00` (`+00` is the preferred offset) and `-00:00` (`+00` is what should |
1280 | /// be used, but only because the offset to local time is not known). Generally |
1281 | /// speaking, one should regard `-00:00` as equivalent to `Z`, per [RFC 9557]. |
1282 | /// |
1283 | /// [RFC 9557]: https://www.rfc-editor.org/rfc/rfc9557 |
1284 | #[derive (Clone, Copy, Debug, Eq, Hash, PartialEq)] |
1285 | pub struct PiecesNumericOffset { |
1286 | offset: Offset, |
1287 | is_negative: bool, |
1288 | } |
1289 | |
1290 | impl PiecesNumericOffset { |
1291 | /// Returns the numeric offset. |
1292 | /// |
1293 | /// # Example |
1294 | /// |
1295 | /// ``` |
1296 | /// use jiff::{ |
1297 | /// fmt::temporal::{Pieces, PiecesOffset}, |
1298 | /// tz::Offset, |
1299 | /// }; |
1300 | /// |
1301 | /// let pieces = Pieces::parse("1970-01-01T00:00:00-05:30" )?; |
1302 | /// let off = match pieces.offset().unwrap() { |
1303 | /// PiecesOffset::Numeric(off) => off, |
1304 | /// _ => unreachable!(), |
1305 | /// }; |
1306 | /// // This is really only useful if you care that an actual |
1307 | /// // numeric offset was written and not, e.g., `Z`. Otherwise, |
1308 | /// // you could just use `PiecesOffset::to_numeric_offset`. |
1309 | /// assert_eq!( |
1310 | /// off.offset(), |
1311 | /// Offset::from_seconds(-5 * 60 * 60 - 30 * 60).unwrap(), |
1312 | /// ); |
1313 | /// |
1314 | /// # Ok::<(), Box<dyn std::error::Error>>(()) |
1315 | /// ``` |
1316 | #[inline ] |
1317 | pub fn offset(&self) -> Offset { |
1318 | self.offset |
1319 | } |
1320 | |
1321 | /// Returns whether the sign of the offset is negative or not. |
1322 | /// |
1323 | /// When formatting a [`Pieces`] to a string, this is _only_ used to |
1324 | /// determine the rendered sign when the [`Offset`] is itself zero. In |
1325 | /// all other cases, the sign rendered matches the sign of the `Offset`. |
1326 | /// |
1327 | /// Since `Offset` does not keep track of a sign when its value is zero, |
1328 | /// when using the `From<Offset>` trait implementation for this type, |
1329 | /// `is_negative` is always set to `false` when the offset is zero. |
1330 | /// |
1331 | /// # Example |
1332 | /// |
1333 | /// ``` |
1334 | /// use jiff::{ |
1335 | /// fmt::temporal::{Pieces, PiecesOffset}, |
1336 | /// tz::Offset, |
1337 | /// }; |
1338 | /// |
1339 | /// let pieces = Pieces::parse("1970-01-01T00:00:00-00:00" )?; |
1340 | /// let off = match pieces.offset().unwrap() { |
1341 | /// PiecesOffset::Numeric(off) => off, |
1342 | /// _ => unreachable!(), |
1343 | /// }; |
1344 | /// // The numeric offset component in this case is |
1345 | /// // indistiguisable from `Offset::UTC`. This is |
1346 | /// // because an `Offset` does not use different |
1347 | /// // representations for negative and positive zero. |
1348 | /// assert_eq!(off.offset(), Offset::UTC); |
1349 | /// // This is where `is_negative` comes in handy: |
1350 | /// assert_eq!(off.is_negative(), true); |
1351 | /// |
1352 | /// # Ok::<(), Box<dyn std::error::Error>>(()) |
1353 | /// ``` |
1354 | #[inline ] |
1355 | pub fn is_negative(&self) -> bool { |
1356 | self.is_negative |
1357 | } |
1358 | |
1359 | /// Sets this numeric offset to use `-00:00` if and only if the offset |
1360 | /// is zero. |
1361 | /// |
1362 | /// # Example |
1363 | /// |
1364 | /// ``` |
1365 | /// use jiff::{ |
1366 | /// fmt::temporal::{Pieces, PiecesNumericOffset}, |
1367 | /// tz::Offset, |
1368 | /// Timestamp, |
1369 | /// }; |
1370 | /// |
1371 | /// // If you create a `Pieces` from a `Timestamp` with a UTC offset, |
1372 | /// // then this is interpreted as "the offset from UTC is known and is |
1373 | /// // zero." |
1374 | /// let pieces = Pieces::from((Timestamp::UNIX_EPOCH, Offset::UTC)); |
1375 | /// assert_eq!(pieces.to_string(), "1970-01-01T00:00:00+00:00" ); |
1376 | /// |
1377 | /// // Otherwise, if you create a `Pieces` from just a `Timestamp` with |
1378 | /// // no offset, then it is interpreted as "the offset from UTC is not |
1379 | /// // known." Typically, this is rendered with `Z` for "Zulu": |
1380 | /// let pieces = Pieces::from(Timestamp::UNIX_EPOCH); |
1381 | /// assert_eq!(pieces.to_string(), "1970-01-01T00:00:00Z" ); |
1382 | /// |
1383 | /// // But it might be the case that you want to use `-00:00` instead, |
1384 | /// // perhaps to conform to some existing convention or legacy |
1385 | /// // applications that require it: |
1386 | /// let pieces = Pieces::from(Timestamp::UNIX_EPOCH) |
1387 | /// .with_offset( |
1388 | /// PiecesNumericOffset::from(Offset::UTC).with_negative_zero(), |
1389 | /// ); |
1390 | /// assert_eq!(pieces.to_string(), "1970-01-01T00:00:00-00:00" ); |
1391 | /// ``` |
1392 | #[inline ] |
1393 | pub fn with_negative_zero(self) -> PiecesNumericOffset { |
1394 | PiecesNumericOffset { is_negative: true, ..self } |
1395 | } |
1396 | } |
1397 | |
1398 | impl From<Offset> for PiecesNumericOffset { |
1399 | #[inline ] |
1400 | fn from(offset: Offset) -> PiecesNumericOffset { |
1401 | // This can of course never return a -00:00 offset, only +00:00. |
1402 | PiecesNumericOffset { offset, is_negative: offset.is_negative() } |
1403 | } |
1404 | } |
1405 | |
1406 | /// An [RFC 9557] time zone annotation, for use with [`Pieces`]. |
1407 | /// |
1408 | /// A time zone annotation is either a time zone name (typically an IANA time |
1409 | /// zone identifier) like `America/New_York`, or an offset like `-05:00`. This |
1410 | /// is normally an implementation detail of parsing into a [`Zoned`], but the |
1411 | /// raw annotation can be accessed via [`Pieces::time_zone_annotation`] after |
1412 | /// parsing into a [`Pieces`]. |
1413 | /// |
1414 | /// The lifetime parameter refers to the lifetime of the time zone |
1415 | /// name. The lifetime is static when the time zone annotation is |
1416 | /// offset or if the name is owned. An owned value can be produced via |
1417 | /// [`TimeZoneAnnotation::into_owned`] when the `alloc` crate feature is |
1418 | /// enabled. |
1419 | /// |
1420 | /// # Construction |
1421 | /// |
1422 | /// If you're using [`Pieces`], then its [`Pieces::with_time_zone_name`] and |
1423 | /// [`Pieces::with_time_zone_offset`] methods should absolve you of needing to |
1424 | /// build values of this type explicitly. But if the need arises, there are |
1425 | /// `From` impls for `&str` (time zone annotation name) and [`Offset`] (time |
1426 | /// zone annotation offset) for this type. |
1427 | /// |
1428 | /// # Example |
1429 | /// |
1430 | /// ``` |
1431 | /// use jiff::{fmt::temporal::{Pieces, TimeZoneAnnotation}, tz::offset}; |
1432 | /// |
1433 | /// // A time zone annotation from a name: |
1434 | /// let pieces = Pieces::parse("2025-01-02T16:47-05[America/New_York]" )?; |
1435 | /// assert_eq!( |
1436 | /// pieces.time_zone_annotation().unwrap(), |
1437 | /// &TimeZoneAnnotation::from("America/New_York" ), |
1438 | /// ); |
1439 | /// |
1440 | /// // A time zone annotation from an offset: |
1441 | /// let pieces = Pieces::parse("2025-01-02T16:47-05[-05:00]" )?; |
1442 | /// assert_eq!( |
1443 | /// pieces.time_zone_annotation().unwrap(), |
1444 | /// &TimeZoneAnnotation::from(offset(-5)), |
1445 | /// ); |
1446 | /// |
1447 | /// # Ok::<(), Box<dyn std::error::Error>>(()) |
1448 | /// ``` |
1449 | /// |
1450 | /// [RFC 9557]: https://www.rfc-editor.org/rfc/rfc9557.html |
1451 | #[derive (Clone, Debug, Eq, Hash, PartialEq)] |
1452 | pub struct TimeZoneAnnotation<'n> { |
1453 | pub(crate) kind: TimeZoneAnnotationKind<'n>, |
1454 | /// Whether the annotation is marked as "critical," i.e., with a |
1455 | /// `!` prefix. When enabled, it's supposed to make the annotation |
1456 | /// un-ignorable. |
1457 | /// |
1458 | /// This is basically unused. And there's no way for callers to flip this |
1459 | /// switch currently. But it can be queried after parsing. Jiff also |
1460 | /// doesn't alter its behavior based on this flag. In particular, Jiff |
1461 | /// basically always behaves as if `critical` is true. |
1462 | pub(crate) critical: bool, |
1463 | } |
1464 | |
1465 | impl<'n> TimeZoneAnnotation<'n> { |
1466 | /// Returns the "kind" of this annotation. The kind is either a name or an |
1467 | /// offset. |
1468 | /// |
1469 | /// # Example |
1470 | /// |
1471 | /// ``` |
1472 | /// use jiff::fmt::temporal::{Pieces, TimeZoneAnnotation}; |
1473 | /// |
1474 | /// // A time zone annotation from a name, which doesn't necessarily have |
1475 | /// // to point to a valid IANA time zone. |
1476 | /// let pieces = Pieces::parse("2025-01-02T16:47-05[Australia/Bluey]" )?; |
1477 | /// assert_eq!( |
1478 | /// pieces.time_zone_annotation().unwrap(), |
1479 | /// &TimeZoneAnnotation::from("Australia/Bluey" ), |
1480 | /// ); |
1481 | /// |
1482 | /// # Ok::<(), Box<dyn std::error::Error>>(()) |
1483 | /// ``` |
1484 | #[inline ] |
1485 | pub fn kind(&self) -> &TimeZoneAnnotationKind<'n> { |
1486 | &self.kind |
1487 | } |
1488 | |
1489 | /// Returns true when this time zone is marked as "critical." This occurs |
1490 | /// when the time zone annotation is preceded by a `!`. It is meant to |
1491 | /// signify that, basically, implementations should error if the annotation |
1492 | /// is invalid in some way. And when it's absent, it's left up to the |
1493 | /// implementation's discretion about what to do (including silently |
1494 | /// ignoring the invalid annotation). |
1495 | /// |
1496 | /// Generally speaking, Jiff ignores this altogether for time zone |
1497 | /// annotations and behaves as if it's always true. But it's exposed here |
1498 | /// for callers to query in case it's useful. |
1499 | /// |
1500 | /// # Example |
1501 | /// |
1502 | /// ``` |
1503 | /// use jiff::fmt::temporal::{Pieces, TimeZoneAnnotation}; |
1504 | /// |
1505 | /// // not critical |
1506 | /// let pieces = Pieces::parse("2025-01-02T16:47-05[Australia/Bluey]" )?; |
1507 | /// assert_eq!( |
1508 | /// Some(false), |
1509 | /// pieces.time_zone_annotation().map(|a| a.is_critical()), |
1510 | /// ); |
1511 | /// |
1512 | /// // critical |
1513 | /// let pieces = Pieces::parse("2025-01-02T16:47-05[!Australia/Bluey]" )?; |
1514 | /// assert_eq!( |
1515 | /// Some(true), |
1516 | /// pieces.time_zone_annotation().map(|a| a.is_critical()), |
1517 | /// ); |
1518 | /// |
1519 | /// # Ok::<(), Box<dyn std::error::Error>>(()) |
1520 | /// ``` |
1521 | #[inline ] |
1522 | pub fn is_critical(&self) -> bool { |
1523 | self.critical |
1524 | } |
1525 | |
1526 | /// A convenience routine for converting this annotation into a time zone. |
1527 | /// |
1528 | /// This can fail if the annotation contains a name that couldn't be found |
1529 | /// in the global time zone database. If you need to use something other |
1530 | /// than the global time zone database, then use |
1531 | /// [`TimeZoneAnnotation::to_time_zone_with`]. |
1532 | /// |
1533 | /// Note that it may be more convenient to use |
1534 | /// [`Pieces::to_time_zone`]. |
1535 | /// |
1536 | /// # Example |
1537 | /// |
1538 | /// ``` |
1539 | /// use jiff::{fmt::temporal::Pieces, tz::TimeZone}; |
1540 | /// |
1541 | /// let pieces = Pieces::parse("2025-01-02T16:47-05[Australia/Tasmania]" )?; |
1542 | /// let ann = pieces.time_zone_annotation().unwrap(); |
1543 | /// assert_eq!( |
1544 | /// ann.to_time_zone().unwrap(), |
1545 | /// TimeZone::get("Australia/Tasmania" ).unwrap(), |
1546 | /// ); |
1547 | /// |
1548 | /// let pieces = Pieces::parse("2025-01-02T16:47-05[Australia/Bluey]" )?; |
1549 | /// let ann = pieces.time_zone_annotation().unwrap(); |
1550 | /// assert_eq!( |
1551 | /// ann.to_time_zone().unwrap_err().to_string(), |
1552 | /// "failed to find time zone `Australia/Bluey` in time zone database" , |
1553 | /// ); |
1554 | /// |
1555 | /// # Ok::<(), Box<dyn std::error::Error>>(()) |
1556 | /// ``` |
1557 | #[inline ] |
1558 | pub fn to_time_zone(&self) -> Result<TimeZone, Error> { |
1559 | self.to_time_zone_with(crate::tz::db()) |
1560 | } |
1561 | |
1562 | /// This is like [`TimeZoneAnnotation::to_time_zone`], but permits the |
1563 | /// caller to pass in their own time zone database. |
1564 | /// |
1565 | /// This can fail if the annotation contains a name that couldn't be found |
1566 | /// in the global time zone database. If you need to use something other |
1567 | /// than the global time zone database, then use |
1568 | /// [`TimeZoneAnnotation::to_time_zone_with`]. |
1569 | /// |
1570 | /// Note that it may be more convenient to use |
1571 | /// [`Pieces::to_time_zone_with`]. |
1572 | /// |
1573 | /// # Example |
1574 | /// |
1575 | /// ``` |
1576 | /// use jiff::{fmt::temporal::Pieces, tz::TimeZone}; |
1577 | /// |
1578 | /// let pieces = Pieces::parse("2025-01-02T16:47-05[Australia/Tasmania]" )?; |
1579 | /// let ann = pieces.time_zone_annotation().unwrap(); |
1580 | /// assert_eq!( |
1581 | /// ann.to_time_zone_with(jiff::tz::db()).unwrap(), |
1582 | /// TimeZone::get("Australia/Tasmania" ).unwrap(), |
1583 | /// ); |
1584 | /// |
1585 | /// # Ok::<(), Box<dyn std::error::Error>>(()) |
1586 | /// ``` |
1587 | #[inline ] |
1588 | pub fn to_time_zone_with( |
1589 | &self, |
1590 | db: &TimeZoneDatabase, |
1591 | ) -> Result<TimeZone, Error> { |
1592 | // NOTE: We don't currently utilize the critical flag here. Temporal |
1593 | // seems to ignore it. It's not quite clear what else we'd do with it, |
1594 | // particularly given that we provide a way to do conflict resolution |
1595 | // between offsets and time zones. |
1596 | let tz = match *self.kind() { |
1597 | TimeZoneAnnotationKind::Named(ref name) => { |
1598 | db.get(name.as_str())? |
1599 | } |
1600 | TimeZoneAnnotationKind::Offset(offset) => TimeZone::fixed(offset), |
1601 | }; |
1602 | Ok(tz) |
1603 | } |
1604 | |
1605 | /// Converts this time zone annotation into an "owned" value whose lifetime |
1606 | /// is `'static`. |
1607 | /// |
1608 | /// If this was already an "owned" value or a time zone annotation offset, |
1609 | /// then this is a no-op. |
1610 | #[cfg (feature = "alloc" )] |
1611 | #[inline ] |
1612 | pub fn into_owned(self) -> TimeZoneAnnotation<'static> { |
1613 | TimeZoneAnnotation { |
1614 | kind: self.kind.into_owned(), |
1615 | critical: self.critical, |
1616 | } |
1617 | } |
1618 | } |
1619 | |
1620 | impl<'n> From<&'n str> for TimeZoneAnnotation<'n> { |
1621 | fn from(string: &'n str) -> TimeZoneAnnotation<'n> { |
1622 | let kind: TimeZoneAnnotationKind<'_> = TimeZoneAnnotationKind::from(string); |
1623 | TimeZoneAnnotation { kind, critical: false } |
1624 | } |
1625 | } |
1626 | |
1627 | impl From<Offset> for TimeZoneAnnotation<'static> { |
1628 | fn from(offset: Offset) -> TimeZoneAnnotation<'static> { |
1629 | let kind: TimeZoneAnnotationKind<'_> = TimeZoneAnnotationKind::from(offset); |
1630 | TimeZoneAnnotation { kind, critical: false } |
1631 | } |
1632 | } |
1633 | |
1634 | /// The kind of time zone found in an [RFC 9557] timestamp, for use with |
1635 | /// [`Pieces`]. |
1636 | /// |
1637 | /// The lifetime parameter refers to the lifetime of the time zone |
1638 | /// name. The lifetime is static when the time zone annotation is |
1639 | /// offset or if the name is owned. An owned value can be produced via |
1640 | /// [`TimeZoneAnnotation::into_owned`] when the `alloc` crate feature is |
1641 | /// enabled. |
1642 | /// |
1643 | /// [RFC 9557]: https://www.rfc-editor.org/rfc/rfc9557.html |
1644 | #[derive (Clone, Debug, Eq, Hash, PartialEq)] |
1645 | #[non_exhaustive ] |
1646 | pub enum TimeZoneAnnotationKind<'n> { |
1647 | /// The time zone annotation is a name, usually an IANA time zone |
1648 | /// identifier. For example, `America/New_York`. |
1649 | Named(TimeZoneAnnotationName<'n>), |
1650 | /// The time zone annotation is an offset. For example, `-05:00`. |
1651 | Offset(Offset), |
1652 | } |
1653 | |
1654 | impl<'n> TimeZoneAnnotationKind<'n> { |
1655 | /// Converts this time zone annotation kind into an "owned" value whose |
1656 | /// lifetime is `'static`. |
1657 | /// |
1658 | /// If this was already an "owned" value or a time zone annotation offset, |
1659 | /// then this is a no-op. |
1660 | #[cfg (feature = "alloc" )] |
1661 | #[inline ] |
1662 | pub fn into_owned(self) -> TimeZoneAnnotationKind<'static> { |
1663 | match self { |
1664 | TimeZoneAnnotationKind::Named(named: TimeZoneAnnotationName<'_>) => { |
1665 | TimeZoneAnnotationKind::Named(named.into_owned()) |
1666 | } |
1667 | TimeZoneAnnotationKind::Offset(offset: Offset) => { |
1668 | TimeZoneAnnotationKind::Offset(offset) |
1669 | } |
1670 | } |
1671 | } |
1672 | } |
1673 | |
1674 | impl<'n> From<&'n str> for TimeZoneAnnotationKind<'n> { |
1675 | fn from(string: &'n str) -> TimeZoneAnnotationKind<'n> { |
1676 | let name: TimeZoneAnnotationName<'_> = TimeZoneAnnotationName::from(string); |
1677 | TimeZoneAnnotationKind::Named(name) |
1678 | } |
1679 | } |
1680 | |
1681 | impl From<Offset> for TimeZoneAnnotationKind<'static> { |
1682 | fn from(offset: Offset) -> TimeZoneAnnotationKind<'static> { |
1683 | TimeZoneAnnotationKind::Offset(offset) |
1684 | } |
1685 | } |
1686 | |
1687 | /// A time zone annotation parsed from a datetime string. |
1688 | /// |
1689 | /// By default, a time zone annotation name borrows its name from the |
1690 | /// input it was parsed from. When the `alloc` feature is enabled, |
1691 | /// callers can de-couple the annotation from the parsed input with |
1692 | /// [`TimeZoneAnnotationName::into_owned`]. |
1693 | /// |
1694 | /// A value of this type is usually found via [`Pieces::time_zone_annotation`], |
1695 | /// but callers can also construct one via this type's `From<&str>` trait |
1696 | /// implementation if necessary. |
1697 | #[derive (Clone, Debug, Eq, Hash, PartialEq)] |
1698 | pub struct TimeZoneAnnotationName<'n> { |
1699 | name: StringCow<'n>, |
1700 | } |
1701 | |
1702 | impl<'n> TimeZoneAnnotationName<'n> { |
1703 | /// Returns the name of this time zone annotation as a string slice. |
1704 | /// |
1705 | /// Note that the lifetime of the string slice returned is tied to the |
1706 | /// lifetime of this time zone annotation. This may be shorter than the |
1707 | /// lifetime of the string, `'n`, in this annotation. |
1708 | #[inline ] |
1709 | pub fn as_str<'a>(&'a self) -> &'a str { |
1710 | self.name.as_str() |
1711 | } |
1712 | |
1713 | /// Converts this time zone annotation name into an "owned" value whose |
1714 | /// lifetime is `'static`. |
1715 | /// |
1716 | /// If this was already an "owned" value, then this is a no-op. |
1717 | #[cfg (feature = "alloc" )] |
1718 | #[inline ] |
1719 | pub fn into_owned(self) -> TimeZoneAnnotationName<'static> { |
1720 | TimeZoneAnnotationName { name: self.name.into_owned() } |
1721 | } |
1722 | } |
1723 | |
1724 | impl<'n> From<&'n str> for TimeZoneAnnotationName<'n> { |
1725 | fn from(string: &'n str) -> TimeZoneAnnotationName<'n> { |
1726 | TimeZoneAnnotationName { name: StringCow::from(string) } |
1727 | } |
1728 | } |
1729 | |