Points and Spans
A point is a moment on one timeline, and a span is how far apart two points are. Each timeline has a point type of its own, and each kind of count a span type of its own, so a sum or a difference that mixes two timelines does not compile.
A Type per Timeline
| Point | Timeline | Origin | Span |
|---|---|---|---|
Timestamp | the wall clock, in UTC | 1970-01-01T00:00:00Z | Timedelta |
TaiTimestamp | International Atomic Time | 1970-01-01T00:00:00 TAI | Timedelta |
Uptime | the monotonic clock | near boot | Timedelta |
RawUptime | the monotonic clock at the hardware’s rate | near boot | Timedelta |
BootUptime | the boot clock, suspensions counted | boot | Timedelta |
Tickstamp | a hardware counter | the hardware’s own | Tickdelta |
Every point and span is an i64 and nothing else: nanoseconds for all but the
counter’s, whose unit is its tick. Each is Copy, ordered and hashable, and its
Default is zero. A count of nanoseconds holds about 292 years either side of
its origin, so a Timestamp holds 1677-09-21 to 2262-04-11.
Choosing a Clock says which clock reads which point.
Arithmetic
A point minus a point is a span, a point plus or minus a span is a point, and
spans add, subtract, negate, scale by an i64 and sum. Two points do not add.
fn a_point_minus_a_point_is_a_span() {
use t2t::{Timedelta, Timestamp};
let published = Timestamp::from_secs(1_700_000_000);
let captured = published + Timedelta::from_micros(850);
let latency = captured - published;
assert_eq!(latency, Timedelta::from_micros(850), "a point minus a point is a span");
assert_eq!(captured - latency, published, "and a point minus a span is a point");
assert_eq!(latency * 3, Timedelta::from_micros(2_550), "a span scales by a count");
assert!(latency < Timedelta::MILLISECOND, "and compares with another span");
}
Two Timelines Never Mix
Two timelines differ by an amount that only the machine knows, and that moves: a time service steps the wall clock and never the monotonic clock; TAI runs ahead of UTC by the offset the kernel holds; the raw monotonic clock drifts from the monotonic clock by whatever slewing a time service applies; the boot clock gains every suspension; and a counter’s origin is the hardware’s own. A difference taken across two of them would be a number with no meaning, so the types refuse it. A wall-clock instant minus an uptime does not compile:
//! Points of two timelines never mix: a wall-clock instant minus an uptime does not compile.
use t2t_core::{Timestamp, Uptime};
fn main() {
let _span = Timestamp::from_secs(10) - Uptime::from_secs(10);
}
error[E0277]: cannot subtract `Uptime` from `Timestamp`
--> tests/compile_fail/a_timestamp_minus_an_uptime.rs:6:42
|
6 | let _span = Timestamp::from_secs(10) - Uptime::from_secs(10);
| ^ no implementation for `Timestamp - Uptime`
|
= help: the trait `Sub<Uptime>` is not implemented for `Timestamp`
Nor does a counter’s reading moved by nanoseconds, since a tick’s length is the counter’s rate, which the type does not know:
//! Spans of two kinds never mix: a counter's reading moves by its ticks, not by nanoseconds.
use t2t_core::{Tickstamp, Timedelta};
fn main() {
let _later = Tickstamp::from_ticks(10) + Timedelta::SECOND;
}
error[E0308]: mismatched types
--> tests/compile_fail/a_tickstamp_plus_a_timedelta.rs:6:46
|
6 | let _later = Tickstamp::from_ticks(10) + Timedelta::SECOND;
| ^^^^^^^^^^^^^^^^^ expected `Tickdelta`, found `Timedelta`
Each of these is a fixture that t2t’s tests compile, and that must fail to
compile. A program that does need to compare two timelines says how: a counter’s
span crosses to nanoseconds through its rate, as
The CPU Counter shows, and two clocks’ counts compare as
plain i64s, as the TAI clock’s listing in
Choosing a Clock does.
Units
Each nanosecond point has from_nanos, from_micros, from_millis and
from_secs, and an as_* for each; a Timedelta adds from_mins,
from_hours and from_days, and a constant for each unit from NANOSECOND to
DAY, a day being 24 hours, with no calendar consulted. A counter’s point and
span have from_ticks and as_ticks. A coarser unit’s constructor saturates
where its count passes the range.
A Timedelta’s accessor counts whole units toward zero, as std’s Duration
does, and subsec_nanos, subsec_micros and subsec_millis give the rest,
signed as the span is. A point’s accessor counts the whole units since its
origin, so a point before the origin rounds down, to the unit it falls in.
fn each_unit_has_a_constructor_and_an_accessor() {
use t2t::{Timedelta, Timestamp, Uptime};
assert_eq!(Timedelta::from_mins(90), Timedelta::HOUR + Timedelta::from_mins(30), "a span");
assert_eq!(Uptime::from_secs(90).as_millis(), 90_000, "a point, counted in a finer unit");
// A span counts whole units toward zero, as std's `Duration` does.
assert_eq!(Timedelta::from_millis(1_500).as_secs(), 1, "forwards");
assert_eq!(Timedelta::from_millis(-1_500).as_secs(), -1, "and backwards");
assert_eq!(Timedelta::from_millis(-1_500).subsec_millis(), -500, "the rest has its sign");
// A point counts the whole units since its origin, so one before it rounds down.
assert_eq!(Timestamp::from_millis(-1_500).as_secs(), -2, "the second it falls in");
}
Saturation and the Checked Twins
Every operator saturates at the ends of the range, so none overflows or panics,
and each has a checked_* twin that returns None where the operator would
saturate:
| Operator | Checked twin |
|---|---|
point + span | checked_add |
point - span | checked_sub |
point - point | checked_since |
span + span | checked_add |
span - span | checked_sub |
span * count | checked_mul |
-span | checked_neg |
span.abs() | checked_abs |
none: a span has no / | checked_div |
The operator suits code where the end of the range is out of reach, or where
stopping there is right; the twin suits code to which reaching it is a bug. A
span has no /: checked_div divides it, refusing zero parts, and MIN in
-1.
fn an_operator_saturates_and_its_checked_twin_refuses() {
use t2t::{Timedelta, Timestamp};
let latest = Timestamp::MAX;
assert_eq!(latest + Timedelta::SECOND, Timestamp::MAX, "the sum stops at the end");
assert_eq!(latest.checked_add(Timedelta::SECOND), None, "where its twin refuses");
let widest = Timestamp::MAX - Timestamp::MIN;
assert_eq!(widest, Timedelta::MAX, "a span past its range stops at its end too");
assert_eq!(Timestamp::MAX.checked_since(Timestamp::MIN), None, "or is refused");
assert_eq!(Timedelta::from_secs(i64::MAX), Timedelta::MAX, "a constructor saturates");
assert_eq!(-Timedelta::MIN, Timedelta::MAX, "and so does a negation");
assert_eq!(Timedelta::MIN.checked_neg(), None, "which its twin refuses");
// A span has no `/`; `checked_div` divides it, and refuses zero parts.
let third = Timedelta::SECOND.checked_div(3);
assert_eq!(third, Some(Timedelta::from_nanos(333_333_333)), "truncated toward zero");
assert_eq!(Timedelta::SECOND.checked_div(0), None, "and refused for no parts");
}
A workspace that denies clippy’s arithmetic_side_effects lint names each point
and span in its clippy configuration’s arithmetic-side-effects-allowed, since
the lint cannot see that they saturate; t2t’s own
clippy.toml shows
the list.
Floor and Ceil
floor and ceil take a point or a span to the nearest multiple of a unit at
or below it, or at or above it: the minute an event falls in, or the end of the
bucket a histogram counts it in. They work on both sides of the origin, take the
unit’s magnitude, and leave the value as it is for a zero unit.
fn floor_and_ceil_find_the_bucket_a_point_falls_in() {
use t2t::{Timedelta, Timestamp};
let event: Timestamp = "2026-09-16T07:45:35.123Z".parse().expect("an instant in UTC");
let start = event.floor(Timedelta::MINUTE);
let end = event.ceil(Timedelta::MINUTE);
assert_eq!(format!("{start:.0}"), "2026-09-16T07:45:00Z", "the minute it falls in");
assert_eq!(format!("{end:.0}"), "2026-09-16T07:46:00Z", "and the next");
assert_eq!(start.floor(Timedelta::MINUTE), start, "a multiple stays where it is");
assert_eq!(event.floor(Timedelta::ZERO), event, "and a zero unit leaves the point");
let budget = Timedelta::from_micros(1_250);
assert_eq!(budget.ceil(Timedelta::MILLISECOND), Timedelta::from_millis(2), "a span too");
}
Code Generic Over Points
TimePoint is what every point shares: its span type, Span,
the operators between the two, and from_count and count, the point as its
i64. Code generic over it works on every timeline, as the manual clocks and
Timed::elapsed do, and a type of your own that wraps a point may implement it
to join them.
fn code_generic_over_points_takes_every_timeline() {
use t2t::{Tickdelta, Tickstamp, TimePoint, Timedelta, Uptime};
/// How long ago `stamp` is, at `now`, on whichever timeline the two are on.
fn age<P: TimePoint>(stamp: P, now: P) -> P::Span {
now - stamp
}
let span = age(Uptime::from_secs(5), Uptime::from_secs(7));
assert_eq!(span, Timedelta::from_secs(2), "a span of nanoseconds on the monotonic clock");
let ticks = age(Tickstamp::from_ticks(100), Tickstamp::from_ticks(124));
assert_eq!(ticks, Tickdelta::from_ticks(24), "and a span of ticks on a counter");
assert_eq!(Uptime::from_count(span.as_nanos()), Uptime::from_secs(2), "from a count");
}