Dates and Spellings
Every value has one spelling: Display writes it, FromStr reads it back, and
Serialization takes the same text. A timestamp’s spelling is
RFC 3339 in UTC, read off the calendar view this chapter starts with.
A Timestamp as a Date
Timestamp::to_utc reads a point as a UtcDateTime: its
year, month and day in the proleptic Gregorian calendar, its hour, minute and
second in UTC, and the nanoseconds past the second. Unix time has no leap
seconds, so the second is never 60.
A UtcDateTime is a view: arithmetic stays on the Timestamp it came from. Its
fields are public, so one may be built by hand; is_valid checks that every
field names a real date and time, and to_timestamp gives the point back, or
None for one that is not valid or is outside 1677-09-21 to 2262-04-11.
fn a_timestamp_reads_as_a_date_and_back() {
use t2t::{Timestamp, UtcDateTime};
let instant = Timestamp::from_nanos(1_789_544_735_123_456_789);
let date_time = instant.to_utc();
assert_eq!((date_time.year, date_time.month, date_time.day), (2026, 9, 16), "the date");
assert_eq!((date_time.hour, date_time.minute, date_time.second), (7, 45, 35), "the time");
assert_eq!(date_time.nanosecond, 123_456_789, "to the nanosecond");
assert_eq!(date_time.to_timestamp(), Some(instant), "and back to the instant");
// The fields are public, so a date and time can be built by hand, and checked.
let leap_day = UtcDateTime { year: 2028, month: 2, day: 29, ..date_time };
assert!(leap_day.is_valid(), "2028 is a leap year");
let no_such_day = UtcDateTime { day: 30, ..leap_day };
assert!(!no_such_day.is_valid(), "February has no 30th");
assert_eq!(no_such_day.to_timestamp(), None, "so it names no instant");
let too_late = UtcDateTime { year: 2263, ..date_time };
assert_eq!(too_late.to_timestamp(), None, "nor does a date past 2262");
}
RFC 3339
A Timestamp is written as RFC 3339 in UTC, with Z, and nine fraction digits:
2026-09-16T07:45:35.123456789Z. A precision chooses the digits: {:.3} writes
milliseconds, {:.0} none, and nine is the most. A width pads the whole
spelling, with the fill and alignment given, and never cuts it. Debug writes
what Display does.
fn an_instant_is_written_as_rfc_3339() {
use t2t::Timestamp;
let instant = Timestamp::from_nanos(1_789_544_735_123_456_789);
assert_eq!(instant.to_string(), "2026-09-16T07:45:35.123456789Z", "nine digits");
assert_eq!(format!("{instant:.3}"), "2026-09-16T07:45:35.123Z", "to the millisecond");
assert_eq!(format!("{instant:.0}"), "2026-09-16T07:45:35Z", "to the second");
assert_eq!(format!("{instant:.12}"), instant.to_string(), "never past the nanosecond");
assert_eq!(format!("[{instant:>24.0}]"), "[ 2026-09-16T07:45:35Z]", "padded to a width");
assert_eq!(format!("{instant:?}"), instant.to_string(), "and debugged as written");
}
FromStr reads what Display writes: T and Z in either case, and a
fraction of one to nine digits, or none. It refuses an offset, a 60th second, a
date that does not exist, an instant outside the range, and any text after the
Z.
fn an_instant_is_read_from_rfc_3339_in_utc() {
use t2t::{ParseTimestampError, Timestamp};
let read = "2026-09-16T07:45:35.5Z".parse::<Timestamp>();
assert_eq!(read, Ok(Timestamp::from_millis(1_789_544_735_500)), "a fraction of any length");
let lower = "1970-01-01t00:00:00z".parse::<Timestamp>();
assert_eq!(lower, Ok(Timestamp::UNIX_EPOCH), "and either case");
for (refused, reason) in [
("2026-09-16T09:45:35+02:00", "an offset: an instant is read in UTC alone"),
("2026-09-16T07:45:60Z", "a leap second, which Unix time has none of"),
("2026-02-30T07:45:35Z", "a date that does not exist"),
("2263-01-01T00:00:00Z", "a date past 2262"),
("2026-09-16T07:45:35.1234567890Z", "a tenth fraction digit"),
("2026-09-16 07:45:35Z", "a space for the `T`"),
] {
assert_eq!(refused.parse::<Timestamp>(), Err(ParseTimestampError), "{reason} is refused");
}
}
TAI
A TaiTimestamp is written as RFC 3339’s date and time,
then the zone TAI: 2026-09-16T07:46:12.123456789 TAI. The precision and the
width work as they do for a Timestamp, and FromStr reads the zone back, and
refuses a Z, since an instant in UTC is not one in TAI.
fn a_tai_instant_is_written_in_its_own_zone() {
use t2t::{ParseTaiTimestampError, TaiTimestamp, Timedelta};
let synced: TaiTimestamp = "2026-09-16T07:46:12.5 TAI".parse().expect("an instant in TAI");
let next = synced + Timedelta::from_millis(500);
assert_eq!(next.to_string(), "2026-09-16T07:46:13.000000000 TAI", "written in TAI");
assert_eq!(format!("{next:.0}"), "2026-09-16T07:46:13 TAI", "to the precision asked");
let utc = "2026-09-16T07:46:12Z".parse::<TaiTimestamp>();
assert_eq!(utc, Err(ParseTaiTimestampError), "an instant in UTC is not one in TAI");
}
Spans
A Timedelta is written the way a config spells a span:
counts with units, coarsest first, from d, h, m, s, ms, us and ns.
Once a unit is written, every finer one with a remainder is too, a zero among
them, so a minute and 123 ms is 1m0s123ms. Zero is 0s, and a span backwards
starts with -. The longest spelling is Timedelta::MIN’s,
-106751d23h47m16s854ms775us808ns.
FromStr reads that, the bare 0, and any count in each unit, so 48h reads
as the span 2d spells. It takes each unit at most once and coarsest first, and
refuses a span past the range.
An Uptime, a RawUptime and a BootUptime are written as the span since
their origin, and read back from it.
fn a_span_is_spelled_coarsest_unit_first() {
use t2t::{ParseTimedeltaError, Timedelta, Uptime};
assert_eq!(Timedelta::from_secs(90).to_string(), "1m30s", "coarsest first");
assert_eq!(Timedelta::from_millis(60_123).to_string(), "1m0s123ms", "a zero inside is written");
assert_eq!(Timedelta::from_micros(-3).to_string(), "-3us", "a sign for a span backwards");
assert_eq!(Timedelta::ZERO.to_string(), "0s", "and zero in seconds");
assert_eq!(Uptime::from_millis(42_050).to_string(), "42s50ms", "a point from boot alike");
assert_eq!("48h".parse(), Ok(Timedelta::from_days(2)), "a count may pass the next unit");
assert_eq!("0".parse(), Ok(Timedelta::ZERO), "and zero may be bare");
for refused in ["90 seconds", "1s1m", "1m1m", "+1s", "-0", "1"] {
assert_eq!(refused.parse::<Timedelta>(), Err(ParseTimedeltaError), "{refused} refused");
}
}
Counts
A Tickstamp and a Tickdelta are written
as their count, then ticks: 24 ticks, -24 ticks. A
TickRate is written as its count of hertz, then Hz:
24000000 Hz. A width pads the count and its unit together. FromStr reads
what Display writes, and nothing else: no +, no 1 tick, no kHz, and no
rate of zero.
fn a_count_is_spelled_with_its_unit() {
use t2t::{ParseTickRateError, ParseTickdeltaError, TickRate, Tickdelta, Tickstamp};
assert_eq!(Tickdelta::from_ticks(-24).to_string(), "-24 ticks", "a span of ticks");
assert_eq!(Tickstamp::from_ticks(1_000).to_string(), "1000 ticks", "a counter's reading");
assert_eq!(TickRate::GIGAHERTZ.to_string(), "1000000000 Hz", "and a rate");
assert_eq!(format!("[{:>10}]", Tickdelta::from_ticks(24)), "[ 24 ticks]", "padded whole");
assert_eq!("24 ticks".parse(), Ok(Tickdelta::from_ticks(24)), "read back");
assert_eq!("24".parse::<Tickdelta>(), Err(ParseTickdeltaError), "a count names its unit");
assert_eq!("0 Hz".parse::<TickRate>(), Err(ParseTickRateError), "and a rate is above zero");
}
Refusals
Each spelling has its refusal, and values that share a spelling share it:
| Refusal | Refuses the text of |
|---|---|
ParseTimestampError | a Timestamp |
ParseTaiTimestampError | a TaiTimestamp |
ParseTimedeltaError | a Timedelta, an Uptime, a RawUptime, a BootUptime |
ParseTickdeltaError | a Tickdelta, a Tickstamp |
ParseTickRateError | a TickRate |
Each is a unit struct, whose message names the spelling it expected, with an example of it.
fn values_that_share_a_spelling_share_its_refusal() {
use t2t::{BootUptime, ParseTickdeltaError, ParseTimedeltaError, Tickstamp, Uptime};
assert_eq!(
"soon".parse::<Uptime>(),
Err(ParseTimedeltaError),
"an uptime is a span's spelling"
);
assert_eq!("soon".parse::<BootUptime>(), Err(ParseTimedeltaError), "and so is a boot uptime");
assert_eq!("soon".parse::<Tickstamp>(), Err(ParseTickdeltaError), "a reading is a count's");
assert_eq!(
ParseTimedeltaError.to_string(),
"parse timedelta error: expected `0`, or counts with units from coarsest to finest, as \
`1m30s`",
"each refusal names the spelling it expected"
);
}