std.time

Getting the current time

now()            # milliseconds since Unix epoch
timestamp()      # seconds since Unix epoch
date()           # {year, month, day, hour, minute, second, weekday, yearday}
format(ms[, fmt])   # default: %Y-%m-%d %H:%M:%S
parse(str, fmt)    # date string -> milliseconds (C++ chrono specifiers)
elapsed(start_ms)  # ms elapsed since start_ms
var t = std.time.now()
print(std.time.format(t))          # 2026-08-31 12:00:00
var d = std.time.date()
print(d.year, d.month, d.day)

date() fields: month is 1-based (1–12), weekday counts from Sunday
(0 = Sunday … 6 = Saturday), yearday counts from zero (0 = Jan 1).

Unit conversion

nanoseconds(n)  microseconds(n)  milliseconds(n)
seconds(n)      minutes(n)  hours(n)  days(n)
to_seconds(ms)  to_minutes(ms)  to_hours(ms)  to_days(ms)

The * (n) helpers convert a number of that unit to milliseconds; the
to_* (ms) helpers convert milliseconds to that unit (integer division —
results truncate toward zero).

std.time.seconds(90)     # 90000 (ms)
std.time.to_minutes(120000)  # 2

Calendar

calendar([year][, month]) returns an object describing a month for planning
and display. Omitting both gives the current month (today set to today’s
day-of-month); passing only a year keeps the current month:

var cal = std.time.calendar(2026, 3)

The object has these fields:

Field Meaning
year, month The requested year and 1-based month
month_name e.g. "March"
days_in_month Number of days (handles leap years)
first_weekday Weekday of the 1st, Monday = 1 … Sunday = 7
today Day-of-month when the requested month is the current one, else nil
weeks Rows of day numbers, nil-padded to seven slots
print(cal.month_name, cal.year)   # March 2026
print(cal.first_weekday)          # 7 (1st is a Sunday)
print(cal.weeks[0])               # [nil, nil, nil, nil, nil, nil, 1]

weeks lays out the month as Monday-first rows, padding with nil before the
1st and after the last day:

# March 2026, first_weekday = 7
[
  [nil, nil, nil, nil, nil, nil, 1],
  [2, 3, 4, 5, 6, 7, 8],
  [9, 10, 11, 12, 13, 14, 15],
  [16, 17, 18, 19, 20, 21, 22],
  [23, 24, 25, 26, 27, 28, 29],
  [30, 31, nil, nil, nil, nil, nil]
]

Render it into a terminal grid by mapping each week’s nil padding to blanks:

var cal = std.time.calendar(2026, 3)
print(cal.month_name, cal.year)
print("Mo Tu We Th Fr Sa Su")
for (week : cal.weeks) {
  var line = ""
  for (d : week) {
    if (d == nil) {
      line += "   "
    } else {
      line += (d < 10) ? " " + d + " " : d + " "
    }
  }
  print(line)
}

This prints:

March 2026
Mo Tu We Th Fr Sa Su
                   1
 2  3  4  5  6  7  8
 9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31

No arguments means the current month, and today is then set to today’s
day-of-month (so you can highlight it in the grid).

Timers

timer({callback, duration[, repeat[, start_now]][, ...args]}) schedules a
callback through the async scheduler — callback and duration (in
milliseconds) are required, repeat defaults to false, and any extra
arguments are forwarded to the callback. start_now (default true) pauses
the timer when false until its handle’s start() runs:

var t = std.time.timer({
  callback: fn() { print("tick") },
  duration: 1000,      # ms
  repeat: true
})
  • Non-repeating timers return a future — await it for the result.
  • Repeating timers return a handle with cancel() / start() / last_error().

Getting the last error

Repeating-timer callbacks run on background threads with no caller to receive
an exception, so each timer records the most recent callback failure on its
own handle and exposes it through handle.last_error() (returns nil
when there is none). It is echoed to stderr as well, so it is visible even if
you never query it.

var t = std.time.timer({
  callback: fn() { does_not_exist },
  duration: 20,
  repeat: true
})

std.async.sleep(80)              # keep the script alive so the tick can fire: exiting would stop the timer with it
var err = t.last_error()  # "Unknown variable: does_not_exist"
if (err != nil) {
  print("timer failed:", err)
}

Note that last_error() is for observing from outside the callback.
Inside the callback you share the engine’s globals, so you can record whatever
you need directly — set a flag, append to a log array, stash the details:

var failures = []
var t2 = std.time.timer({
  callback: fn() {
    failures += "tick failed"   # visible immediately, no last_error() needed
  },
  duration: 20,
  repeat: true
})

Which surface an error takes depends on the timer kind:

Timer kind Where the error goes
Non-repeating The returned future — re-thrown when you await() it (as Async error: <msg>)
Repeating Recorded on that timer’s handle via handle.last_error(); each tick keeps running
# Non-repeating: the error surfaces on await, not last_error()
var f = std.time.timer({
  callback: fn() { does_not_exist },
  duration: 10
})
std.async.await(f)   # RUNTIME_ERROR: Async error: Unknown variable: does_not_exist

last_error() is not cleared when read and holds only the most recent
message, so a repeating timer’s latest failure overwrites its previous one.
Each timer keeps its own slot — one timer failing does not mask another’s error.