time

tape
import time from "time";
import io from "io";

let start: time.Instant = time.now();
do_work();
io.println("took {start.elapsed().as_millis()} ms");

let today: time.DateTime = time.utc_now();
io.println(today.to_iso8601());          // 2026-10-02T14:03:00.250Z

time has two clocks. The monotonic clock (Instant) is for measuring and never goes backwards. The wall clock (unix_nanos, DateTime) is the date and time in UTC; it can jump when the clock is set. A Zoned pairs an instant with a zone’s offset: the user’s own zone, or the zone a mail was sent from.

Durations

Duration is a span of time in nanoseconds, made with:

Function
nanos(n), micros(n), millis(n), seconds(n)A Duration of n units
MethodReturns
d.as_nanos(), d.as_micros(), d.as_millis(), d.as_secs()The length in that unit, truncated
d.as_secs_f64()Seconds with the fraction
d.add(other), d.sub(other)A new Duration

Measuring and waiting

FunctionDoes
now() -> InstantRead the monotonic clock
i.elapsed() -> DurationTime since i
i.since(earlier) -> DurationTime from earlier to i
i.add(d) -> Instanti moved by d
sleep(d: Duration)Pause for at least d
tape
let deadline: time.Instant = time.now().add(time.seconds(5));
while (deadline.since(time.now()).as_nanos() > 0) {
    poll();
    time.sleep(time.millis(100));
}

The OS may sleep a little longer than asked; Windows rounds up to whole milliseconds.

Dates

tape
pub struct DateTime { year: i64; month: i64; day: i64; hour: i64; minute: i64; second: i64; nanos: i64; }
FunctionDoes
unix_nanos() -> i64Nanoseconds since 1970-01-01 00:00:00 UTC
utc_now() -> DateTimeThe current UTC date and time
from_unix_nanos(ns) -> DateTimeThe date and time ns after (or before) 1970
dt.to_unix_nanos() -> i64Back to nanoseconds
dt.to_iso8601() -> string"2026-10-02T14:03:00Z"
parse_iso8601(text) -> DateTime or TimeErrorRead ISO 8601 / RFC 3339 text

to_iso8601 adds a fraction only when there is one, with 3, 6 or 9 digits (...:07.250Z).

parse_iso8601 accepts a date (2026-10-02) or a date and time (2026-10-02T14:03:00, also with t or a space), an optional fraction (.5, up to 9 digits), and an optional zone: Z, or an offset like +02:00, which is converted to UTC. Text without a zone is UTC.

tape
let t: time.DateTime = time.parse_iso8601("2026-10-02T16:03:00+02:00") or {
    io.println("bad date: {err}");
    return 1;
};
io.println(t.to_iso8601());              // 2026-10-02T14:03:00Z

It fails with TimeError.InvalidFormat for text in another shape and TimeError.OutOfRange for a field outside its range: month 13, February 30, hour 24, second 60 (leap seconds aren’t represented).

modified_ns from fs.metadata is a unix_nanos value: time.from_unix_nanos(m.modified_ns) is when the file last changed.

Time zones

tape
pub struct Zoned { utc: DateTime; offset: i64; }   // offset: minutes east of UTC
FunctionDoes
local_offset(ns) -> i64The system zone’s offset in minutes at the instant ns, daylight saving time included
local(ns) -> ZonedThe instant ns in the system zone
local_now() -> ZonedNow, in the system zone
z.local() -> DateTimeThe date and time a clock in that zone shows
z.to_iso8601() -> string"2026-10-13T09:12:03+03:00"
dt.weekday() -> i641 Monday … 7 Sunday
tape
let z: time.Zoned = time.local_now();
let t: time.DateTime = z.local();
io.println("{t.hour}:{t.minute}");       // the user's wall clock

local_offset asks the operating system, at the instant you give it: a date from last winter gets winter time, a date in summer gets summer time. Zones by name (Europe/Helsinki) aren’t supported; the system zone is the one the user set. On Uusi the offset is 0.

Mail dates

FunctionDoes
parse_rfc5322(text) -> Zoned or TimeErrorRead a mail’s Date header
z.to_rfc5322() -> string"Tue, 13 Oct 2026 09:12:03 +0300"
tape
let sent: time.Zoned = time.parse_rfc5322("Tue, 13 Oct 2026 09:12:03 +0300") or {
    return 1;
};
io.println(sent.utc.to_iso8601());       // 2026-10-13T06:12:03Z
io.println("{sent.offset}");             // 180

let date_header: string = time.local_now().to_rfc5322();

parse_rfc5322 is lenient, as mail needs. The day of the week is optional. Names match on their first three letters in any case. Comments like (EEST) and line folds are skipped. IMAP’s 13-Oct-2026 09:12:03 +0300 reads too. A 2-digit year is 1950-2049. The seconds are optional. The zone can be +0300, +03:00, GMT, UT, or a US zone name like EST or PDT. A zone that isn’t given or isn’t known (-0000, CEST) gives offset 0. Text after the zone is ignored.

It fails with InvalidFormat when the day, month, year, hour and minute aren’t there, and OutOfRange for a field outside its range (32 Oct, 24:00).

Formatting

Build a Format once, call by call, then apply it to any number of dates:

tape
var f: time.Format = time.format();
f.day();
f.text(".");
f.month();
f.text(".");
f.year();
f.text(" ");
f.hour();
f.text(":");
f.minute(2);

let z: time.Zoned = time.local_now();
io.println(f.apply_zoned(&z));          // 13.10.2026 9:05
CallWrites
year(pad), year_short()2026, 26
month(pad), month_name(), month_short()10, October, Oct
day(pad)13
weekday(), weekday_name(), weekday_short()2 (Monday is 1), Tuesday, Tue
hour(pad), hour12(pad), am_pm()9, 9, AM
minute(pad), second(pad)5 (05 with pad 2)
fraction(digits)the second’s fraction: fraction(3) is milliseconds, 250
offset(), offset(false)+03:00, +0300
text(s)s as it is

pad is the least number of digits, filled with zeros: month(2) writes 03. Without it the number is written as it is.

f.apply(d) writes a DateTime as it is (a UTC one, or a zone’s reading from z.local()); f.apply_zoned(&z) writes a Zoned in its own zone, with its offset. Day and month names are English for now.

Relative time and message-list dates

tape
io.println(time.relative(sent_ns, time.unix_nanos()));   // 5 minutes ago
io.println(time.brief(sent_ns, time.unix_nanos()));      // 14:32

relative(then, now) says how long ago (or how far ahead) then is: “just now”, “1 minute ago”, “5 hours ago”, “3 days ago”, “7 months ago”, “2 years ago”, “in 3 days”. It rounds to the nearest whole unit.

brief(then, now) writes a date as a message list does, in the user’s time zone: the time for today (“14:32”), “Yesterday”, the day’s name for the past week (“Tue”), the day and month this year (“13 Oct”), and the year too for older dates (“13 Oct 2025”).

To change the shapes, take time.brief_format() and replace its fields (today, yesterday, week, year, older), then call b.apply(&then, &now) with two Zoned values:

tape
var b: time.Brief = time.brief_format();
b.yesterday = "eilen";
let then: time.Zoned = time.local(sent_ns);
let now: time.Zoned = time.local_now();
io.println(b.apply(&then, &now));

Changed in this release

The old time.nanos(), time.micros() and time.millis() (no argument, returning a clock reading) are gone: those names now make Durations. Use time.now() and Instant.elapsed():

tape
let start: time.Instant = time.now();   // was: let start: i64 = time.nanos();
let ns: i64 = start.elapsed().as_nanos();

Platforms

Windows, Linux and macOS have both clocks and the system zone (Windows’ time zone settings; on Linux and macOS TZ, else /etc/localtime). On Uusi the monotonic clock works in millisecond steps and the wall clock in whole seconds (the kernel’s real-time clock; on a kernel without it unix_nanos() is 0). Measure time on Uusi with now(), not with differences of unix_nanos(). Uusi has no time zones: local_offset is 0.

Last modified: