Synsemadocsv0.6.xENES

Standard library / primitives

Time & random

time-random.syn
-- Doc example: dates as types (date / datetime / duration, IANA zones, DST), formatting and parsing,
-- and randomness: seeded generators (pure, numpy's numbers) and OS randomness (`require random`).
intent: "doc example: time and random"
require random                 -- only for random() / random_int(lo, hi) without a generator

let g be rng(42)
print("rng(42)() = " + text(g()) + ",  Madrid 01:30 + 1h = " + text(datetime("2026-03-29T01:30:00", "Europe/Madrid") + duration(hours = 1)))

test "format_time / parse_time: unix timestamps, pure (no clock), date-only = midnight UTC"
    assert_eq(format_time(0), "1970-01-01T00:00:00Z")
    assert_eq(format_time(0, "%Y-%m-%d %H:%M"), "1970-01-01 00:00")
    assert_eq(parse_time("1970-01-01T00:00:00Z"), 0)
    assert_eq(parse_time("2026-01-03", "%Y-%m-%d"), 1767398400.0)
    assert_eq(date_parts(0).year, 1970)
    assert(now() > 1000000000)                      -- now() reads the clock: `time`, auto-granted under run/test

test "three types: date, datetime, duration"
    assert_eq(type_of(date(2026, 1, 31)), "date")
    assert_eq(date("2026-01-31"), date(2026, 1, 31))
    assert_eq(type_of(datetime("2026-01-03T10:00:00Z")), "datetime")
    assert_eq(type_of(duration(hours = 1)), "duration")
    assert_eq(text(date(2026, 1, 31)), "2026-01-31")
    assert_eq(text(datetime("2026-01-03T10:00:00Z")), "2026-01-03T10:00:00Z")
    assert_eq(text(datetime(2026, 1, 3, 10, 0, 0, "UTC")), "2026-01-03T10:00:00Z")
    assert_eq(text(datetime("2026-01-03")), "2026-01-03T00:00:00Z")          -- a bare date is midnight UTC
    assert_eq(text(datetime(0)), "1970-01-01T00:00:00Z")                     -- from unix seconds
    assert_eq(text(datetime(1767434400)), "2026-01-03T10:00:00Z")
    assert_eq(text(duration(hours = 1, minutes = 30)), "PT1H30M")
    assert_eq(text(duration(hours = 1.5)), "PT1H30M")
    assert_eq(text(duration(weeks = 1)), "P7D")
    assert_eq(text(duration(days = 1)), "P1D")
    assert_eq(text(duration(seconds = 90)), "PT1M30S")
    assert_eq(text(duration(milliseconds = 5)), "PT0.005S")
    assert_error(() => date(2026, 2, 30))                                    -- not a calendar date
    assert_error(() => date("31/01/2026"))                                   -- date(text) is ISO only: parse_date for others

test "zones: IANA names follow DST, fixed offsets are kept"
    let madrid be datetime("2026-03-29T01:30:00", "Europe/Madrid")          -- local time + zone
    assert_eq(text(madrid), "2026-03-29T01:30:00+01:00[Europe/Madrid]")
    assert_eq(text(datetime("2026-01-03T10:00:00-03:00")), "2026-01-03T10:00:00-03:00")
    assert_eq(text(datetime("2026-09-24T02:00:00+0530")), "2026-09-24T02:00:00+05:30")
    assert_eq(date_parts(datetime("2026-09-24T02:00:00+05:30")).hour, 2)
    assert_eq(date_parts(datetime("2026-09-24T02:00:00+05:30")).zone, "+05:30")
    assert_eq(text(datetime("2026-01-03T10:00:00[Europe/Madrid]")), "2026-01-03T10:00:00+01:00[Europe/Madrid]")
    assert_error(() => datetime("2026-01-03T10:00:00+05:30[Europe/Madrid]")) -- offset and zone disagree
    assert_eq(text(datetime("2026-01-03T10:00:00Z", "America/Sao_Paulo")), "2026-01-03T07:00:00-03:00[America/Sao_Paulo]")
    assert_eq(text(datetime(date(2026, 1, 31), "Europe/Madrid")), "2026-01-31T00:00:00+01:00[Europe/Madrid]")
    assert_eq(text(datetime(0, "Asia/Kolkata")), "1970-01-01T05:30:00+05:30[Asia/Kolkata]")
    assert_eq(date(datetime("2026-01-03T23:30:00-03:00")), date(2026, 1, 3))  -- the day in its own zone
    assert_error(() => datetime("2026-03-29T02:30:00", "Europe/Madrid"))     -- falls in the DST gap
    assert_eq(text(datetime("2026-10-25T02:30:00", "Europe/Madrid")), "2026-10-25T02:30:00+02:00[Europe/Madrid]")  -- repeated hour: the first
    assert_error(() => datetime("2026-01-03T10:00:00Z", "Mars/Olympus"))     -- unknown zone
    assert_eq(text(to_timezone(datetime("2026-01-03T10:00:00Z"), "-0300")), "2026-01-03T07:00:00-03:00")
    assert_eq(text(to_timezone(datetime("2026-01-03T10:00:00Z"), "+05")), "2026-01-03T15:00:00+05:00")
    let parts be date_parts(datetime("2026-03-29T01:30:00", "Europe/Madrid"))
    assert_eq([parts.year, parts.month, parts.day, parts.hour, parts.minute, parts.second, parts.weekday, parts.yearday, parts.zone], [2026, 3, 29, 1, 30, 0, 7, 88, "Europe/Madrid"])
    assert_eq(text(to_timezone(datetime("2026-01-03T10:00:00Z"), "Asia/Kolkata")), "2026-01-03T15:30:00+05:30[Asia/Kolkata]")
    assert_eq(text(to_timezone(datetime("2026-01-03T10:00:00Z"), "-03:00")), "2026-01-03T07:00:00-03:00")

test "arithmetic: elapsed time vs calendar steps"
    assert_eq(text(datetime("2026-03-29T01:30:00", "Europe/Madrid") + duration(hours = 1)), "2026-03-29T03:30:00+02:00[Europe/Madrid]")
    assert_eq(date("2026-03-01") - date(2026, 1, 31), duration(days = 29))
    assert_eq(datetime("2026-01-01T12:00:00Z") - datetime("2026-01-01T09:00:00Z"), duration(hours = 3))
    assert_eq(date(2026, 1, 31) + duration(days = 1), date(2026, 2, 1))
    assert_error(() => date(2026, 1, 1) + duration(hours = 1))               -- a date moves by whole days
    assert_eq(duration(hours = 3) / duration(minutes = 30), 6.0)
    assert_eq(duration(hours = 1) * 2, duration(hours = 2))
    assert_eq(duration(hours = 1) / 4, duration(minutes = 15))
    assert_eq(text(duration(hours = 1) - duration(hours = 2)), "-PT1H")
    let noon be datetime("2026-03-28T12:00:00", "Europe/Madrid")
    assert_eq(text(add_days(noon, 1)), "2026-03-29T12:00:00+02:00[Europe/Madrid]")             -- same local time
    assert_eq(text(noon + duration(days = 1)), "2026-03-29T13:00:00+02:00[Europe/Madrid]")     -- exactly 24 h
    assert_eq(text(add_days(datetime("2026-03-28T02:30:00", "Europe/Madrid"), 1)), "2026-03-29T03:30:00+02:00[Europe/Madrid]")  -- lands in the gap: moved forward
    assert_eq(add_days(date(2026, 1, 31), 1), date(2026, 2, 1))
    assert_eq(add_months(date(2026, 1, 31), 1), date(2026, 2, 28))           -- clamps to the month's end
    assert_eq(add_months(date(2024, 1, 31), 1), date(2024, 2, 29))
    assert_eq(add_months(date(2026, 3, 31), -1), date(2026, 2, 28))

test "comparison is by instant; a date is not a datetime"
    assert(datetime("2026-01-01T12:00:00Z") == datetime("2026-01-01T09:00:00-03:00"))
    assert(date(2026, 1, 1) < date(2026, 1, 2))
    assert_error(() => date(2026, 1, 1) < datetime("2026-01-01T00:00:00Z"))
    assert_eq(min([date(2026, 2, 1), date(2026, 1, 1)]), date(2026, 1, 1))
    assert_eq(sort([duration(hours = 2), duration(minutes = 1)]), [duration(minutes = 1), duration(hours = 2)])
    assert_eq(max([datetime("2026-01-01T12:00:00Z"), datetime("2026-01-01T10:00:00-03:00")]), datetime("2026-01-01T13:00:00Z"))
    assert_eq(sort_by([{"d": date(2026, 2, 1)}, {"d": date(2026, 1, 1)}], (r) => r.d)[0].d, date(2026, 1, 1))

test "truncate: the start of a period (group by month, week...)"
    let t be datetime("2026-05-14T15:42:10", "Europe/Madrid")
    assert_eq(text(truncate(t, "month")), "2026-05-01T00:00:00+02:00[Europe/Madrid]")
    assert_eq(truncate(date(2026, 5, 14), "year"), date(2026, 1, 1))
    assert_eq(truncate(date(2026, 5, 14), "quarter"), date(2026, 4, 1))
    assert_eq(truncate(date(2026, 5, 14), "week"), date(2026, 5, 11))        -- Monday
    assert_eq(truncate(date(2026, 5, 14), "day"), date(2026, 5, 14))
    let u be datetime("2026-05-14T15:42:10Z")
    assert_eq(text(truncate(u, "hour")), "2026-05-14T15:00:00Z")
    assert_eq(text(truncate(u, "minute")), "2026-05-14T15:42:00Z")
    assert_eq(text(truncate(datetime("2026-05-14T15:42:10.5Z"), "second")), "2026-05-14T15:42:10Z")
    -- a day without midnight (DST jump at 00:00) starts when it starts
    assert_eq(text(truncate(datetime("2024-09-08T15:00:00", "America/Santiago"), "day")), "2024-09-08T01:00:00-03:00[America/Santiago]")

test "date_range: inclusive, by calendar unit or by elapsed time"
    assert_eq(date_range(date(2026, 1, 1), date(2026, 1, 3)), [date(2026, 1, 1), date(2026, 1, 2), date(2026, 1, 3)])
    assert_eq(date_range(date(2026, 1, 1), date(2026, 3, 1), "month"), [date(2026, 1, 1), date(2026, 2, 1), date(2026, 3, 1)])
    assert_eq(length(date_range(date(2026, 1, 1), date(2026, 1, 15), "week")), 3)
    assert_eq(length(date_range(date(2026, 1, 1), date(2027, 1, 1), "quarter")), 5)
    assert_eq(length(date_range(date(2020, 1, 1), date(2026, 1, 1), "year")), 7)
    let hours be date_range(datetime("2026-01-01T00:00:00Z"), datetime("2026-01-01T02:00:00Z"), "hour")
    assert_eq(length(hours), 3)
    assert_eq(length(date_range(datetime("2026-01-01T00:00:00Z"), datetime("2026-01-01T00:02:00Z"), "minute")), 3)
    assert_eq(length(date_range(datetime("2026-01-01T00:00:00Z"), datetime("2026-01-01T00:00:02Z"), "second")), 3)
    assert_eq(length(date_range(datetime("2026-01-01T00:00:00Z"), datetime("2026-01-01T01:00:00Z"), duration(minutes = 20))), 4)
    let days be date_range(datetime("2026-03-28T12:00:00", "Europe/Madrid"), datetime("2026-03-30T12:00:00", "Europe/Madrid"))
    assert_eq(text(days[1]), "2026-03-29T12:00:00+02:00[Europe/Madrid]")    -- the local time survives DST
    assert_error(() => date_range(date(2026, 1, 1), date(2026, 1, 2), "hour"))   -- a date has no hours
    assert_error(() => date_range(date(2026, 1, 1), datetime("2026-01-02T00:00:00Z")))   -- one type for both ends

test "timestamp and in_units"
    assert_eq(timestamp(datetime("1970-01-02T00:00:00Z")), 86400.0)
    assert_error(() => timestamp(date(2026, 1, 1)))                          -- a datetime only
    assert_eq(in_units(duration(hours = 1, minutes = 30), "minutes"), 90.0)
    assert_eq(in_units(duration(days = 14), "weeks"), 2.0)
    assert_eq(in_units(duration(days = 1), "days"), 1.0)
    assert_eq(in_units(duration(hours = 1), "hours"), 1.0)
    assert_eq(in_units(duration(minutes = 1), "seconds"), 60.0)
    assert_eq(in_units(duration(seconds = 1), "milliseconds"), 1000.0)

test "parse_date / parse_datetime / format_time"
    assert_eq(parse_date("31/01/2026", "%d/%m/%Y"), date(2026, 1, 31))
    assert_eq(parse_date("2026-01-31"), date(2026, 1, 31))
    assert_error(() => parse_date("2026/01/31", "%d/%m/%Y"))
    assert_eq(text(parse_datetime("2026-01-03T10:00:00Z")), "2026-01-03T10:00:00Z")
    assert_eq(text(parse_datetime("24/09/2026 02:00 +0530", "%d/%m/%Y %H:%M %z")), "2026-09-24T02:00:00+05:30")
    assert_eq(text(parse_datetime("24/09/2026 02:00 +0530", "%d/%m/%Y %H:%M %z", "UTC")), "2026-09-23T20:30:00Z")
    assert_eq(text(parse_datetime("24/09/2026 02:00", "%d/%m/%Y %H:%M", "Europe/Madrid")), "2026-09-24T02:00:00+02:00[Europe/Madrid]")
    assert_eq(text(parse_datetime("03/01/2026", "%d/%m/%Y")), "2026-01-03T00:00:00Z")                  -- date-only: midnight
    assert_eq(text(parse_datetime("03/01/2026", "%d/%m/%Y", "Europe/Madrid")), "2026-01-03T00:00:00+01:00[Europe/Madrid]")
    -- %Z: only UTC and GMT identify a zone; any other abbreviation is an error
    assert_eq(text(parse_datetime("Thu, 24 Sep 2026 10:00:00 GMT", "%a, %d %b %Y %H:%M:%S %Z")), "2026-09-24T10:00:00Z")
    assert_eq(text(parse_datetime("24/09/2026 10:00 utc", "%d/%m/%Y %H:%M %Z")), "2026-09-24T10:00:00Z")
    assert_error(() => parse_datetime("24/09/2026 10:00 IST", "%d/%m/%Y %H:%M %Z"))
    assert_eq(text(parse_datetime("24/09/2026 10:00 +0530 IST", "%d/%m/%Y %H:%M %z %Z")), "2026-09-24T10:00:00+05:30")  -- the offset decides
    assert_eq(text(parse_datetime("24/09/2026 10:00", "%d/%m/%Y %H:%M", "Asia/Kolkata")), "2026-09-24T10:00:00+05:30[Asia/Kolkata]")
    assert_eq(format_time(date(2026, 1, 31), "%d/%m/%Y"), "31/01/2026")
    assert_eq(format_time(datetime("2026-03-29T01:30:00", "Europe/Madrid"), "%H:%M %z"), "01:30 +0100")   -- in its own zone
    assert_eq(format_time(datetime("2026-01-03T10:00:00Z")), "2026-01-03T10:00:00Z")
    assert_error(() => format_time(date(2026, 1, 31), "%H"))                 -- a date has no hours
    assert_error(() => format_time(0, "%Q"))                                 -- not a strftime specifier
    assert_eq(date_parts(date(2026, 1, 31)), {"year": 2026, "month": 1, "day": 31, "weekday": 6, "yearday": 31})

test "rng(seed): numpy's default_rng, bit for bit — pure, no capability"
    assert_eq(rng(42)(), 0.7739560485559633)                                 -- np.random.default_rng(42).random()
    let g be rng(42)
    assert_eq(random(g), 0.7739560485559633)                                 -- random(g) is g()
    assert_eq(random(g), 0.4388784397520523)
    let d be random_int(g, 1, 6)                                             -- inclusive: integers(1, 7)
    assert(d >= 1 and d <= 6)
    assert_eq(is_integer(d), true)
    let x be random_normal(g, mean = 10, std = 2)
    assert_eq(type_of(x), "number")
    assert_eq(type_of(random_normal(g)), "number")                           -- mean 0, std 1
    assert_error(() => random_normal(g, std = -1))
    assert_error(() => random_int(g, 1.5, 6))                                -- bounds are integers
    assert_error(() => rng(-1))

test "same seed, same sequence; shuffle, choice, sample, rng_spawn"
    assert_eq(shuffle(rng(0), ["a", "b", "c"]), shuffle(rng(0), ["a", "b", "c"]))
    assert_eq(sort(shuffle(rng(1), [3, 1, 2])), [1, 2, 3])                   -- a NEW list, same items
    assert(choice(rng(1), ["a", "b", "c"]) in ["a", "b", "c"])
    let picked be sample(rng(1), range(0, 100), 5)
    assert_eq(length(unique(picked)), 5)                                     -- distinct
    assert_error(() => sample(rng(1), [1, 2], 3))
    assert_error(() => choice(rng(1), []))
    let kids be rng_spawn(rng(42), 2)
    assert_eq(length(kids), 2)
    assert_eq(kids[0](), 0.9167441575549085)                                 -- np default_rng(42).spawn(2)[0].random()
    assert(kids[0]() != kids[1]())

test "a generator is a process: an alias shares the sequence"
    let a be rng(7)
    let b be a
    let first be a()
    assert(b() != first)                                                     -- b continued a's sequence
    assert_eq(rng(7)(), first)

test "parallel_map: one child per item with rng_spawn; the same generator twice is an error"
    let draws be parallel_map((c) => c(), rng_spawn(rng(42), 3))
    assert_eq(draws[0], 0.9167441575549085)
    let c be rng(3)
    assert_error(() => parallel_map((x) => x(), [c, c]))
    assert_error(() => parallel_map((x) => g(), [1, 2]))                     -- g was created at the top level
    assert_error(() => json_encode({"g": rng(1)}))                            -- a generator is not data

test "random() / random_int(lo, hi) without a generator: OS entropy, `require random`"
    let r be random()
    assert(r >= 0 and r < 1)
    let d be random_int(1, 6)
    assert(d >= 1 and d <= 6)

The clock§

Only two builtins read or wait on the clock: now() (unix timestamp, a number) and sleep(seconds). They need the time capability, which is auto-granted under run/test (declare require time under serve). Everything else on this page — building, parsing, formatting, comparing and computing dates — is pure: no capability, the same answer every run.

now()                               -- unix timestamp (number) — needs time
sleep(0.5)                          -- pause — needs time
format_time(0)                      -- "1970-01-01T00:00:00Z" — ISO 8601 by default
format_time(0, "%Y-%m-%d %H:%M")    -- strftime pattern
parse_time("2026-06-30T12:00:00Z")  -- inverse of format_time: unix seconds (float)
parse_time("2026-01-03", "%Y-%m-%d")   -- a date-only pattern is midnight UTC
date_parts(0)                       -- {year, month, day, hour, minute, second, ...}

format_time, parse_time and date_parts accept a plain unix timestamp. For real date work, use the three types below.

Dates as types: date, datetime, duration§

TypeWhat it isBuild itShows as
datea civil day — no time, no zonedate(2026, 1, 31), date("2026-01-31"), date(dt) (the day in dt's own zone)2026-01-31
datetimean instant + its zonesee below2026-01-03T10:00:00Z, 2026-01-03T10:00:00-03:00, 2026-03-29T01:30:00+01:00[Europe/Madrid]
durationan exact amount of timeduration(days =, hours =, minutes =, seconds =, milliseconds =, weeks =) — all by name, all optional, fractions allowedISO 8601: PT1H30M, P29D, -PT1H

type_of returns "date", "datetime" or "duration". date(text) reads only YYYY-MM-DD (for other formats, parse_date); an impossible day (date(2026, 2, 30)) is an error. A day inside a duration is exactly 24 hours.

Every way to build a datetime:

datetime("2026-01-03T10:00:00Z")                   -- UTC
datetime("2026-01-03T10:00:00-03:00")              -- keeps its offset (+0530 without the colon reads too)
datetime("2026-03-29T01:30:00", "Europe/Madrid")   -- local time in an IANA zone
datetime("2026-01-03T10:00:00[Europe/Madrid]")     -- RFC 9557: local time in that zone
datetime("2026-01-03T10:00:00Z", "America/Sao_Paulo")   -- a text with an offset + a zone: the same instant, converted
datetime("2026-01-03")                             -- midnight UTC
datetime(2026, 1, 3, 10, 0, 0, "UTC")              -- hour/minute/second optional, zone last (default UTC)
datetime(1767434400)                               -- from unix seconds (a zone may follow)
datetime(date(2026, 1, 31), "Europe/Madrid")       -- midnight of that day in that zone

Zones§

A zone is an IANA name ("Europe/Madrid", "America/Buenos_Aires"), "UTC", or a fixed offset ("+05:30", "-0300", "+05") — anywhere a zone goes (datetime, to_timezone, parse_datetime). An unknown zone is an error that suggests both forms.

Arithmetic and comparison§

Periods: truncate and date_range§

truncate(t, unit) → the start of the period: "year", "quarter", "month", "week" (Monday), "day", and on a datetime "hour", "minute", "second" — in the value's own zone. On a day with no midnight (a DST jump at 00:00, as in Santiago), "day" gives the first instant that exists (01:00). It is the tool for grouping by period: summarize(rows, (r) => truncate(r.when, "month"), {…}) — see Data analysis.

date_range(start, end, step?) → a list from start to end, inclusive. step is "day" (default), "week", "month", "quarter", "year", on datetimes also "hour", "minute", "second", or any positive duration. Calendar steps keep the local time across DST; hours, minutes, seconds and durations are elapsed time. Start and end must be the same type; asking a date for "hour" is an error.

Parsing and formatting§

parse_date("31/01/2026", "%d/%m/%Y")                              -- 2026-01-31
parse_datetime("24/09/2026 02:00 +0530", "%d/%m/%Y %H:%M %z")     -- keeps +05:30
parse_datetime("24/09/2026 02:00", "%d/%m/%Y %H:%M", "Europe/Madrid")   -- no offset: local time in that zone
parse_datetime("03/01/2026", "%d/%m/%Y")                          -- date-only: 2026-01-03T00:00:00Z
format_time(date(2026, 1, 31), "%d/%m/%Y")                        -- "31/01/2026"

Seeded randomness — rng(seed), pure§

rng(seed) returns a generator: bit for bit numpy.random.default_rng(seed) (SeedSequence + PCG64), so the same seed gives numpy's numbers on every platform. It is pure — no capability, allowed under --deterministic, in sandbox and in tests — which makes it the tool for train/test splits, simulations and reproducible samples.

let g be rng(42)
g()                                     -- 0.7739560485559633, like np.random.default_rng(42).random()
random(g)                               -- the same as g()
random_int(g, 1, 6)                     -- inclusive: g.integers(1, 7)
random_normal(g, mean = 0, std = 1)     -- g.normal(0, 1); mean and std by name, optional
shuffle(g, [1, 2, 3])                   -- a NEW list: g.permutation(xs)
choice(g, ["a", "b", "c"])              -- one item: g.choice(xs)
sample(g, range(0, 100), 5)             -- 5 distinct items: g.choice(xs, 5, replace=False)
rng_spawn(g, 4)                         -- 4 independent generators: g.spawn(4)

OS randomness — deny-by-default§

Without a generator, randomness comes from the operating system and needs require random — even under run. Randomness is what tokens and nonces are made of, so it is a capability, not a freebie.

require random
random()            -- float in [0, 1)
random_int(1, 6)    -- integer in [min, max], integer bounds
random_bytes(16)    -- n bytes from the OS CSPRNG
token()             -- 32 CSPRNG bytes as base64url — session ids, CSRF tokens

Neither random() nor rng is for secrets: tokens, nonces and salts go through random_bytes/token, which read the OS CSPRNG. More in Login & sessions.