SCUA

Manual

Dates and times

SCUA has two layers of time. The low-level clock gives you raw milliseconds; the time module turns those into civil dates you can read, format, and parse.

  • now() returns the current time as milliseconds since the Unix epoch (an int). It is frozen within a turn — every now() in the same turn returns the same value — which keeps programs deterministic and replayable.
  • dt() returns the milliseconds elapsed since the previous turn (handy for per-frame updates).
  • Duration literals are just ints in milliseconds: 5s, 200ms, 2min, 1h. So now() + 5s is ordinary arithmetic — no special type.

That's enough for timers and elapsed-time logic. For calendar work — "what day is this?", "format this for a report", "parse this date string" — use the time module and the datetime value.

#The datetime value

A datetime is an absolute instant plus the UTC offset it's read at. It prints as ISO-8601:

import time

let t = time.now()              -- the current instant, at the configured timezone (see below)
print(t)                        -- e.g. 2026-06-29T14:05:09.123Z
print(type(t))                  -- datetime

The instant and the offset are separate ideas: the same moment can be expressed at different offsets. now() (the int) is offset-free absolute time; a datetime carries an offset so it knows what local wall-clock reading it represents.

#Building a datetime

let a = time.now()                                 -- now, at the configured offset
let b = time.at(1_782_741_909_123)                 -- wrap a specific epoch-ms instant
let c = time.of(2026, 6, 29, 14, 5, 9)             -- year, month, day, [hour, minute, second, milli]
let d = time.of(2026, 6, 29, 14, 0, 0, 0, 120)     -- last arg = UTC offset in minutes (+02:00)

time.of only requires year/month/day; the rest default to zero, and the offset defaults to the program's configured timezone. Out-of-range fields normalize rather than fail — time.of(2026, 13, 1) is January 2027, time.of(2026, 1, 32) is February 1 — so date arithmetic is easy and never crashes.

#Reading the fields

Everything you do to a datetime is a method on it (dt.year()); the time module is just the constructors and the duration helpers. Read fields individually, or grab them all with .parts():

print(`{t.year()}-{t.month()}-{t.day()}`)
print(`{t.hour()}:{t.minute()}:{t.second()}.{t.millisecond()}`)
print(`weekday {t.weekday()}`)    -- ISO: 1 = Monday … 7 = Sunday
print(`day {t.year_day()} of the year`)

let p = t.parts()
-- p = { year, month, day, hour, minute, second, millisecond, weekday, year_day, offset_min }

let ms  = t.to_ms()               -- back to absolute epoch milliseconds
let off = t.offset()              -- the offset (minutes) it's expressed at

t.with_offset(minutes) re-expresses the same instant at a different offset (the wall-clock reading changes; the moment doesn't):

let utc = t.with_offset(0)
print(utc.to_ms() == t.to_ms())   -- true — same instant

#Formatting

t.iso() gives the canonical ISO-8601 string. For anything else, t.format(pattern) uses a small token language — a token is a run of one letter; any other character is literal:

Token Means Example
YYYY / YY year 2026 / 26
M / MM month 6 / 06
D / DD day 9 / 09
H / HH hour (24) 14
h / hh hour (12) 2 / 02
mm minute 05
ss second 09
SSS millisecond 123
Z / ZZ offset +0200 / +02:00 (or Z at UTC)
print(t.format("YYYY-MM-DD HH:mm:ss"))   -- 2026-06-29 14:05:09
print(t.format("D/M/YYYY"))              -- 29/6/2026
print(t.iso())                           -- 2026-06-29T14:05:09.123+02:00

Month and weekday names (MMMM, dddd) and AM/PM (A) aren't part of dt.format() — using one is an error today, so stick to the numeric tokens here. For a locale-aware month name (1 de febrero de 2023), reach for the render layer's @fmt instead — see Rendering and localization.

#Parsing

time.parse(s) reads an ISO-8601 date or date-time and returns an outcomeOk(datetime) or Error(reason) — so bad input is a value you handle, not a crash:

match time.parse("2026-06-29T12:00:00Z")
  Ok(t)    -> print(t.format("YYYY-MM-DD"))
  Error(e) -> print(`bad date: {e}`)
end

It accepts 2026-06-29 (date only → midnight), and 2026-06-29T12:00:00 with an optional .123 fraction, T or a space separator, and an optional zone (Z, +02:00, +0200). If the string carries a zone it wins; otherwise the reading is taken at the offset you pass (or the configured timezone).

#Durations

Durations are plain millisecond ints. time.add and time.diff move between datetimes and millisecond gaps; the rest format and parse spans:

let later = time.add(t, 90min)          -- advance by a duration
let gap   = time.diff(later, t)         -- 5400000 (ms between two instants)

print(time.humanize(gap))               -- "1h 30m"
let parts = time.duration_parts(gap)    -- { days, hours, minutes, seconds, milliseconds }

match time.parse_duration("2h5m")       -- "2h5m" → 7500000 ms
  Ok(ms)   -> print(time.humanize(ms))  -- "2h 5m"
  Error(e) -> print(e)
end

time.parse_duration understands ms, s, m (or min), h, and d.

#What this layer is not

To stay small and fully deterministic, the time module is a fixed-offset proleptic-Gregorian calendar. It does not include:

  • Daylight saving time or IANA zones (Europe/Berlin). You work with a fixed UTC offset; see Set the locale and timezone.
  • Leap seconds — a day is exactly 86,400,000 ms.
  • Localized textdt.format() itself is locale-neutral; for a localized month name and locale-specific ordering, use the render layer's @fmt (Rendering and localization). Localized weekday names are still pending.

See examples/time.scua for a runnable tour, and Set the locale and timezone for where the default offset comes from and how to override it for testing.