Struct Pieces
struct Pieces<'n> { ... }
A low level representation of a parsed Temporal ISO 8601 datetime string.
Most users should not need to use or care about this type. Its purpose is to represent the individual components of a datetime string for more flexible parsing when use cases call for it.
One can parse into Pieces via Pieces::parse. Its date, time
(optional), offset (optional) and time zone annotation (optional) can be
queried independently. Each component corresponds to the following in a
datetime string:
{date}T{time}{offset}[{time-zone-annotation}]
For example:
2025-01-03T19:54-05[America/New_York]
A date is the only required component.
A Pieces can also be constructed from structured values via its From
trait implementations. The From trait has the following implementations
available:
From<Date>creates aPieceswith just a civilDate. All other components are left empty.From<DateTime>creates aPieceswith a civilDateandTime. The offset and time zone annotation are left empty.From<Timestamp>creates aPiecesfrom aTimestampusing a Zulu offset. This signifies that the precise instant is known, but the local time's offset from UTC is unknown. TheDateandTimeare determined viaOffset::UTC.to_datetime(timestamp). The time zone annotation is left empty.From<(Timestamp, Offset)>creates aPiecesfrom aTimestampand anOffset. TheDateandTimeare determined viaoffset.to_datetime(timestamp). The time zone annotation is left empty.From<&Zoned>creates aPiecesfrom aZoned. This populates all fields of aPieces.
A Pieces can be converted to a Temporal ISO 8601 string via its Display
trait implementation.
Example: distinguishing between Z, +00:00 and -00:00
With Pieces, it's possible to parse a datetime string and inspect the
"type" of its offset when it is zero. This makes use of the
PiecesOffset and PiecesNumericOffset auxiliary types.
use ;
let pieces = parse?;
let off = pieces.offset.unwrap;
// Parsed as Zulu.
assert_eq!;
// Gets converted from Zulu to UTC, i.e., just zero.
assert_eq!;
let pieces = parse?;
let off = pieces.offset.unwrap;
// Parsed as a negative zero.
assert_eq!;
// Gets converted from -00:00 to UTC, i.e., just zero.
assert_eq!;
let pieces = parse?;
let off = pieces.offset.unwrap;
// Parsed as a positive zero.
assert_eq!;
// Gets converted from -00:00 to UTC, i.e., just zero.
assert_eq!;
# Ok::
It's rare to need to care about these differences, but the above example
demonstrates that Pieces doesn't try to do any automatic translation for
you.
Example: it is very easy to misuse Pieces
This example shows how easily you can shoot yourself in the foot with
Pieces:
use ;
let mut pieces = parse?;
pieces = pieces.with_offset;
// This is nonsense because the offset isn't compatible with the time zone!
// Moreover, the actual instant that this timestamp represents has changed.
assert_eq!;
# Ok::
In the above example, we take a parsed Pieces, change its offset and
then format it back into a string. There are no speed bumps or errors.
A Pieces will just blindly follow your instruction, even if it produces
a nonsense result. Nonsense results are still parsable back into Pieces:
use ;
let pieces = parse?;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
# Ok::
This exemplifies that Pieces is a mostly "dumb" type that passes
through the data it contains, even if it doesn't make sense.
Case study: how to parse 2025-01-03T17:28-05 into Zoned
One thing in particular that Pieces enables callers to do is side-step
some of the stricter requirements placed on the higher level parsing
functions (such as Zoned's FromStr trait implementation). For example,
parsing a datetime string into a Zoned requires that the string contain
a time zone annotation. Namely, parsing 2025-01-03T17:28-05 into a
Zoned will fail:
use Zoned;
assert_eq!;
The above fails because an RFC 3339 timestamp only contains an offset,
not a time zone, and thus the resulting Zoned could never do time zone
aware arithmetic.
However, in some cases, you might want to bypass these protections and
creat a Zoned value with a fixed offset time zone anyway. For example,
perhaps your use cases don't need time zone aware arithmetic, but want to
preserve the offset anyway. This can be accomplished with Pieces:
use ;
let pieces = parse?;
let time = pieces.time.unwrap_or_else;
let dt = pieces.date.to_datetime;
let Some = pieces.to_numeric_offset else ;
let zdt = fixed.to_zoned?;
assert_eq!;
# Ok::
One problem with the above code snippet is that it completely ignores if
a time zone annotation is present. If it is, it probably makes sense to use
it, but "fall back" to a fixed offset time zone if it isn't (which the
higher level Zoned parsing function won't do for you):
use ;
let timestamp = "2025-01-02T15:13-05";
let pieces = parse?;
let time = pieces.time.unwrap_or_else;
let dt = pieces.date.to_datetime;
let tz = match pieces.to_time_zone? ;
// We don't bother with offset conflict resolution. And note that
// this uses automatic "compatible" disambiguation in the case of
// discontinuities. Of course, this is all moot if `TimeZone` is
// fixed. The above code handles the case where it isn't!
let zdt = tz.to_zoned?;
assert_eq!;
# Ok::
This is mostly the same as above, but if an annotation is present, we use
a TimeZone derived from that over the offset present.
However, this still doesn't quite capture what happens when parsing into a
Zoned value. In particular, parsing into a Zoned is also doing offset
conflict resolution for you. An offset conflict occurs when there is a
mismatch between the offset in an RFC 3339 timestamp and the time zone in
an RFC 9557 time zone annotation.
For example, 2024-06-14T17:30-05[America/New_York] has a mismatch
since the date is in daylight saving time, but the offset, -05, is the
offset for standard time in America/New_York. If this datetime were
fed to the above code, then the -05 offset would be completely ignored
and America/New_York would resolve the datetime based on its rules. In
this case, you'd get 2024-06-14T17:30-04, which is a different instant
than the original datetime!
You can either implement your own conflict resolution or use
tz::OffsetConflict to do it for you.
use ;
let timestamp = "2024-06-14T17:30-05[America/New_York]";
// The default for conflict resolution when parsing into a `Zoned` is
// actually `Reject`, but we use `AlwaysOffset` here to show a different
// strategy. You'll want to pick the conflict resolution that suits your
// needs. The `Reject` strategy is what you should pick if you aren't
// sure.
let conflict_resolution = AlwaysOffset;
let pieces = parse?;
let time = pieces.time.unwrap_or_else;
let dt = pieces.date.to_datetime;
let ambiguous_zdt = match pieces.to_time_zone? ;
// We do compatible disambiguation here like we do in the previous
// examples, but you could choose any strategy. As with offset conflict
// resolution, if you aren't sure what to pick, a safe choice here would
// be `ambiguous_zdt.unambiguous()`, which will return an error if the
// datetime is ambiguous in any way. Then, if you ever hit an error, you
// can examine the case to see if it should be handled in a different way.
let zdt = ambiguous_zdt.compatible?;
// Notice that we now have a different civil time and offset, but the
// instant it corresponds to is the same as the one we started with.
assert_eq!;
# Ok::
The above has effectively completely rebuilt the higher level Zoned
parsing routine, but with a fallback to a fixed time zone when a time zone
annotation is not present.
Case study: inferring the time zone of RFC 3339 timestamps
As one real world use case details, it might be desirable to try and infer the time zone of RFC 3339 timestamps with varying offsets. This might be applicable when:
- You have out-of-band information, possibly contextual, that indicates the timestamps have to come from a fixed set of time zones.
- The time zones have different standard offsets.
- You have a specific desire or need to use a
Zonedvalue for its ergonomics and time zone aware handling. After all, in this case, you believe the timestamps to actually be generated from a specific time zone, but the interchange format doesn't support carrying that information. Or the source data simply omits it.
In other words, you might be trying to make the best of a bad situation.
A Pieces can help you accomplish this because it gives you access to each
component of a parsed datetime, and thus lets you implement arbitrary logic
for how to translate that into a Zoned. In this case, there is
contextual information that Jiff can't possibly know about.
The general approach we take here is to make use of
tz::OffsetConflict to query whether a
timestamp has a fixed offset compatible with a particular time zone. And if
so, we can probably assume it comes from that time zone. One hitch is
that it's possible for the timestamp to be valid for multiple time zones,
so we check that as well.
In the use case linked above, we have fixed offset timestamps from
America/Chicago and America/New_York. So let's try implementing the
above strategy. Note that we assume our inputs are RFC 3339 fixed offset
timestamps and error otherwise. This is just to keep things simple. To
handle data that is more varied, see the previous case study where we
respect a time zone annotation if it's present, and fall back to a fixed
offset time zone if it isn't.
use ;
// The time zones we're allowed to choose from.
let tzs = &;
// Here's our data that lacks time zones. The task is to assign a time zone
// from `tzs` to each below and convert it to a `Zoned`. If we fail on any
// one, then we substitute `None`.
let data = &;
// Our answers.
let mut zdts: = vec!;
for string in data
assert_eq!;
# Ok::
The one hitch here is that if the time zones are close to each
geographically and both have daylight saving time, then there are some
RFC 3339 timestamps that are truly ambiguous. For example,
2024-11-03T01:30-05 is perfectly valid for both America/New_York and
America/Chicago. In this case, there is no way to tell which time zone
the timestamp belongs to. It might be reasonable to return an error in
this case or omit the timestamp. It depends on what you need to do.
With more effort, it would also be possible to optimize the above routine
by utilizing TimeZone::preceding and TimeZone::following to get
the exact boundaries of each time zone transition. Then you could use an
offset lookup table for each range to determine the appropriate time zone.
Implementations
impl<'n> Pieces<'n>
fn parse<I: ?Sized + AsRef<[u8]> + 'n>(input: &'n I) -> Result<Pieces<'n>, Error>Parses a Temporal ISO 8601 datetime string into a
Pieces.This is a convenience routine for
DateTimeParser::parses_pieces.Note that the
Piecesreturned is parameterized by the lifetime ofinput. This is because it might borrow a sub-slice ofinputfor a time zone annotation name. For example,Canada/Yukonin2025-01-03T16:42-07[Canada/Yukon].Example
use ; let pieces = parse?; assert_eq!; assert_eq!; assert_eq!; assert_eq!; # Ok::fn date(self: &Self) -> DateReturns the civil date in this
Pieces.Note that every
Piecesvalue is guaranteed to have aDate.Example
use ; let pieces = parse?; assert_eq!; # Ok::fn time(self: &Self) -> Option<Time>Returns the civil time in this
Pieces.The time component is optional. In
DateTimeParser, parsing into types that require a time (likeDateTime) when a time is missing automatically set the time to midnight. (Or, more precisely, the first instant of the day.)Example
use ; let pieces = parse?; assert_eq!; assert_eq!; // tricksy tricksy, the first instant of 2015-10-18 in Sao Paulo is // not midnight! let pieces = parse?; // Parsing into pieces just gives us the component parts, so no time: assert_eq!; // But if this uses higher level routines to parse into a `Zoned`, // then we can see that the missing time implies the first instant // of the day: let zdt: Zoned = "2015-10-18[America/Sao_Paulo]".parse?; assert_eq!; # Ok::fn offset(self: &Self) -> Option<PiecesOffset>Returns the offset in this
Pieces.The offset returned can be infallibly converted to a numeric offset, i.e.,
Offset. But it also includes extra data to indicate whether aZor a-00:00was parsed. (Neither of which are representable by anOffset, which doesn't distinguish between Zulu and UTC and doesn't represent negative and positive zero differently.)Example
This example shows how different flavors of
Offset::UTCcan be parsed and inspected.use ; let pieces = parse?; let off = pieces.offset.unwrap; // Parsed as Zulu. assert_eq!; // Gets converted from Zulu to UTC, i.e., just zero. assert_eq!; let pieces = parse?; let off = pieces.offset.unwrap; // Parsed as a negative zero. assert_eq!; // Gets converted from -00:00 to UTC, i.e., just zero. assert_eq!; let pieces = parse?; let off = pieces.offset.unwrap; // Parsed as a positive zero. assert_eq!; // Gets converted from -00:00 to UTC, i.e., just zero. assert_eq!; # Ok::fn time_zone_annotation(self: &Self) -> Option<&TimeZoneAnnotation<'n>>Returns the time zone annotation in this
Pieces.A time zone annotation is optional. The higher level
DateTimeParserrequires a time zone annotation when parsing into aZoned.A time zone annotation is either an offset, or more commonly, an IANA time zone identifier.
Example
use ; // A time zone annotation from a name: let pieces = parse?; assert_eq!; // A time zone annotation from an offset: let pieces = parse?; assert_eq!; # Ok::fn to_numeric_offset(self: &Self) -> Option<Offset>A convenience routine for converting an offset on this
Pieces, if present, to a numericOffset.This collapses the offsets
Z,-00:00and+00:00all toOffset::UTC. If you need to distinguish between them, then usePieces::offset.Example
This example shows how
Z,-00:00and+00:00all map to the sameOffsetvalue:use ; let pieces = parse?; assert_eq!; let pieces = parse?; assert_eq!; let pieces = parse?; assert_eq!; # Ok::fn to_time_zone(self: &Self) -> Result<Option<TimeZone>, Error>A convenience routine for converting a time zone annotation, if present, into a
TimeZone.If no annotation is on this
Pieces, then this returnsOk(None).This may return an error if the time zone annotation is a name and it couldn't be found in Jiff's global time zone database.
Example
use ; // No time zone annotations means you get `Ok(None)`: let pieces = parse?; assert_eq!; // An offset time zone annotation gets you a fixed offset `TimeZone`: let pieces = parse?; assert_eq!; // A time zone annotation name gets you a IANA time zone: let pieces = parse?; assert_eq!; // A time zone annotation name that doesn't exist gives you an error: let pieces = parse?; assert_eq!; # Ok::fn to_time_zone_with(self: &Self, db: &TimeZoneDatabase) -> Result<Option<TimeZone>, Error>A convenience routine for converting a time zone annotation, if present, into a
TimeZoneusing the givenTimeZoneDatabase.If no annotation is on this
Pieces, then this returnsOk(None).This may return an error if the time zone annotation is a name and it couldn't be found in Jiff's global time zone database.
Example
use ; // A time zone annotation name gets you a IANA time zone: let pieces = parse?; assert_eq!; # Ok::fn with_date(self: Self, date: Date) -> Pieces<'n>Set the date on this
Piecesto the one given.A
Dateis the minimal piece of information necessary to create aPieces. This method will override any previous setting.Example
use ; let pieces = from; assert_eq!; // Alternatively, build a `Pieces` from another data type, and the // date field will be automatically populated. let pieces = from; assert_eq!; assert_eq!; # Ok::fn with_time(self: Self, time: Time) -> Pieces<'n>Set the time on this
Piecesto the one given.Setting a
TimeonPiecesis optional. When formatting aPiecesto a string, a missingTimemay be omitted from the datetime string in some cases. SeePieces::with_offsetfor more details.Example
use ; let pieces = from .with_time; assert_eq!; // Alternatively, build a `Pieces` from a `DateTime` directly: let pieces = from; assert_eq!; # Ok::fn with_offset<T: Into<PiecesOffset>>(self: Self, offset: T) -> Pieces<'n>Set the offset on this
Piecesto the one given.Setting the offset on
Piecesis optional.The type of offset is polymorphic, and includes anything that can be infallibly converted into a
PiecesOffset. This includes anOffset.This refers to the offset in the RFC 3339 component of a Temporal ISO 8601 datetime string.
Since a string like
2025-01-03+11is not valid, if aPieceshas an offset set but no [Time] set, then formatting thePieceswill write an explicitTimeset to midnight.Note that this is distinct from
Pieces::with_time_zone_offset. This routine sets the offset on the datetime, whilePieces::with_time_zone_offsetsets the offset inside the time zone annotation. When the timestamp offset and the time zone annotation offset are both present, then they must be equivalent or else the datetime string is not a valid Temporal ISO 8601 string. However, aPieceswill let you format a string with mismatching offsets.Example
This example shows how easily you can shoot yourself in the foot with this routine:
use ; let mut pieces = parse?; pieces = pieces.with_offset; // This is nonsense because the offsets don't match! // And notice also that the instant that this timestamp refers to has // changed. assert_eq!; # Ok::This exemplifies that
Piecesis a mostly "dumb" type that passes through the data it contains, even if it doesn't make sense.Example: changing the offset can change the instant
Consider this case where a
Piecesis created directly from aTimestamp, and then the offset is changed.use ; let pieces = from .with_offset; assert_eq!;You might do this naively as a way of printing the timestamp of the Unix epoch with an offset of
-05from UTC. But the above does not correspond to the Unix epoch:use ; let ts: Timestamp = "1970-01-01T00:00:00-05:00".parse?; assert_eq!; # Ok::This further exemplifies how
Piecesis just a "dumb" type that passes through the data it contains.This specific example is also why
Pieceshas aFromtrait implementation for(Timestamp, Offset), which correspond more to what you want:use ; let pieces = from; assert_eq!;A decent mental model of
Piecesis that setting fields onPiecescan't change the values in memory of other fields.Example: setting an offset forces a time to be written
Consider these cases where formatting a
Pieceswon't write a [Time]:use Pieces; let pieces = from; assert_eq!; let pieces = from .with_time_zone_name; assert_eq!;This works because the resulting strings are valid. In particular, when one parses a
2025-01-03[Africa/Cairo]into aZoned, it results in a time component of midnight automatically (or more precisely, the first instead of the corresponding day):use ; let zdt: Zoned = "2025-01-03[Africa/Cairo]".parse?; assert_eq!; // tricksy tricksy, the first instant of 2015-10-18 in Sao Paulo is // not midnight! let zdt: Zoned = "2015-10-18[America/Sao_Paulo]".parse?; assert_eq!; // This happens because midnight didn't appear on the clocks in // Sao Paulo on 2015-10-18. So if you try to parse a datetime with // midnight, automatic disambiguation kicks in and chooses the time // after the gap automatically: let zdt: Zoned = "2015-10-18T00:00:00[America/Sao_Paulo]".parse?; assert_eq!; # Ok::However, if you have a date and an offset, then since things like
2025-01-03+10aren't valid Temporal ISO 8601 datetime strings, the default midnight time is automatically written:use ; let pieces = from .with_offset; assert_eq!; let pieces = from .with_offset .with_time_zone_name; assert_eq!;Example: formatting a Zulu or
-00:00offsetA
PiecesOffsetencapsulates not just a numeric offset, but also whether aZor a signed zero are used. While it's uncommon to need this, this permits one to format aPiecesusing either of these constructs:use ; let pieces = from .with_offset; assert_eq!; let pieces = from .with_offset; assert_eq!; let pieces = from .with_offset; assert_eq!;fn with_time_zone_name<'a>(self: Self, name: &'a str) -> Pieces<'a>Sets the time zone annotation on this
Piecesto the given time zone name.Setting a time zone annotation on
Piecesis optional.This is a convenience routine for using
Pieces::with_time_zone_annotationwith an explicitly constructedTimeZoneAnnotationfor a time zone name.Example
This example shows how easily you can shoot yourself in the foot with this routine:
use ; let mut pieces = parse?; pieces = pieces.with_time_zone_name; // This is nonsense because `Australia/Bluey` isn't a valid time zone! assert_eq!; # Ok::This exemplifies that
Piecesis a mostly "dumb" type that passes through the data it contains, even if it doesn't make sense.fn with_time_zone_offset(self: Self, offset: Offset) -> Pieces<'static>Sets the time zone annotation on this
Piecesto the given offset.Setting a time zone annotation on
Piecesis optional.This is a convenience routine for using
Pieces::with_time_zone_annotationwith an explicitly constructedTimeZoneAnnotationfor a time zone offset.Note that this is distinct from
Pieces::with_offset. This routine sets the offset inside the time zone annotation, whilePieces::with_offsetsets the offset on the timestamp itself. When the timestamp offset and the time zone annotation offset are both present, then they must be equivalent or else the datetime string is not a valid Temporal ISO 8601 string. However, aPieceswill let you format a string with mismatching offsets.Example
This example shows how easily you can shoot yourself in the foot with this routine:
use ; let mut pieces = parse?; pieces = pieces.with_time_zone_offset; // This is nonsense because the offset `+02` does not match `-07`. assert_eq!; # Ok::This exemplifies that
Piecesis a mostly "dumb" type that passes through the data it contains, even if it doesn't make sense.fn with_time_zone_annotation<'a>(self: Self, ann: TimeZoneAnnotation<'a>) -> Pieces<'a>Returns a new
Pieceswith the given time zone annotation.Setting a time zone annotation on
Piecesis optional.You may find it more convenient to use
Pieces::with_time_zone_nameorPieces::with_time_zone_offset.Example
This example shows how easily you can shoot yourself in the foot with this routine:
use ; let mut pieces = parse?; pieces = pieces.with_time_zone_annotation; // This is nonsense because the offset `+02` is never valid for the // `Canada/Yukon` time zone. assert_eq!; # Ok::This exemplifies that
Piecesis a mostly "dumb" type that passes through the data it contains, even if it doesn't make sense.fn into_owned(self: Self) -> Pieces<'static>Converts this
Piecesinto an "owned" value whose lifetime is'static.Ths "owned" value in this context refers to the time zone annotation name, if present. For example,
Canada/Yukonin2025-01-03T07:55-07[Canada/Yukon]. When parsing into aPieces, the time zone annotation name is borrowed. But callers may find it more convenient to work with an owned value. By calling this method, the borrowed string internally will be copied into a new string heap allocation.If
Piecesdoesn't have a time zone annotation, is already owned or the time zone annotation is an offset, then this is a no-op.
impl From for Pieces<'static>
fn from(dt: DateTime) -> Pieces<'static>
impl From for Pieces<'static>
fn from(ts: Timestamp) -> Pieces<'static>
impl From for Pieces<'static>
fn from(date: Date) -> Pieces<'static>
impl From for Pieces<'static>
fn from((ts, offset): (Timestamp, Offset)) -> Pieces<'static>
impl<'a> From for Pieces<'a>
fn from(zdt: &'a Zoned) -> Pieces<'a>
impl<'n> Clone for Pieces<'n>
fn clone(self: &Self) -> Pieces<'n>
impl<'n> Debug for Pieces<'n>
fn fmt(self: &Self, f: &mut Formatter<'_>) -> Result
impl<'n> Display for Pieces<'n>
fn fmt(self: &Self, f: &mut Formatter<'_>) -> Result
impl<'n> Eq for Pieces<'n>
impl<'n> Freeze for Pieces<'n>
impl<'n> Hash for Pieces<'n>
fn hash<__H: $crate::hash::Hasher>(self: &Self, state: &mut __H)
impl<'n> PartialEq for Pieces<'n>
fn eq(self: &Self, other: &Pieces<'n>) -> bool
impl<'n> RefUnwindSafe for Pieces<'n>
impl<'n> Send for Pieces<'n>
impl<'n> StructuralPartialEq for Pieces<'n>
impl<'n> Sync for Pieces<'n>
impl<'n> Unpin for Pieces<'n>
impl<'n> UnsafeUnpin for Pieces<'n>
impl<'n> UnwindSafe for Pieces<'n>
impl<T> Any for Pieces<'n>
fn type_id(self: &Self) -> TypeId
impl<T> Borrow for Pieces<'n>
fn borrow(self: &Self) -> &T
impl<T> BorrowMut for Pieces<'n>
fn borrow_mut(self: &mut Self) -> &mut T
impl<T> CloneToUninit for Pieces<'n>
unsafe fn clone_to_uninit(self: &Self, dest: *mut u8)
impl<T> From for Pieces<'n>
fn from(t: T) -> TReturns the argument unchanged.
impl<T> ToOwned for Pieces<'n>
fn to_owned(self: &Self) -> Tfn clone_into(self: &Self, target: &mut T)
impl<T> ToString for Pieces<'n>
fn to_string(self: &Self) -> String
impl<T, U> Into for Pieces<'n>
fn into(self: Self) -> UCalls
U::from(self).That is, this conversion is whatever the implementation of
[From]<T> for Uchooses to do.
impl<T, U> TryFrom for Pieces<'n>
fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>
impl<T, U> TryInto for Pieces<'n>
fn try_into(self: Self) -> Result<U, <U as TryFrom<T>>::Error>