#Time
Status: standard library specification draft.
This chapter defines the part of std.time that lib/std writes in
ordinary hd over the language tier:
- the
Durationtype, its public API, its arithmetic, and itsDisplaytext; - the duration suffixes
ms,s,min, andh; impl Parse for Duration, which reads Go-style duration text, andimpl Parse for Timestamp, which reads RFC 3339 text;- the host capability trait
Clock, theTimestampandInstanttypes it returns, and the helpersnow,monotonic,elapsed, andsleep!; ManualClock, the deterministicClockprovider;- the UTC
Dateof aTimestamp, RFC 3339 and ISO 8601 text, theTimestamp.formatlayouts, andParseTimeError; - the serialization opt-in of
Duration,Timestamp, andInstant.
The language tier keeps the suffix mechanism (Literal Suffixes):
| Item | Why it stays in the language tier |
|---|---|
@num_suffix, std.ops.NumSuffix |
the compiler recognizes the marker by its qualified name |
| a suffixed literal as a call | expr.suffix.user-call, a language rule, turns 250ms into ms(250) |
| suffix name lookup and typing | Literal Suffix Names and Suffixed Literals hold for every suffix |
Duration? in the test signature |
it names it as its timeout type (Test Cases); the stdlib-tier it_each, it_prop, and it_prop_with name it too |
The duration suffixes are ordinary functions marked @num_suffix, so
nothing in the language tier names ms, s, min, or h. What the
timeout option does is stdlib tier as well:
Test Timeout.
#Duration
- std-time.suffix.std.durationA
std.time.Durationis a whole number of milliseconds, held in ani64. - std-time.duration.non-negativeA
Durationis never negative: no call builds one below zero, andDurationholds zero milliseconds at the least. It is modeled on Rust'sstd::time::Duration. - std-time.suffix.std.duration-apiThe public API of
DurationisDuration::milliseconds(n: i64),Duration::seconds(n: i64),d.as_milliseconds() -> i64,d.checked_sub(rhs: Duration) -> Duration?, andd.saturating_sub(rhs: Duration) -> Duration. - std-time.duration.negative-count
Duration::milliseconds(n)andDuration::seconds(n)with a negativenpanic at run time, in every build. Panic:explicit-panic. - std-time.duration.no-neg
Durationdoes not implementstd.ops.Neg, so-5s, which is-(5s), is rejected by the type check:type-mismatch. - std-time.prelude.time-suffixes
std.timedeclaresDurationand the duration suffixesms,s,min, andh, which code imports, as inuse std.time.{Duration, s}. - std-time.duration.eq-ord
DurationimplementsEqandOrd, which compare its milliseconds. - std-time.duration.value
Durationis a struct, a value with private fields, so copying one copies its milliseconds.
use std.time.{Duration, s}
fn remaining(limit: Duration, used: Duration) -> Duration:
limit.saturating_sub(used) # zero when used is the longerWhy. A wait, a timeout, or a backoff can then never be negative, so
sleep!needs no rule for one. A signed difference is a question of order, whichchecked_subandsinceanswer with an absent value or zero, as Rust'sDurationandInstantdo.
#Duration Arithmetic
Durations add and subtract, and a duration moves a timestamp:
use std.time.{Duration, Timestamp, ms, s}
fn total(first: Duration, second: Duration) -> Duration:
first + second - 500ms
fn later(start: Timestamp) -> Timestamp:
start + 2s- std-time.duration.add
Durationimplementsstd.ops.AddwithOut = Duration.a + bholds the sum of the milliseconds ofaandb. - std-time.duration.sub-difference
Durationimplementsstd.ops.SubwithOut = Duration. Whenais not shorter thanb,a - bholds the milliseconds ofaminus those ofb. - std-time.duration.sub.underflow
a - bwithashorter thanbpanics at run time, in every build. Panic:explicit-panic. - std-time.duration.checked-sub
a.checked_sub(b)is.Some(a - b)whenais not shorter thanb, and.Noneotherwise. It never panics. - std-time.duration.saturating-sub
a.saturating_sub(b)isa - bwhenais not shorter thanb, and zero otherwise. It never panics. - std-time.duration.add-overflowA sum that does not fit in
i64milliseconds panics at run time. In a debug or test build this is checkedi64arithmetic. Panic:integer-overflow. A release build wraps the sum, and the wrapped value is negative, so the result panics asstd-time.duration.negative-countsays. - std-time.timestamp.add
Timestampimplementsstd.ops.Add[Duration]withOut = Timestamp.t + dis the timestamp whose milliseconds are those oftplus those ofd. - std-time.timestamp.add.overflowA
t + dwhose milliseconds do not fit ini64panics at run time in a debug or test build, and wraps in a release build. Panic:integer-overflow.
Note.
Timestamphas noSub:t.since(earlier)is the duration between two timestamps, bystd-time.timestamp.since-saturating.
#Duration Display
A duration displays as Go's time.Duration does, with hd's unit names.
That is hours, minutes, and seconds, as in 1h2min3.5s, or milliseconds
under one second, as in 500ms:
use std.time.{Duration, min, s}
fn waited() -> string:
"waited ${90s}, then ${60min}" # "waited 1min30s, then 1h0min0s"- std-time.duration.text
DurationimplementsDisplaywith the algorithm of Go'stime.Duration.String, using the unit namesh,min,s, andms. - std-time.duration.text.componentsA duration of one second or more displays as components. Each is a whole count followed by its unit with no space: hours
h, then minutesmin, then secondss. - std-time.duration.text.firstThe first component is the largest unit whose count is not zero.
- std-time.duration.text.lowerEvery component after the first is written down to
s, even when its count is zero, as in1h0min5sand1min0s. - std-time.duration.text.fractionThe seconds component carries the remaining milliseconds as a decimal fraction without trailing zeros, as in
3.5sand1.005s. Whole seconds have no point. - std-time.duration.text.sub-secondA duration under one second that is not zero displays as its milliseconds, then
ms, as in500ms. - std-time.duration.text.zeroA zero duration displays as
0s. - std-time.duration.text.max-valueThe longest duration displays as
2562047788015h12min55.807s. Displaying a duration never overflows.
| Milliseconds | Text |
|---|---|
0 |
0s |
500 |
500ms |
1500 |
1.5s |
1005 |
1.005s |
2000 |
2s |
60000 |
1min0s |
90000 |
1min30s |
3600000 |
1h0min0s |
3605000 |
1h0min5s |
3723500 |
1h2min3.5s |
Why. Go's text is the most widely read duration format, so a log line needs no explanation. Writing every component below the first, as Go writes
1h0m0s, keeps one spelling per duration and a shape that depends only on its largest unit.
#Duration Parsing
impl Parse for Duration reads the text form of Go's time.ParseDuration, so "1h30m".parse::[Duration]() calls it:
use std.time.{Duration, ParseTimeError}
fn timeout_of(text: string) -> Result[Duration, ParseTimeError]:
text.parse::[Duration]() # "1h30m" is 5400000 millisecondsThe text is an optional +, then one or more components. A component
is a number and a unit:
| Part | Form |
|---|---|
| sign | +, optional, once at the start |
| number | one or more digits, then optionally . and one or more digits |
| unit | h, min, m, s, ms, us, µs, μs, or ns |
- std-time.duration.parse
std.timedeclaresimpl Parse for Durationwithtype Error = ParseTimeError, and no inherent parse function.text.parse::[Duration]()returns.Okof the duration thattextnames, or.Errof the first error. - std-time.duration.parse.text-grammar
textmust hold the optional+, then one or more components, each with the parts above in order, and nothing else. No space is allowed anywhere. - std-time.duration.parse.units
his 3600 seconds.minandmare 60 seconds.sis one second, andmsis a millisecond.us,µs(U+00B5), andμs(U+03BC) are a microsecond.nsis a nanosecond. - std-time.duration.parse.components-sumThe duration is the sum of its components. A component's value is its number times its unit, with the fraction exact.
- std-time.duration.parse.exactA component whose value is not a whole number of milliseconds gives
Inexact. Nothing is rounded, so1.5msand500usare errors, and2000usand1.005sare not. - std-time.duration.parse.zero-textThe text
0, with an optional+and no unit, is the zero duration. No other number is valid without a unit. - std-time.duration.parse.range-maxA component or a running total that does not fit in
i64milliseconds givesOutOfRange. The longest duration is in range, so9223372036854775807msparses. - std-time.duration.parse.no-signA
-is not a duration character: no duration is negative, so-5sgivesInvalidCharacterat 0, as does-0. - std-time.duration.parse.round-tripFor every duration
d,d.to_string().parse::[Duration]()is.Ok(d). - std-time.duration.parse.no-panicThe
Parseimpl ofDurationnever panics.
| Text | parse gives the milliseconds |
|---|---|
1h30m |
5400000 |
1h0min5s |
3605000 |
250ms |
250 |
1.5s |
1500 |
1.5h |
5400000 |
2000us |
2 |
0 |
0 |
Why. Go's text is what config files and flags already hold.
minis accepted next to Go'smso that the textDisplaywrites parses back. A unit finer than the resolution is allowed when the value is whole, and rejected otherwise, so a duration never changes silently.
#Duration Parse Errors
A failed duration parse reports a ParseTimeError,
the one error type of all time text, at a byte position:
- std-time.duration.parse-error.too-shortText that ends where a number is needed gives
TooShortat the text's length. That is empty text, a+alone, or a.with no digit after it. - std-time.duration.parse-error.invalid-characterA character that is not a digit, where a number must start, gives
InvalidCharacterat that character, as does a non-digit after a.. So a-gives it at its own position. - std-time.duration.parse-error.missing-unitA number that is followed by the end of the text, or by a
., givesMissingUnitat the end of the number. - std-time.duration.parse-error.unknown-unitThe characters after a number, up to the next digit or
., that are not a unit giveUnknownUnitat their start. - std-time.duration.parse-error.range-position
InexactandOutOfRangeare at the first digit of the component. - std-time.duration.parse-error.orderThe text is read from the left, one component at a time. In a component the number and unit errors come first, then
OutOfRange, thenInexact, and the first error wins.
| Text | parse gives |
|---|---|
"" |
.Err(TooShort(0)) |
"+" |
.Err(TooShort(1)) |
"-5s" |
.Err(InvalidCharacter(0)) |
"1" |
.Err(MissingUnit(1)) |
"1x" |
.Err(UnknownUnit(1)) |
"1 s" |
.Err(UnknownUnit(1)) |
".5s" |
.Err(InvalidCharacter(0)) |
"1.5ms" |
.Err(Inexact(0)) |
"1h2.5ms" |
.Err(Inexact(2)) |
"9223372036854775808ms" |
.Err(OutOfRange(0)) |
#Duration Suffixes
The standard library declares these suffixes in std.time:
| Rule | Suffix | Meaning |
|---|---|---|
| std-time.suffix.std.msMilliseconds | ms |
Duration of that many milliseconds |
| std-time.suffix.std.sSeconds | s |
Duration of that many seconds |
| std-time.suffix.std.minMinutes | min |
Duration of that many minutes |
| std-time.suffix.std.hHours | h |
Duration of that many hours |
- std-time.suffix.std.fnEach is a suffix function that takes one
i64and returns the standardstd.time.Duration, as in@num_suffix pub fn ms(count: i64) -> Duration. - std-time.suffix.std.only-fourThese four are the only standard suffixes.
stddeclares nons,us,m,d, byte-size, or string suffix. - std-time.suffix.std.call-importNone is a prelude name; ordinary calls import them, as in
use std.time.{ms, s}. - std-time.suffix.std.call-overflowA standard suffix call whose result does not fit in
i64milliseconds panics at run time in a debug or test build, as inh(10_000_000_000_000_000). It wraps in a release build, asi64arithmetic does. Panic:integer-overflow.
use std.time.{Duration, ms, s}
enum Tier(limit: Duration):
Fast -> Tier(limit=250ms)
Slow -> Tier(limit=5s)Why. Every duration suffix returns
Duration, so5sand250mshave one type and mix freely.
See also: Literal Suffixes, Test Timeout.
#Clock
Clock is the host capability trait that reads time and waits:
pub trait Clock:
fn now(self) -> Timestamp
fn monotonic(self) -> Instant
fn sleep!(&mut self, duration: Duration) -> void- std-time.clock.decl-ref
std.timedeclares the host capability traitClockwith the methods above. Code imports it, as inuse std.time.Clock. - std-time.clock.now
nowreturns the current wall-clock time as aTimestamp. - std-time.clock.monotonic
monotonicreturns the current reading of a clock that never goes backwards, as anInstant. - std-time.clock.sleep
sleep!(duration)completes oncedurationhas passed on the clock. - std-time.clock.sleep.zero
sleep!with a zerodurationcompletes without waiting. - std-time.clock.plain-reads
nowandmonotonicare plain calls. Onlysleep!suspends. - std-time.clock.mut-ref.reference-values
sleep!takes&mut self, soClockis a mutable requirement trait,$.use(Clock)is a&mut dyn Clockbyreq.ref.use.reference-values, and a provider may advance its own time.
Why. A clock read is a value from the host, as an environment read is, so it needs no driver. Replay records it at the boundary either way. Waiting is the one operation that suspends.
#Timestamps And Instants
- std-time.timestamp.decl
std.timedeclaresTimestamp, a point in UTC time held as whole milliseconds since the Unix epoch in ani64, with private fields. - std-time.timestamp.api-millisecondsThe public API of
TimestampisTimestamp::from_unix_milliseconds(milliseconds: i64),t.unix_milliseconds() -> i64,t.since(earlier: Timestamp) -> Duration,t.checked_since(earlier: Timestamp) -> Duration?, and the two methods of Dates:dateandto_rfc3339. Reading RFC 3339 text isimpl Parse for Timestamp. - std-time.timestamp.unix-milliseconds
t.unix_milliseconds()returns the milliseconds since the Unix epoch thattholds, soTimestamp::from_unix_milliseconds(m).unix_milliseconds()ismfor everym. - std-time.timestamp.since-saturating
t.since(earlier)is the time fromearliertot. It is zero whenearlieris the later one, and the longest duration when the time does not fit ini64milliseconds. It never panics. - std-time.timestamp.checked-since
t.checked_since(earlier)is.Someof that time, and.Nonewhenearlieris the later one or the time does not fit ini64milliseconds. - std-time.instant.decl
std.timedeclaresInstant, a reading of the monotonic clock as whole milliseconds since an origin that the provider chooses, with private fields. - std-time.instant.api-millisecondsThe public API of
InstantisInstant::from_milliseconds(milliseconds: i64),i.as_milliseconds() -> i64,i.since(earlier: Instant) -> Duration, andi.checked_since(earlier: Instant) -> Duration?. - std-time.instant.since
i.since(earlier)is the time fromearliertoi, with the cases ofstd-time.timestamp.since-saturating: zero whenearlieris the later one, and the longest duration when the time does not fit. - std-time.instant.checked-since
i.checked_since(earlier)is.Someof that time, and.Nonein the cases ofstd-time.timestamp.checked-since. - std-time.instant.as-milliseconds
i.as_milliseconds()returns the milliseconds since the origin thatiholds, soInstant::from_milliseconds(m).as_milliseconds()ismfor everym. - std-time.time.eq-ord
TimestampandInstantimplementEqandOrd, which order them by time. - std-time.timestamp.display
TimestampimplementsDisplay, and its text ist.to_rfc3339().Instantdoes not implementDisplay. - std-time.time.importCode imports both, as in
use std.time.{Instant, Timestamp}. - std-time.time.value
TimestampandInstantare structs, values with private fields, so copying one copies its milliseconds.
use std.time.{Instant, Timestamp}
fn epoch() -> Timestamp:
Timestamp::from_unix_milliseconds(0) # displays as 1970-01-01T00:00:00Z
fn same_milliseconds(m: i64) -> bool:
Timestamp::from_unix_milliseconds(m).unix_milliseconds() == m && Instant::from_milliseconds(m).as_milliseconds() == m#Clock Helpers
use std.time.{Clock, Duration, Timestamp, now, sleep}
fn wait_and_stamp!(pause: Duration) -> Timestamp $ Clock:
sleep!(pause)
now()use std.time.{Clock, Duration, elapsed, monotonic}
fn timed_sum() -> Duration $ Clock:
start := monotonic()
let total = 0
for i in 0..1000:
total = total + i
elapsed(start)- std-time.helper.now
std.timedeclarespub fn now() -> Timestamp $ Clock, which returnsnow()of theClockprovider that covers the call. - std-time.helper.sleep
std.timedeclarespub fn sleep!(duration: Duration) -> void $ Clock, which callssleep!(duration)on that provider. - std-time.helper.monotonic
std.timedeclarespub fn monotonic() -> Instant $ Clock, which returnsmonotonic()of theClockprovider that covers the call. - std-time.helper.elapsed
std.timedeclarespub fn elapsed(since: Instant) -> Duration $ Clock, which returnsmonotonic().since(since): the time fromsinceto the provider's monotonic reading now. - std-time.helper.elapsed.manualUnder a
ManualClock,elapsed(start)is the total that was slept sincestartwas read.
Why. Timing a block is two lines, as Go's
time.Sinceand Rust'sInstant::elapsedmake it. The helpers take the provider from the requirement row, so theClocktrait is unchanged and a test times a block in virtual time.
#Manual Clock
ManualClock is the deterministic Clock provider. Its virtual time
moves only when code sleeps, and a sleep returns at once:
use std.time.{Clock, ManualClock, Timestamp, s}
fn wait_twice!() -> Timestamp $ Clock:
let mut clock = $.use(Clock)
clock.sleep!(2s)
clock.sleep!(3s)
clock.now()
tests:
use std.testing.assert
it("sleeps in virtual time"):
clock := ManualClock::new(Timestamp::from_unix_milliseconds(0))
$.with(Clock=clock):
assert(wait_twice!() == Timestamp::from_unix_milliseconds(5000), reason="five virtual seconds")- std-time.manual.decl-ref
std.timedeclares the structManualClock, which implementsClock, with private fields. Code imports it, as inuse std.time.ManualClock. Itssleep!takes&mut self, as the trait's does, so aManualClockserves as a provider through a reference to it. - std-time.manual.stateA
ManualClock's only state is its current time, aTimestamp. - std-time.manual.new-ref
ManualClock::new(start: Timestamp) -> *mut ManualClockreturns a clock whose current time isstart, a new object. A provider installed from that reference, and every copy of it, share the one clock, so a sleep through the provider moves the clock the caller holds. - std-time.manual.now
nowreturns the current time. - std-time.manual.monotonic
monotonicreturnsInstant::from_milliseconds(m), wheremis the current time in milliseconds since the Unix epoch. - std-time.manual.sleep
sleep!(duration)completes without waiting. Thedurationis added to the current time. - std-time.manual.sleep.zeroA zero
durationleaves the current time unchanged. - std-time.manual.reads
nowandmonotonicnever change the current time. - std-time.manual.no-hostA
ManualClockreads nothing from the host's clock.
Why. Virtual time advances by itself. A test of a timeout or a backoff sleeps through it in no real time and needs no extra call. A test that only moves the clock calls
sleep!on it.
See also: Host Capabilities, Mutable Providers.
#Dates
A Date is a calendar day in UTC, and t.date() gives the day that a
timestamp falls on:
use std.time.{Date, Timestamp}
fn day_of(millis: i64) -> Date:
Timestamp::from_unix_milliseconds(millis).date() # 2000-02-29 for 951782400000std.time declares it with three public fields:
pub struct Date:
pub year: i32
pub month: i32
pub day: i32- std-time.date.decl-struct
std.timedeclares the structDate, a value with the public fieldsyear,month, andday, each ani32. Copying aDatecopies its fields. Code imports it, as inuse std.time.Date. - std-time.date.calendarA
Datenames a day of the proleptic Gregorian calendar: the Gregorian leap-year rule holds for every year, including years before 1582. - std-time.date.year
yearuses astronomical numbering: year 0 is 1 BC, and year -1 is 2 BC. - std-time.date.fields
monthcounts from 1, January, to 12, anddaycounts from 1. - std-time.date.of-timestamp
t.date()returns the UTC date of the day that containst. - std-time.date.before-epochA timestamp before the epoch belongs to the day that contains it, so the timestamp of
-1milliseconds falls on 1969-12-31. - std-time.date.traits
DateimplementsEq, which compares the three fields, andDebug. - std-time.date.utc-only
std.timehas no local time and no time-zone type. A fixed offset appears only in parsed text, bystd-time.rfc3339.parse.offset.
| Milliseconds | date() |
|---|---|
0 |
1970-01-01 |
-1 |
1969-12-31 |
951782400000 |
2000-02-29 |
-2203891200000 |
1900-03-01 |
-12219379200000 |
1582-10-14 |
-62167219200000 |
0000-01-01 |
Note. The fields are public, so a literal may hold a day that does not exist, such as month 13. Only
date()promises a real day, and nostdfunction takes aDate.
Why. The proleptic Gregorian calendar with a year 0 is the one ISO 8601 and RFC 3339 use. So a date needs no table of calendar reforms. Time zones need a database, and Rust, Kotlin, and Zig keep them out of their standard libraries too.
#RFC 3339 Text
to_rfc3339 writes a timestamp as RFC 3339 text in UTC:
use std.time.Timestamp
fn stamp(millis: i64) -> string:
Timestamp::from_unix_milliseconds(millis).to_rfc3339() # "2023-11-14T22:13:20.500Z" for 1700000000500The text is YYYY-MM-DDThh:mm:ss, then .fff when the milliseconds are
not zero, then Z:
| Part | Text |
|---|---|
YYYY |
the year of t.date() |
MM, DD |
the month and day of t.date(), two digits each |
T |
the letter T |
hh, mm, ss |
the UTC hour 00 to 23, minute 00 to 59, and second 00 to 59, two digits each |
.fff |
a point and the millisecond of the second, 001 to 999, in three digits |
Z |
the letter Z |
- std-time.rfc3339.text
t.to_rfc3339()returns the text oftin UTC with the parts in the table above, in order, and nothing else. - std-time.rfc3339.text.fractionThe fraction is left out when the millisecond of the second is zero. Otherwise it has exactly three digits, so
500milliseconds is.500. - std-time.rfc3339.text.yearA year from 0 to 9999 has exactly four digits. A later year has all its digits, and a negative year is
-and at least four digits, as in-0001. - std-time.rfc3339.text.no-panic
to_rfc3339never panics.
| Milliseconds | to_rfc3339() |
|---|---|
0 |
1970-01-01T00:00:00Z |
1700000000500 |
2023-11-14T22:13:20.500Z |
1700000000007 |
2023-11-14T22:13:20.007Z |
-1 |
1969-12-31T23:59:59.999Z |
-62198755200000 |
-0001-01-01T00:00:00Z |
253402300800000 |
10000-01-01T00:00:00Z |
Note. RFC 3339 allows only the years 0000 to 9999. So the text of a year outside them is not RFC 3339, and parsing it as a
Timestamprejects it. Go'stime.Formatwrites such a year the same way.
Why. Whole seconds stay short, and a fraction shows every millisecond a
Timestampholds, as Rust's chrono writes it.
#ISO 8601 Dates
to_iso8601 writes a date as the calendar form of ISO 8601:
use std.time.Timestamp
fn day_text(millis: i64) -> string:
Timestamp::from_unix_milliseconds(millis).date().to_iso8601() # "2023-11-14" for 1700000000500- std-time.date.iso8601
d.to_iso8601()returnsYYYY-MM-DD: the year, the month in two digits, and the day in two digits, joined by-. - std-time.date.iso8601.yearThe year is written as
std-time.rfc3339.text.yearwrites it, so-0001-01-01is the first day of 2 BC. - std-time.date.iso8601.fieldsThe month and day are the fields, padded to two digits. A
Dateliteral that holds a day that does not exist is written as it is.to_iso8601never panics. - std-time.date.iso8601.same-date
t.date().to_iso8601()is the first ten characters oft.to_rfc3339()for a year from 0 to 9999.
#Layouts
format writes a timestamp in UTC by a layout of % specifiers, as
strftime does:
use std.time.Timestamp
fn clock_text(millis: i64) -> string:
Timestamp::from_unix_milliseconds(millis).format("%Y-%m-%d %H:%M") # "2023-11-14 22:13"| Specifier | Text |
|---|---|
%Y |
the year, as std-time.rfc3339.text.year writes it |
%m, %d |
the month and the day of t.date(), two digits each |
%H, %M, %S |
the UTC hour 00 to 23, minute, and second, two digits each |
%z |
the UTC offset, always +0000 |
%% |
a % |
- std-time.format
t.format(layout: string) -> stringreturnslayoutwith each specifier in the table above replaced by its text, and every other character kept. - std-time.format.unknownA
%followed by any other character, or by the end oflayout, is kept as written. So%qstays%q, and a final%stays%.formatnever panics. - std-time.format.no-locale
formathas no locale, no names of months or days, and no specifier for the milliseconds. A program writes the milliseconds withto_rfc3339. - std-time.format.rfc3339
t.format("%Y-%m-%dT%H:%M:%SZ")ist.to_rfc3339()when the milliseconds oftare zero.
Why. Dates, times, and an offset cover log lines and file names. The specifiers are those of C, Python, and Rust's chrono, which agents already write, where Go's reference-date layouts are easy to misspell. The set stays small because there is no time zone and no locale.
#Parsing RFC 3339
impl Parse for Timestamp reads the date-time form of RFC 3339,
section 5.6, the text its Display writes:
use std.time.{ParseTimeError, Timestamp}
fn read_stamp(text: string) -> Timestamp?:
match text.parse::[Timestamp](): # "2026-10-03T12:34:56.789+05:30"
.Ok(stamp) => .Some(stamp)
.Err(_) => .None| Part | Form | Range |
|---|---|---|
| year | four digits | 0000 to 9999 |
| month | - and two digits |
01 to 12 |
| day | - and two digits |
01 to the last day of the month |
| separator | T or t |
|
| hour | two digits | 00 to 23 |
| minute | : and two digits |
00 to 59 |
| second | : and two digits |
00 to 59 |
| fraction | optional: . and one or more digits |
any |
| offset | Z, z, or + or -, two hour digits, :, and two minute digits |
hours 00 to 23, minutes 00 to 59 |
- std-time.rfc3339.parse
std.timedeclaresimpl Parse for Timestampwithtype Error = ParseTimeError, and no inherent parse function.text.parse::[Timestamp]()returns.Okof the instant thattextnames, or.Errof the first error. - std-time.rfc3339.parse.grammar
textmust hold the parts in the table above, in order, each in its range, and nothing before or after them. - std-time.rfc3339.parse.caseThe separator may be
Tort, and the UTC offsetZorz. - std-time.rfc3339.parse.offsetA numeric offset is the local time's offset from UTC, so the instant is the local time minus the offset.
-00:00and+00:00mean UTC, asZdoes. - std-time.rfc3339.parse.fractionThe fraction may have any number of digits. Its digits after the third are ignored, so the time is truncated to the millisecond.
- std-time.rfc3339.parse.leap-dayFebruary 29 is in range only in a leap year: a year divisible by 4, except one divisible by 100 but not by 400.
- std-time.rfc3339.parse.leap-secondSecond
60is out of range. - std-time.rfc3339.parse.round-tripFor every timestamp
twhose year is 0 to 9999,t.to_string().parse::[Timestamp]()is.Ok(t). - std-time.rfc3339.parse.no-panicThe
Parseimpl ofTimestampnever panics.
| Text | Parsed as a Timestamp, gives the milliseconds |
|---|---|
1970-01-01T00:00:00Z |
0 |
2000-02-29t00:00:00z |
951782400000 |
2026-10-03T12:34:56.789+05:30 |
1791011096789 |
1999-12-31T23:00:00-02:00 |
946688400000 |
1969-12-31T23:59:59.9999999Z |
-1 |
Why. A
Timestampcounts Unix milliseconds, which have no leap seconds, so:60has no instant to name; Go'stime.Parserejects it too. RFC 3339 allows a space for theTonly in a note, so the parser keeps to its grammar.
#Time Parse Errors
One error type serves all time text: RFC 3339 timestamps and duration text. A failed parse reports one of six errors, each at a position:
pub enum ParseTimeError:
InvalidCharacter(position: usize)
OutOfRange(position: usize)
TooShort(position: usize)
MissingUnit(position: usize)
UnknownUnit(position: usize)
Inexact(position: usize)- std-time.parse-error.declared
std.timedeclares the enumParseTimeErrorwith the variants above, each with one fieldposition: usize. Code imports it, as inuse std.time.ParseTimeError. - std-time.parse-error.position-bytesA
positionis a byte offset into the text, counted in bytes of its UTF-8 encoding from 0, asslicecounts. - std-time.parse-error.invalid-characterA character that the grammar does not allow at its place gives
InvalidCharacterat that character. So does a character after a complete timestamp. - std-time.parse-error.out-of-rangeA field whose digits are well formed but whose value is out of its range gives
OutOfRangeat the field's first digit. - std-time.parse-error.too-shortText that ends before the timestamp is complete gives
TooShort, whosepositionis the text's length. - std-time.parse-error.orderThe text is read from the left, and the first error found wins. A field's range is checked as soon as its last digit is read.
- std-time.parse-error.traits
ParseTimeErrorimplementsEq,Debug,Display, andError. - std-time.parse-error.duration-variants
MissingUnit,UnknownUnit, andInexactcome only from duration text, as Duration Parse Errors says. A timestamp parse gives onlyInvalidCharacter,OutOfRange, andTooShort.
| Text | Parsed as a Timestamp, gives |
|---|---|
"" |
.Err(TooShort(0)) |
"2026-10-03" |
.Err(TooShort(10)) |
"2026-10-03 12:00:00Z" |
.Err(InvalidCharacter(10)) |
"2026-10-03T12:00:00+0530" |
.Err(InvalidCharacter(22)) |
"2026-10-03T12:00:00Zx" |
.Err(InvalidCharacter(20)) |
"2026-13-03T12:00:00Z" |
.Err(OutOfRange(5)) |
"1900-02-29T12:00:00Z" |
.Err(OutOfRange(8)) |
"2016-12-31T23:59:60Z" |
.Err(OutOfRange(17)) |
"2026-02-30" |
.Err(OutOfRange(8)), not TooShort |
See also: Decode Errors, Integer Parsing.
#Serialization
Duration and Instant give their
serialization opt-in as their
whole milliseconds, and Timestamp as its RFC 3339 text:
use std.json.encode
use std.serde.{Serialize, Deserialize}
use std.time.{Duration, Timestamp}
@derive(Serialize, Deserialize)
struct Lease:
holder: string
granted_at: Timestamp
term: Duration
fn record(lease: Lease) -> string:
encode(lease) # {"holder":"ada","granted_at":"2023-11-14T22:13:20Z","term":30000}- std-time.serde.impls
Duration,Timestamp, andInstanteach implementstd.serde.Serializeandstd.serde.Deserialize. - std-time.serde.int-formA
Durationand anInstanteach write oneintholding their milliseconds, asas_milliseconds()gives. - std-time.serde.text-formA
Timestampwrites one text: its RFC 3339 text in UTC with milliseconds, the same text itsDisplaygives. - std-time.serde.int-readAn
Instant, and aDurationwhose value is a number, each read oneintwith the type's name asexpected, as in"Duration", and hold that many milliseconds. - std-time.serde.negative-readA
Durationwhose number is negative is a decode error namingDuration, as one the parser rejects is. No negativeDurationis built. - std-time.serde.text-readA
Timestampreads one text with"Timestamp"asexpected, and holds whatParseforTimestampaccepts. A text the parser rejects is a decode error namingTimestamp. - std-time.serde.duration-text-readA
Durationwhose value is text reads one text with"Duration"asexpected, and holds whatParseforDurationaccepts, as"30s". A text the parser rejects is a decode error namingDuration. Writing stays whole milliseconds, so aDurationreads both forms and writes one.
Why. A timestamp reads the way it displays, so JSON carries the same text a log line does. Durations and instants stay integers of milliseconds, which round trip exactly, and the private fields stay private to code.
Note. The implementations let a
Clockprovider'sTimestampandInstantcross the host boundary into hd, andsleep!'sDurationcross out, bymodule.boundary.in.