jiff/fmt/temporal/pieces.rs
1use 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 annotation in square brackets, \
150/// which is required for parsing a zoned datetime",
151/// );
152/// ```
153///
154/// The above fails because an RFC 3339 timestamp only contains an offset,
155/// not a time zone, and thus the resulting `Zoned` could never do time zone
156/// aware arithmetic.
157///
158/// However, in some cases, you might want to bypass these protections and
159/// creat a `Zoned` value with a fixed offset time zone anyway. For example,
160/// perhaps your use cases don't need time zone aware arithmetic, but want to
161/// preserve the offset anyway. This can be accomplished with `Pieces`:
162///
163/// ```
164/// use jiff::{fmt::temporal::Pieces, tz::TimeZone};
165///
166/// let pieces = Pieces::parse("2025-01-03T17:28-05")?;
167/// let time = pieces.time().unwrap_or_else(jiff::civil::Time::midnight);
168/// let dt = pieces.date().to_datetime(time);
169/// let Some(offset) = pieces.to_numeric_offset() else {
170/// let msg = format!(
171/// "datetime string has no offset, \
172/// and thus cannot be parsed into an instant",
173/// );
174/// return Err(msg.into());
175/// };
176/// let zdt = TimeZone::fixed(offset).to_zoned(dt)?;
177/// assert_eq!(zdt.to_string(), "2025-01-03T17:28:00-05:00[-05:00]");
178///
179/// # Ok::<(), Box<dyn std::error::Error>>(())
180/// ```
181///
182/// One problem with the above code snippet is that it completely ignores if
183/// a time zone annotation is present. If it is, it probably makes sense to use
184/// it, but "fall back" to a fixed offset time zone if it isn't (which the
185/// higher level `Zoned` parsing function won't do for you):
186///
187/// ```
188/// use jiff::{fmt::temporal::Pieces, tz::TimeZone};
189///
190/// let timestamp = "2025-01-02T15:13-05";
191///
192/// let pieces = Pieces::parse(timestamp)?;
193/// let time = pieces.time().unwrap_or_else(jiff::civil::Time::midnight);
194/// let dt = pieces.date().to_datetime(time);
195/// let tz = match pieces.to_time_zone()? {
196/// Some(tz) => tz,
197/// None => {
198/// let Some(offset) = pieces.to_numeric_offset() else {
199/// let msg = format!(
200/// "timestamp `{timestamp}` has no time zone \
201/// or offset, and thus cannot be parsed into \
202/// an instant",
203/// );
204/// return Err(msg.into());
205/// };
206/// TimeZone::fixed(offset)
207/// }
208/// };
209/// // We don't bother with offset conflict resolution. And note that
210/// // this uses automatic "compatible" disambiguation in the case of
211/// // discontinuities. Of course, this is all moot if `TimeZone` is
212/// // fixed. The above code handles the case where it isn't!
213/// let zdt = tz.to_zoned(dt)?;
214/// assert_eq!(zdt.to_string(), "2025-01-02T15:13:00-05:00[-05:00]");
215///
216/// # Ok::<(), Box<dyn std::error::Error>>(())
217/// ```
218///
219/// This is mostly the same as above, but if an annotation is present, we use
220/// a `TimeZone` derived from that over the offset present.
221///
222/// However, this still doesn't quite capture what happens when parsing into a
223/// `Zoned` value. In particular, parsing into a `Zoned` is _also_ doing offset
224/// conflict resolution for you. An offset conflict occurs when there is a
225/// mismatch between the offset in an RFC 3339 timestamp and the time zone in
226/// an RFC 9557 time zone annotation.
227///
228/// For example, `2024-06-14T17:30-05[America/New_York]` has a mismatch
229/// since the date is in daylight saving time, but the offset, `-05`, is the
230/// offset for standard time in `America/New_York`. If this datetime were
231/// fed to the above code, then the `-05` offset would be completely ignored
232/// and `America/New_York` would resolve the datetime based on its rules. In
233/// this case, you'd get `2024-06-14T17:30-04`, which is a different instant
234/// than the original datetime!
235///
236/// You can either implement your own conflict resolution or use
237/// [`tz::OffsetConflict`](crate::tz::OffsetConflict) to do it for you.
238///
239/// ```
240/// use jiff::{fmt::temporal::Pieces, tz::{OffsetConflict, TimeZone}};
241///
242/// let timestamp = "2024-06-14T17:30-05[America/New_York]";
243/// // The default for conflict resolution when parsing into a `Zoned` is
244/// // actually `Reject`, but we use `AlwaysOffset` here to show a different
245/// // strategy. You'll want to pick the conflict resolution that suits your
246/// // needs. The `Reject` strategy is what you should pick if you aren't
247/// // sure.
248/// let conflict_resolution = OffsetConflict::AlwaysOffset;
249///
250/// let pieces = Pieces::parse(timestamp)?;
251/// let time = pieces.time().unwrap_or_else(jiff::civil::Time::midnight);
252/// let dt = pieces.date().to_datetime(time);
253/// let ambiguous_zdt = match pieces.to_time_zone()? {
254/// Some(tz) => {
255/// match pieces.to_numeric_offset() {
256/// None => tz.into_ambiguous_zoned(dt),
257/// Some(offset) => {
258/// conflict_resolution.resolve(dt, offset, tz)?
259/// }
260/// }
261/// }
262/// None => {
263/// let Some(offset) = pieces.to_numeric_offset() else {
264/// let msg = format!(
265/// "timestamp `{timestamp}` has no time zone \
266/// or offset, and thus cannot be parsed into \
267/// an instant",
268/// );
269/// return Err(msg.into());
270/// };
271/// // Won't even be ambiguous, but gets us the same
272/// // type as the branch above.
273/// TimeZone::fixed(offset).into_ambiguous_zoned(dt)
274/// }
275/// };
276/// // We do compatible disambiguation here like we do in the previous
277/// // examples, but you could choose any strategy. As with offset conflict
278/// // resolution, if you aren't sure what to pick, a safe choice here would
279/// // be `ambiguous_zdt.unambiguous()`, which will return an error if the
280/// // datetime is ambiguous in any way. Then, if you ever hit an error, you
281/// // can examine the case to see if it should be handled in a different way.
282/// let zdt = ambiguous_zdt.compatible()?;
283/// // Notice that we now have a different civil time and offset, but the
284/// // instant it corresponds to is the same as the one we started with.
285/// assert_eq!(zdt.to_string(), "2024-06-14T18:30:00-04:00[America/New_York]");
286///
287/// # Ok::<(), Box<dyn std::error::Error>>(())
288/// ```
289///
290/// The above has effectively completely rebuilt the higher level `Zoned`
291/// parsing routine, but with a fallback to a fixed time zone when a time zone
292/// annotation is not present.
293///
294/// # Case study: inferring the time zone of RFC 3339 timestamps
295///
296/// As [one real world use case details][infer-time-zone], it might be
297/// desirable to try and infer the time zone of RFC 3339 timestamps with
298/// varying offsets. This might be applicable when:
299///
300/// * You have out-of-band information, possibly contextual, that indicates
301/// the timestamps have to come from a fixed set of time zones.
302/// * The time zones have different standard offsets.
303/// * You have a specific desire or need to use a [`Zoned`] value for its
304/// ergonomics and time zone aware handling. After all, in this case, you
305/// believe the timestamps to actually be generated from a specific time zone,
306/// but the interchange format doesn't support carrying that information. Or
307/// the source data simply omits it.
308///
309/// In other words, you might be trying to make the best of a bad situation.
310///
311/// A `Pieces` can help you accomplish this because it gives you access to each
312/// component of a parsed datetime, and thus lets you implement arbitrary logic
313/// for how to translate that into a `Zoned`. In this case, there is
314/// contextual information that Jiff can't possibly know about.
315///
316/// The general approach we take here is to make use of
317/// [`tz::OffsetConflict`](crate::tz::OffsetConflict) to query whether a
318/// timestamp has a fixed offset compatible with a particular time zone. And if
319/// so, we can _probably_ assume it comes from that time zone. One hitch is
320/// that it's possible for the timestamp to be valid for multiple time zones,
321/// so we check that as well.
322///
323/// In the use case linked above, we have fixed offset timestamps from
324/// `America/Chicago` and `America/New_York`. So let's try implementing the
325/// above strategy. Note that we assume our inputs are RFC 3339 fixed offset
326/// timestamps and error otherwise. This is just to keep things simple. To
327/// handle data that is more varied, see the previous case study where we
328/// respect a time zone annotation if it's present, and fall back to a fixed
329/// offset time zone if it isn't.
330///
331/// ```
332/// use jiff::{fmt::temporal::Pieces, tz::{OffsetConflict, TimeZone}, Zoned};
333///
334/// // The time zones we're allowed to choose from.
335/// let tzs = &[
336/// TimeZone::get("America/New_York")?,
337/// TimeZone::get("America/Chicago")?,
338/// ];
339///
340/// // Here's our data that lacks time zones. The task is to assign a time zone
341/// // from `tzs` to each below and convert it to a `Zoned`. If we fail on any
342/// // one, then we substitute `None`.
343/// let data = &[
344/// "2024-01-13T10:33-05",
345/// "2024-01-25T12:15-06",
346/// "2024-03-10T02:30-05",
347/// "2024-06-08T14:01-05",
348/// "2024-06-12T11:46-04",
349/// "2024-11-03T01:30-05",
350/// ];
351/// // Our answers.
352/// let mut zdts: Vec<Option<Zoned>> = vec![];
353/// for string in data {
354/// // Parse and gather up the data that we can from the input.
355/// // In this case, that's a civil datetime and an offset from UTC.
356/// let pieces = Pieces::parse(string)?;
357/// let time = pieces.time().unwrap_or_else(jiff::civil::Time::midnight);
358/// let dt = pieces.date().to_datetime(time);
359/// let Some(offset) = pieces.to_numeric_offset() else {
360/// // A robust implementation should use a TZ annotation if present.
361/// return Err("missing offset".into());
362/// };
363/// // Now collect all time zones that are valid for this timestamp.
364/// let mut candidates = vec![];
365/// for tz in tzs {
366/// let result = OffsetConflict::Reject.resolve(dt, offset, tz.clone());
367/// // The parsed offset isn't valid for this time zone, so reject it.
368/// let Ok(ambiguous_zdt) = result else { continue };
369/// // This can never fail because we used the "reject" conflict
370/// // resolution strategy. It will never return an ambiguous
371/// // `Zoned` since we always have a valid offset that does
372/// // disambiguation for us.
373/// let zdt = ambiguous_zdt.unambiguous().unwrap();
374/// candidates.push(zdt);
375/// }
376/// if candidates.len() == 1 {
377/// zdts.push(Some(candidates.pop().unwrap()));
378/// } else {
379/// zdts.push(None);
380/// }
381/// }
382/// assert_eq!(zdts, vec![
383/// Some("2024-01-13T10:33-05[America/New_York]".parse()?),
384/// Some("2024-01-25T12:15-06[America/Chicago]".parse()?),
385/// // Failed because the clock time falls in a gap in the
386/// // transition to daylight saving time, and it could be
387/// // valid for either America/New_York or America/Chicago.
388/// None,
389/// Some("2024-06-08T14:01-05[America/Chicago]".parse()?),
390/// Some("2024-06-12T11:46-04[America/New_York]".parse()?),
391/// // Failed because the clock time falls in a fold in the
392/// // transition out of daylight saving time, and it could be
393/// // valid for either America/New_York or America/Chicago.
394/// None,
395/// ]);
396///
397/// # Ok::<(), Box<dyn std::error::Error>>(())
398/// ```
399///
400/// The one hitch here is that if the time zones are close to each
401/// geographically and both have daylight saving time, then there are some
402/// RFC 3339 timestamps that are truly ambiguous. For example,
403/// `2024-11-03T01:30-05` is perfectly valid for both `America/New_York` and
404/// `America/Chicago`. In this case, there is no way to tell which time zone
405/// the timestamp belongs to. It might be reasonable to return an error in
406/// this case or omit the timestamp. It depends on what you need to do.
407///
408/// With more effort, it would also be possible to optimize the above routine
409/// by utilizing [`TimeZone::preceding`] and [`TimeZone::following`] to get
410/// the exact boundaries of each time zone transition. Then you could use an
411/// offset lookup table for each range to determine the appropriate time zone.
412///
413/// [infer-time-zone]: https://github.com/BurntSushi/jiff/discussions/181#discussioncomment-11729435
414#[derive(Clone, Debug, Eq, Hash, PartialEq)]
415#[cfg_attr(feature = "defmt", derive(defmt::Format))]
416pub struct Pieces<'n> {
417 date: Date,
418 time: Option<Time>,
419 offset: Option<PiecesOffset>,
420 time_zone_annotation: Option<TimeZoneAnnotation<'n>>,
421}
422
423impl<'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 /// The "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
1063impl 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
1070impl 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
1077impl From<Timestamp> for Pieces<'static> {
1078 #[inline]
1079 fn from(ts: Timestamp) -> Pieces<'static> {
1080 let dt = Offset::UTC.to_datetime(ts);
1081 Pieces::from(dt).with_offset(PiecesOffset::Zulu)
1082 }
1083}
1084
1085impl From<(Timestamp, Offset)> for Pieces<'static> {
1086 #[inline]
1087 fn from((ts, offset): (Timestamp, Offset)) -> Pieces<'static> {
1088 Pieces::from(offset.to_datetime(ts)).with_offset(offset)
1089 }
1090}
1091
1092impl<'a> From<&'a Zoned> for Pieces<'a> {
1093 #[inline]
1094 fn from(zdt: &'a Zoned) -> Pieces<'a> {
1095 let mut pieces =
1096 Pieces::from(zdt.datetime()).with_offset(zdt.offset());
1097 if let Some(name) = 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
1106impl<'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 =
1111 f.precision().map(|p| u8::try_from(p).unwrap_or(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#[cfg_attr(feature = "defmt", derive(defmt::Format))]
1179#[non_exhaustive]
1180pub enum PiecesOffset {
1181 /// The "Zulu" offset, corresponding to UTC in a context where the offset
1182 /// for civil time is unknown or unavailable.
1183 ///
1184 /// [RFC 9557] defines this as equivalent in semantic meaning to `-00:00`:
1185 ///
1186 /// > If the time in UTC is known, but the offset to local time is unknown,
1187 /// > this can be represented with an offset of `Z`. (The original version
1188 /// > of this specification provided `-00:00` for this purpose, which is
1189 /// > not allowed by ISO-8601:2000 and therefore is less interoperable;
1190 /// > Section 3.3 of RFC 5322 describes a related convention for email,
1191 /// > which does not have this problem). This differs semantically from an
1192 /// > offset of `+00:00`, which implies that UTC is the preferred reference
1193 /// > point for the specified time.
1194 ///
1195 /// [RFC 9557]: https://www.rfc-editor.org/rfc/rfc9557
1196 Zulu,
1197 /// A specific numeric offset, including whether the parsed sign is
1198 /// negative.
1199 ///
1200 /// The sign is usually redundant, since an `Offset` is itself signed. But
1201 /// it can be used to distinguish between `+00:00` (`+00` is the preferred
1202 /// offset) and `-00:00` (`+00` is what should be used, but only because
1203 /// the offset to local time is not known). Generally speaking, one should
1204 /// regard `-00:00` as equivalent to `Z`, per RFC 9557.
1205 Numeric(PiecesNumericOffset),
1206}
1207
1208impl PiecesOffset {
1209 /// Converts this offset to a concrete numeric offset in all cases.
1210 ///
1211 /// If this was a `Z` or a `-00:00` offset, then `Offset::UTC` is returned.
1212 /// In all other cases, the underlying numeric offset is returned as-is.
1213 ///
1214 /// # Example
1215 ///
1216 /// ```
1217 /// use jiff::{
1218 /// fmt::temporal::{Pieces, PiecesNumericOffset, PiecesOffset},
1219 /// tz::Offset,
1220 /// };
1221 ///
1222 /// let pieces = Pieces::parse("1970-01-01T00:00:00Z")?;
1223 /// let off = pieces.offset().unwrap();
1224 /// // Parsed as Zulu.
1225 /// assert_eq!(off, PiecesOffset::Zulu);
1226 /// // Gets converted from Zulu to UTC, i.e., just zero.
1227 /// assert_eq!(off.to_numeric_offset(), Offset::UTC);
1228 ///
1229 /// let pieces = Pieces::parse("1970-01-01T00:00:00-00:00")?;
1230 /// let off = pieces.offset().unwrap();
1231 /// // Parsed as a negative zero.
1232 /// assert_eq!(off, PiecesOffset::from(
1233 /// PiecesNumericOffset::from(Offset::UTC).with_negative_zero(),
1234 /// ));
1235 /// // Gets converted from -00:00 to UTC, i.e., just zero.
1236 /// assert_eq!(off.to_numeric_offset(), Offset::UTC);
1237 ///
1238 /// let pieces = Pieces::parse("1970-01-01T00:00:00+00:00")?;
1239 /// let off = pieces.offset().unwrap();
1240 /// // Parsed as a positive zero.
1241 /// assert_eq!(off, PiecesOffset::from(
1242 /// PiecesNumericOffset::from(Offset::UTC),
1243 /// ));
1244 /// // Gets converted from -00:00 to UTC, i.e., just zero.
1245 /// assert_eq!(off.to_numeric_offset(), Offset::UTC);
1246 ///
1247 /// # Ok::<(), Box<dyn std::error::Error>>(())
1248 /// ```
1249 #[inline]
1250 pub fn to_numeric_offset(&self) -> Offset {
1251 match *self {
1252 PiecesOffset::Zulu => Offset::UTC,
1253 // -00:00 and +00:00 both collapse to zero here.
1254 PiecesOffset::Numeric(ref noffset) => noffset.offset(),
1255 }
1256 }
1257}
1258
1259impl From<Offset> for PiecesOffset {
1260 #[inline]
1261 fn from(offset: Offset) -> PiecesOffset {
1262 PiecesOffset::from(PiecesNumericOffset::from(offset))
1263 }
1264}
1265
1266impl From<PiecesNumericOffset> for PiecesOffset {
1267 #[inline]
1268 fn from(offset: PiecesNumericOffset) -> PiecesOffset {
1269 PiecesOffset::Numeric(offset)
1270 }
1271}
1272
1273/// A specific numeric offset, including the sign of the offset, for use with
1274/// [`Pieces`].
1275///
1276/// # Signedness
1277///
1278/// The sign attached to this type is usually redundant, since the underlying
1279/// [`Offset`] is itself signed. But it can be used to distinguish between
1280/// `+00:00` (`+00` is the preferred offset) and `-00:00` (`+00` is what should
1281/// be used, but only because the offset to local time is not known). Generally
1282/// speaking, one should regard `-00:00` as equivalent to `Z`, per [RFC 9557].
1283///
1284/// [RFC 9557]: https://www.rfc-editor.org/rfc/rfc9557
1285#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
1286#[cfg_attr(feature = "defmt", derive(defmt::Format))]
1287pub struct PiecesNumericOffset {
1288 offset: Offset,
1289 is_negative: bool,
1290}
1291
1292impl PiecesNumericOffset {
1293 /// Returns the numeric offset.
1294 ///
1295 /// # Example
1296 ///
1297 /// ```
1298 /// use jiff::{
1299 /// fmt::temporal::{Pieces, PiecesOffset},
1300 /// tz::Offset,
1301 /// };
1302 ///
1303 /// let pieces = Pieces::parse("1970-01-01T00:00:00-05:30")?;
1304 /// let off = match pieces.offset().unwrap() {
1305 /// PiecesOffset::Numeric(off) => off,
1306 /// _ => unreachable!(),
1307 /// };
1308 /// // This is really only useful if you care that an actual
1309 /// // numeric offset was written and not, e.g., `Z`. Otherwise,
1310 /// // you could just use `PiecesOffset::to_numeric_offset`.
1311 /// assert_eq!(
1312 /// off.offset(),
1313 /// Offset::from_seconds(-5 * 60 * 60 - 30 * 60).unwrap(),
1314 /// );
1315 ///
1316 /// # Ok::<(), Box<dyn std::error::Error>>(())
1317 /// ```
1318 #[inline]
1319 pub fn offset(&self) -> Offset {
1320 self.offset
1321 }
1322
1323 /// Returns whether the sign of the offset is negative or not.
1324 ///
1325 /// When formatting a [`Pieces`] to a string, this is _only_ used to
1326 /// determine the rendered sign when the [`Offset`] is itself zero. In
1327 /// all other cases, the sign rendered matches the sign of the `Offset`.
1328 ///
1329 /// Since `Offset` does not keep track of a sign when its value is zero,
1330 /// when using the `From<Offset>` trait implementation for this type,
1331 /// `is_negative` is always set to `false` when the offset is zero.
1332 ///
1333 /// # Example
1334 ///
1335 /// ```
1336 /// use jiff::{
1337 /// fmt::temporal::{Pieces, PiecesOffset},
1338 /// tz::Offset,
1339 /// };
1340 ///
1341 /// let pieces = Pieces::parse("1970-01-01T00:00:00-00:00")?;
1342 /// let off = match pieces.offset().unwrap() {
1343 /// PiecesOffset::Numeric(off) => off,
1344 /// _ => unreachable!(),
1345 /// };
1346 /// // The numeric offset component in this case is
1347 /// // indistiguisable from `Offset::UTC`. This is
1348 /// // because an `Offset` does not use different
1349 /// // representations for negative and positive zero.
1350 /// assert_eq!(off.offset(), Offset::UTC);
1351 /// // This is where `is_negative` comes in handy:
1352 /// assert_eq!(off.is_negative(), true);
1353 ///
1354 /// # Ok::<(), Box<dyn std::error::Error>>(())
1355 /// ```
1356 #[inline]
1357 pub fn is_negative(&self) -> bool {
1358 self.is_negative
1359 }
1360
1361 /// Sets this numeric offset to use `-00:00` if and only if the offset
1362 /// is zero.
1363 ///
1364 /// # Example
1365 ///
1366 /// ```
1367 /// use jiff::{
1368 /// fmt::temporal::{Pieces, PiecesNumericOffset},
1369 /// tz::Offset,
1370 /// Timestamp,
1371 /// };
1372 ///
1373 /// // If you create a `Pieces` from a `Timestamp` with a UTC offset,
1374 /// // then this is interpreted as "the offset from UTC is known and is
1375 /// // zero."
1376 /// let pieces = Pieces::from((Timestamp::UNIX_EPOCH, Offset::UTC));
1377 /// assert_eq!(pieces.to_string(), "1970-01-01T00:00:00+00:00");
1378 ///
1379 /// // Otherwise, if you create a `Pieces` from just a `Timestamp` with
1380 /// // no offset, then it is interpreted as "the offset from UTC is not
1381 /// // known." Typically, this is rendered with `Z` for "Zulu":
1382 /// let pieces = Pieces::from(Timestamp::UNIX_EPOCH);
1383 /// assert_eq!(pieces.to_string(), "1970-01-01T00:00:00Z");
1384 ///
1385 /// // But it might be the case that you want to use `-00:00` instead,
1386 /// // perhaps to conform to some existing convention or legacy
1387 /// // applications that require it:
1388 /// let pieces = Pieces::from(Timestamp::UNIX_EPOCH)
1389 /// .with_offset(
1390 /// PiecesNumericOffset::from(Offset::UTC).with_negative_zero(),
1391 /// );
1392 /// assert_eq!(pieces.to_string(), "1970-01-01T00:00:00-00:00");
1393 /// ```
1394 #[inline]
1395 pub fn with_negative_zero(self) -> PiecesNumericOffset {
1396 PiecesNumericOffset { is_negative: true, ..self }
1397 }
1398}
1399
1400impl From<Offset> for PiecesNumericOffset {
1401 #[inline]
1402 fn from(offset: Offset) -> PiecesNumericOffset {
1403 // This can of course never return a -00:00 offset, only +00:00.
1404 PiecesNumericOffset { offset, is_negative: offset.is_negative() }
1405 }
1406}
1407
1408/// An [RFC 9557] time zone annotation, for use with [`Pieces`].
1409///
1410/// A time zone annotation is either a time zone name (typically an IANA time
1411/// zone identifier) like `America/New_York`, or an offset like `-05:00`. This
1412/// is normally an implementation detail of parsing into a [`Zoned`], but the
1413/// raw annotation can be accessed via [`Pieces::time_zone_annotation`] after
1414/// parsing into a [`Pieces`].
1415///
1416/// The lifetime parameter refers to the lifetime of the time zone
1417/// name. The lifetime is static when the time zone annotation is
1418/// offset or if the name is owned. An owned value can be produced via
1419/// [`TimeZoneAnnotation::into_owned`] when the `alloc` crate feature is
1420/// enabled.
1421///
1422/// # Construction
1423///
1424/// If you're using [`Pieces`], then its [`Pieces::with_time_zone_name`] and
1425/// [`Pieces::with_time_zone_offset`] methods should absolve you of needing to
1426/// build values of this type explicitly. But if the need arises, there are
1427/// `From` impls for `&str` (time zone annotation name) and [`Offset`] (time
1428/// zone annotation offset) for this type.
1429///
1430/// # Example
1431///
1432/// ```
1433/// use jiff::{fmt::temporal::{Pieces, TimeZoneAnnotation}, tz::offset};
1434///
1435/// // A time zone annotation from a name:
1436/// let pieces = Pieces::parse("2025-01-02T16:47-05[America/New_York]")?;
1437/// assert_eq!(
1438/// pieces.time_zone_annotation().unwrap(),
1439/// &TimeZoneAnnotation::from("America/New_York"),
1440/// );
1441///
1442/// // A time zone annotation from an offset:
1443/// let pieces = Pieces::parse("2025-01-02T16:47-05[-05:00]")?;
1444/// assert_eq!(
1445/// pieces.time_zone_annotation().unwrap(),
1446/// &TimeZoneAnnotation::from(offset(-5)),
1447/// );
1448///
1449/// # Ok::<(), Box<dyn std::error::Error>>(())
1450/// ```
1451///
1452/// [RFC 9557]: https://www.rfc-editor.org/rfc/rfc9557.html
1453#[derive(Clone, Debug, Eq, Hash, PartialEq)]
1454#[cfg_attr(feature = "defmt", derive(defmt::Format))]
1455pub struct TimeZoneAnnotation<'n> {
1456 pub(crate) kind: TimeZoneAnnotationKind<'n>,
1457 /// Whether the annotation is marked as "critical," i.e., with a
1458 /// `!` prefix. When enabled, it's supposed to make the annotation
1459 /// un-ignorable.
1460 ///
1461 /// This is basically unused. And there's no way for callers to flip this
1462 /// switch currently. But it can be queried after parsing. Jiff also
1463 /// doesn't alter its behavior based on this flag. In particular, Jiff
1464 /// basically always behaves as if `critical` is true.
1465 pub(crate) critical: bool,
1466}
1467
1468impl<'n> TimeZoneAnnotation<'n> {
1469 /// Returns the "kind" of this annotation. The kind is either a name or an
1470 /// offset.
1471 ///
1472 /// # Example
1473 ///
1474 /// ```
1475 /// use jiff::fmt::temporal::{Pieces, TimeZoneAnnotation};
1476 ///
1477 /// // A time zone annotation from a name, which doesn't necessarily have
1478 /// // to point to a valid IANA time zone.
1479 /// let pieces = Pieces::parse("2025-01-02T16:47-05[Australia/Bluey]")?;
1480 /// assert_eq!(
1481 /// pieces.time_zone_annotation().unwrap(),
1482 /// &TimeZoneAnnotation::from("Australia/Bluey"),
1483 /// );
1484 ///
1485 /// # Ok::<(), Box<dyn std::error::Error>>(())
1486 /// ```
1487 #[inline]
1488 pub fn kind(&self) -> &TimeZoneAnnotationKind<'n> {
1489 &self.kind
1490 }
1491
1492 /// Returns true when this time zone is marked as "critical." This occurs
1493 /// when the time zone annotation is preceded by a `!`. It is meant to
1494 /// signify that, basically, implementations should error if the annotation
1495 /// is invalid in some way. And when it's absent, it's left up to the
1496 /// implementation's discretion about what to do (including silently
1497 /// ignoring the invalid annotation).
1498 ///
1499 /// Generally speaking, Jiff ignores this altogether for time zone
1500 /// annotations and behaves as if it's always true. But it's exposed here
1501 /// for callers to query in case it's useful.
1502 ///
1503 /// # Example
1504 ///
1505 /// ```
1506 /// use jiff::fmt::temporal::{Pieces, TimeZoneAnnotation};
1507 ///
1508 /// // not critical
1509 /// let pieces = Pieces::parse("2025-01-02T16:47-05[Australia/Bluey]")?;
1510 /// assert_eq!(
1511 /// Some(false),
1512 /// pieces.time_zone_annotation().map(|a| a.is_critical()),
1513 /// );
1514 ///
1515 /// // critical
1516 /// let pieces = Pieces::parse("2025-01-02T16:47-05[!Australia/Bluey]")?;
1517 /// assert_eq!(
1518 /// Some(true),
1519 /// pieces.time_zone_annotation().map(|a| a.is_critical()),
1520 /// );
1521 ///
1522 /// # Ok::<(), Box<dyn std::error::Error>>(())
1523 /// ```
1524 #[inline]
1525 pub fn is_critical(&self) -> bool {
1526 self.critical
1527 }
1528
1529 /// A convenience routine for converting this annotation into a time zone.
1530 ///
1531 /// This can fail if the annotation contains a name that couldn't be found
1532 /// in the global time zone database. If you need to use something other
1533 /// than the global time zone database, then use
1534 /// [`TimeZoneAnnotation::to_time_zone_with`].
1535 ///
1536 /// Note that it may be more convenient to use
1537 /// [`Pieces::to_time_zone`].
1538 ///
1539 /// # Example
1540 ///
1541 /// ```
1542 /// use jiff::{fmt::temporal::Pieces, tz::TimeZone};
1543 ///
1544 /// let pieces = Pieces::parse("2025-01-02T16:47-05[Australia/Tasmania]")?;
1545 /// let ann = pieces.time_zone_annotation().unwrap();
1546 /// assert_eq!(
1547 /// ann.to_time_zone().unwrap(),
1548 /// TimeZone::get("Australia/Tasmania").unwrap(),
1549 /// );
1550 ///
1551 /// let pieces = Pieces::parse("2025-01-02T16:47-05[Australia/Bluey]")?;
1552 /// let ann = pieces.time_zone_annotation().unwrap();
1553 /// assert_eq!(
1554 /// ann.to_time_zone().unwrap_err().to_string(),
1555 /// "failed to find time zone `Australia/Bluey` in time zone database",
1556 /// );
1557 ///
1558 /// # Ok::<(), Box<dyn std::error::Error>>(())
1559 /// ```
1560 #[inline]
1561 pub fn to_time_zone(&self) -> Result<TimeZone, Error> {
1562 self.to_time_zone_with(crate::tz::db())
1563 }
1564
1565 /// This is like [`TimeZoneAnnotation::to_time_zone`], but permits the
1566 /// caller to pass in their own time zone database.
1567 ///
1568 /// This can fail if the annotation contains a name that couldn't be found
1569 /// in the global time zone database. If you need to use something other
1570 /// than the global time zone database, then use
1571 /// [`TimeZoneAnnotation::to_time_zone_with`].
1572 ///
1573 /// Note that it may be more convenient to use
1574 /// [`Pieces::to_time_zone_with`].
1575 ///
1576 /// # Example
1577 ///
1578 /// ```
1579 /// use jiff::{fmt::temporal::Pieces, tz::TimeZone};
1580 ///
1581 /// let pieces = Pieces::parse("2025-01-02T16:47-05[Australia/Tasmania]")?;
1582 /// let ann = pieces.time_zone_annotation().unwrap();
1583 /// assert_eq!(
1584 /// ann.to_time_zone_with(jiff::tz::db()).unwrap(),
1585 /// TimeZone::get("Australia/Tasmania").unwrap(),
1586 /// );
1587 ///
1588 /// # Ok::<(), Box<dyn std::error::Error>>(())
1589 /// ```
1590 #[inline]
1591 pub fn to_time_zone_with(
1592 &self,
1593 db: &TimeZoneDatabase,
1594 ) -> Result<TimeZone, Error> {
1595 // NOTE: We don't currently utilize the critical flag here. Temporal
1596 // seems to ignore it. It's not quite clear what else we'd do with it,
1597 // particularly given that we provide a way to do conflict resolution
1598 // between offsets and time zones.
1599 let tz = match *self.kind() {
1600 TimeZoneAnnotationKind::Named(ref name) => {
1601 db.get(name.as_str())?
1602 }
1603 TimeZoneAnnotationKind::Offset(offset) => TimeZone::fixed(offset),
1604 };
1605 Ok(tz)
1606 }
1607
1608 /// Converts this time zone annotation into an "owned" value whose lifetime
1609 /// is `'static`.
1610 ///
1611 /// If this was already an "owned" value or a time zone annotation offset,
1612 /// then this is a no-op.
1613 #[cfg(feature = "alloc")]
1614 #[inline]
1615 pub fn into_owned(self) -> TimeZoneAnnotation<'static> {
1616 TimeZoneAnnotation {
1617 kind: self.kind.into_owned(),
1618 critical: self.critical,
1619 }
1620 }
1621}
1622
1623impl<'n> From<&'n str> for TimeZoneAnnotation<'n> {
1624 fn from(string: &'n str) -> TimeZoneAnnotation<'n> {
1625 let kind = TimeZoneAnnotationKind::from(string);
1626 TimeZoneAnnotation { kind, critical: false }
1627 }
1628}
1629
1630impl From<Offset> for TimeZoneAnnotation<'static> {
1631 fn from(offset: Offset) -> TimeZoneAnnotation<'static> {
1632 let kind = TimeZoneAnnotationKind::from(offset);
1633 TimeZoneAnnotation { kind, critical: false }
1634 }
1635}
1636
1637/// The kind of time zone found in an [RFC 9557] timestamp, for use with
1638/// [`Pieces`].
1639///
1640/// The lifetime parameter refers to the lifetime of the time zone
1641/// name. The lifetime is static when the time zone annotation is
1642/// offset or if the name is owned. An owned value can be produced via
1643/// [`TimeZoneAnnotation::into_owned`] when the `alloc` crate feature is
1644/// enabled.
1645///
1646/// [RFC 9557]: https://www.rfc-editor.org/rfc/rfc9557.html
1647#[derive(Clone, Debug, Eq, Hash, PartialEq)]
1648#[cfg_attr(feature = "defmt", derive(defmt::Format))]
1649#[non_exhaustive]
1650pub enum TimeZoneAnnotationKind<'n> {
1651 /// The time zone annotation is a name, usually an IANA time zone
1652 /// identifier. For example, `America/New_York`.
1653 Named(TimeZoneAnnotationName<'n>),
1654 /// The time zone annotation is an offset. For example, `-05:00`.
1655 Offset(Offset),
1656}
1657
1658impl<'n> TimeZoneAnnotationKind<'n> {
1659 /// Converts this time zone annotation kind into an "owned" value whose
1660 /// lifetime is `'static`.
1661 ///
1662 /// If this was already an "owned" value or a time zone annotation offset,
1663 /// then this is a no-op.
1664 #[cfg(feature = "alloc")]
1665 #[inline]
1666 pub fn into_owned(self) -> TimeZoneAnnotationKind<'static> {
1667 match self {
1668 TimeZoneAnnotationKind::Named(named) => {
1669 TimeZoneAnnotationKind::Named(named.into_owned())
1670 }
1671 TimeZoneAnnotationKind::Offset(offset) => {
1672 TimeZoneAnnotationKind::Offset(offset)
1673 }
1674 }
1675 }
1676}
1677
1678impl<'n> From<&'n str> for TimeZoneAnnotationKind<'n> {
1679 fn from(string: &'n str) -> TimeZoneAnnotationKind<'n> {
1680 let name = TimeZoneAnnotationName::from(string);
1681 TimeZoneAnnotationKind::Named(name)
1682 }
1683}
1684
1685impl From<Offset> for TimeZoneAnnotationKind<'static> {
1686 fn from(offset: Offset) -> TimeZoneAnnotationKind<'static> {
1687 TimeZoneAnnotationKind::Offset(offset)
1688 }
1689}
1690
1691/// A time zone annotation parsed from a datetime string.
1692///
1693/// By default, a time zone annotation name borrows its name from the
1694/// input it was parsed from. When the `alloc` feature is enabled,
1695/// callers can de-couple the annotation from the parsed input with
1696/// [`TimeZoneAnnotationName::into_owned`].
1697///
1698/// A value of this type is usually found via [`Pieces::time_zone_annotation`],
1699/// but callers can also construct one via this type's `From<&str>` trait
1700/// implementation if necessary.
1701#[derive(Clone, Debug, Eq, Hash, PartialEq)]
1702#[cfg_attr(feature = "defmt", derive(defmt::Format))]
1703pub struct TimeZoneAnnotationName<'n> {
1704 name: StringCow<'n>,
1705}
1706
1707impl<'n> TimeZoneAnnotationName<'n> {
1708 /// Returns the name of this time zone annotation as a string slice.
1709 ///
1710 /// Note that the lifetime of the string slice returned is tied to the
1711 /// lifetime of this time zone annotation. This may be shorter than the
1712 /// lifetime of the string, `'n`, in this annotation.
1713 #[inline]
1714 pub fn as_str<'a>(&'a self) -> &'a str {
1715 self.name.as_str()
1716 }
1717
1718 /// Converts this time zone annotation name into an "owned" value whose
1719 /// lifetime is `'static`.
1720 ///
1721 /// If this was already an "owned" value, then this is a no-op.
1722 #[cfg(feature = "alloc")]
1723 #[inline]
1724 pub fn into_owned(self) -> TimeZoneAnnotationName<'static> {
1725 TimeZoneAnnotationName { name: self.name.into_owned() }
1726 }
1727}
1728
1729impl<'n> From<&'n str> for TimeZoneAnnotationName<'n> {
1730 fn from(string: &'n str) -> TimeZoneAnnotationName<'n> {
1731 TimeZoneAnnotationName { name: StringCow::from(string) }
1732 }
1733}