Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

std.time

std.time provides nonnegative spans, monotonic instants, deadlines, and wall-clock timestamps as four distinct types, with clock reads and sleep. The arithmetic is ordinary embedded Plenty code. The runtime supplies only two clock reads and one sleep. No operation allocates.

Every time value is a small class, so it is affine: pass it by reference, as every operation here does, and use copy for an independent value. == compares values structurally. Ordering has no operator; compare as_nanoseconds() results or use the operations below.

from std.time import deadline_after, instant_at, milliseconds, monotonic_now, seconds, sleep

def main() -> Result[(), RangeError | IoError]:
    start = monotonic_now()
    sleep(&milliseconds(20))
    elapsed = monotonic_now().duration_since(&start)?
    print(elapsed.as_milliseconds() >= 20)?

    # A fake timeline makes deadline arithmetic exact.
    deadline = deadline_after(&instant_at(0), &seconds(5))?
    later = instant_at(seconds(2).as_nanoseconds())
    print(deadline.remaining(&later).as_milliseconds(), deadline.expired(&later))?
    Ok(())
True
3000 False

Durations

A Duration is a span of u64 nanoseconds, from zero to just over 584 years. A negative span is unrepresentable.

OperationResultContract
nanoseconds(count: u64)DurationExact
microseconds(count: u32), milliseconds(count: u32), seconds(count: u32)DurationExact; a u32 count always fits
span.as_nanoseconds()u64Exact
span.as_microseconds(), as_milliseconds(), as_seconds()u64Whole units, discarding the remainder
span.as_timeout_ms()u64Milliseconds rounded up
span.is_zero()bool
span.checked_add(&other)Result[Duration, RangeError]Error above the maximum
span.checked_sub(&other)Result[Duration, RangeError]Error when other is longer
span.saturating_sub(&other)DurationZero when other is longer
span.checked_mul(factor: u64)Result[Duration, RangeError]Error above the maximum

Constructors never fail and arithmetic never traps or wraps. Build a span of minutes or longer by multiplying, as in seconds(60).checked_mul(5). There is no float conversion, division, or text format.

Instants and deadlines

An Instant is a point on a monotonic timeline: it never moves backwards and is unaffected by changes to the system clock. Its origin is unspecified, so an instant means something only relative to another from the same timeline.

OperationResultContract
monotonic_now()InstantRead the process’s monotonic clock
instant_at(nanoseconds_after_origin: u64)InstantA point on a caller-defined timeline
at.checked_add(&span)Result[Instant, RangeError]Error past the end of the timeline
at.duration_since(&earlier)Result[Duration, RangeError]Error when earlier is later
at.saturating_duration_since(&earlier)DurationZero when earlier is later

A Deadline is the instant at which a total budget ends. Code that receives a &Deadline can make several waits without restarting the budget.

OperationResultContract
deadline_after(&start, &budget)Result[Deadline, RangeError]Error past the end of the timeline
deadline_at(&at)Deadline
deadline.expired(&now)boolTrue once now reaches the deadline
deadline.remaining(&now)DurationZero once expired

A zero budget is expired at its own start. Deadline operations take the current instant as an argument and read no clock, so their results depend only on their inputs. instant_at supplies the fake clock for simulations and tests; mixing its instants with monotonic_now() readings is meaningless and undetected.

The monotonic clock is Linux CLOCK_MONOTONIC, reported as nanoseconds since the process’s first reading. It does not advance while the machine is suspended. Readings are comparable across threads of one process and not across processes.

Clocks

Clock is the protocol for code that reads the time through a value instead of calling monotonic_now(), so a test can control what it sees:

pub protocol Clock:
    def now(self) -> Instant:
        pass

monotonic_clock() returns a MonotonicClock, whose now() is monotonic_now(); it holds no state. std.test.fake_clock returns a clock that moves only when told. A function written as def f[C: Clock](clock: &C) is compiled once per clock type, with no dynamic dispatch.

Wall-clock timestamps

A Timestamp has public fields seconds: i64, counted from the Unix epoch in UTC, and nanoseconds: u32 in [0, 1_000_000_000). Times before the epoch use floor seconds: half a second before it is Timestamp(-1, 500000000). std.fs metadata returns this type.

OperationResultContract
wall_now()TimestampRead the system clock once
Timestamp(seconds, nanoseconds)TimestampUnchecked field construction
stamp.duration_since(&earlier)Result[Duration, RangeError]Error when earlier is later, or the span exceeds a Duration
stamp.checked_add(&span)Result[Timestamp, RangeError]Error past i64 seconds

The system clock can be stepped in either direction, by an administrator or by time synchronization. A later reading may therefore be earlier, and duration_since reports that as an error instead of a negative or wrapped span. Measure elapsed time and timeouts with instants. A timestamp cannot be converted to an instant or compared with one.

A timestamp whose nanoseconds is out of range is an error in both operations. There is no subtraction of a span, leap-second table, parsing, or formatting here; the tz package provides calendars, zones, and text.

Civil time and time zones

Calendar dates, wall-clock times, IANA time zones, and RFC 3339 text live in the official tz package, not in std. It bundles a pinned release of the IANA time-zone database as Plenty source, so results do not depend on the host’s zone files, and a program that never looks up a zone links no zone data. Its API follows Rust’s jiff. Add it as a Git dependency:

[dependencies]
tz = { git = "https://github.com/plentylang/tz.git", tag = "v0.3.0" }

A tz.Zoned holds a Timestamp with its zone. Adding calendar units keeps the wall-clock reading; a local time that a transition skips or repeats is resolved by the tz.Disambiguation the caller passes:

import tz

def main() -> Result[(), tz.TzError | FormatError | AllocError | IoError]:
    new_york = tz.zone("America/New_York")?
    london = tz.zone("Europe/London")?
    meeting = tz.datetime(2024, 3, 9, 9, 30)?.to_zoned(&new_york, tz.Disambiguation.Reject)?
    next_week = meeting.checked_add(&tz.span(weeks=1), tz.Disambiguation.Reject)?
    print(f"{next_week}"?)?
    print(f"{next_week.in_zone(&london)?}"?)?
    print(meeting.span_until(&next_week).hours())?
    Ok(())
2024-03-16T09:30:00-04:00[America/New_York]
2024-03-16T13:30:00+00:00[Europe/London]
167

span_until measures elapsed time: the 167 hours above include the hour the DST transition skips. zoned.until(&other, largest, disambiguation) and since instead give a difference in calendar units up to largest (tz.Unit.Year down to Nanosecond), computed on the wall clock as jiff does; calendar units need both values in the same zone.

tz.zoned(&stamp, &zone) and zoned.timestamp() convert between the two types.

tz.zone(name) can return any zone, so it links the whole database (about 150 KB). The tz.zones module has one function per canonical zone, and each links only that zone’s data. The functions cannot fail:

import tz
from tz.zones import america_new_york

def main() -> Result[(), tz.TzError | AllocError | IoError]:
    new_york = america_new_york()
    meeting = tz.datetime(2024, 3, 10, 2, 30)?.to_zoned(&new_york, tz.Disambiguation.Compatible)?
    print(meeting.offset().seconds())?
    Ok(())

This prints -14400: 02:30 falls in that day’s gap, and Compatible moves it forward to 03:30 EDT. The package README lists the API, how zone names map to function names, its data version, and how the data is regenerated.

Sleeping and timed waits

OperationResultContract
sleep(&span)()Block this thread for at least the span
sleep_until(&deadline)()Block until the deadline; return at once if expired

Sleep measures the monotonic clock, resumes after signals, and cannot be cancelled. Scheduling may extend it; nothing shortens it. Workers may sleep and read both clocks.

Prefer std.sync’s typed waits: recv_before, send_before, select_recv_before, future_before and cancelled_before take a shared deadline and a cancellation token. The wrappers keep one total budget and preserve unsent values on every failure.

Low-level timed waits have explicit _timeout_ms names and accept u64 milliseconds. as_timeout_ms() rounds up so a positive remainder does not become a zero-budget retry loop. For example, token.wait_timeout_ms(span.as_timeout_ms()) observes that token without a second cancellation source. The old _timeout names remain aliases.

An expired deadline yields a zero budget, which those operations define as an immediate check where readiness still wins.

Errors, costs, and limits

Additions, products and non-saturating differences handle local checked arithmetic and map unrepresentable results to the existing RangeError.OutOfRange. Infallible unit conversions, saturating differences and fractional timestamp arithmetic use explicit unchecked: suites with bounds documented in source. Their guards prove nonzero divisors and representable results; no operation relies on silent wrapping except the explicit two’s-complement difference used for ordered timestamp seconds.

Every recoverable failure is the built-in RangeError.OutOfRange: the result is not representable in its type. Time arithmetic therefore propagates with ? alongside T.try_from. Clock reads and sleep cannot fail.

Each operation takes constant time and allocates nothing, including clock reads and sleep. Values hold no OS resource and may move between threads.

Supported target: x86_64-unknown-linux-gnu. See the lessons Measure time and wait and Share one deadline across waits.

Design decisions

  • Four types, no conversions between clocks. A span, a monotonic point, a budget end, and a wall time have different valid operations. Distinct types reject using a wall time for a timeout at compile time.
  • Integer nanoseconds. One exact representation with no rounding rule. Unit constructors take u32 so that construction is infallible; arithmetic is where overflow can occur, and it is always checked.
  • No hidden clock reads. expired and remaining take now. A function’s clock reads are visible at its call sites, and a fake clock needs no protocol, global state, or test-only mode.
  • Explicit rounding names. as_milliseconds discards and as_timeout_ms rounds up. Neither is a default.
  • Existing waits are unchanged. Millisecond budgets, cancellation tokens, and their wakeup rules remain the single mechanism; this module supplies the arithmetic that feeds them.
  • One timestamp type. std.fs reuses Timestamp instead of declaring its own, so file times and clock readings subtract directly.

Acknowledgements

  • Duration, Instant and their checked and saturating operations are modelled on Rust’s std::time.