Synsemadocsv0.6.xENES

Biblioteca estándar

Tiempo y 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)

El reloj§

Solo dos builtins leen o esperan al reloj: now() (timestamp unix, un número) y sleep(segundos). Necesitan la capacidad time, que se auto-otorga bajo run/test (declarás require time bajo serve). Todo lo demás de esta página — construir, parsear, formatear, comparar y calcular fechas — es puro: sin capacidad, la misma respuesta en cada corrida.

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 y date_parts aceptan un timestamp unix pelado. Para trabajar de verdad con fechas, usá los tres tipos de abajo.

Fechas como tipos: date, datetime, duration§

TipoQué esCómo se construyeSe muestra como
dateun día civil — sin hora ni zonadate(2026, 1, 31), date("2026-01-31"), date(dt) (el día en la zona propia de dt)2026-01-31
datetimeun instante + su zonaver abajo2026-01-03T10:00:00Z, 2026-01-03T10:00:00-03:00, 2026-03-29T01:30:00+01:00[Europe/Madrid]
durationuna cantidad exacta de tiempoduration(days =, hours =, minutes =, seconds =, milliseconds =, weeks =) — todo por nombre, todo opcional, se admiten fraccionesISO 8601: PT1H30M, P29D, -PT1H

type_of devuelve "date", "datetime" o "duration". date(texto) lee solo YYYY-MM-DD (para otros formatos, parse_date); un día imposible (date(2026, 2, 30)) es un error. Un día dentro de una duration son exactamente 24 horas.

Todas las formas de construir un 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

Zonas§

Una zona es un nombre IANA ("Europe/Madrid", "America/Buenos_Aires"), "UTC", o un offset fijo ("+05:30", "-0300", "+05") — en cualquier lugar donde va una zona (datetime, to_timezone, parse_datetime). Una zona desconocida es un error que sugiere las dos formas.

Aritmética y comparación§

Períodos: truncate y date_range§

truncate(t, unidad) → el inicio del período: "year", "quarter", "month", "week" (lunes), "day", y en un datetime "hour", "minute", "second" — en la zona propia del valor. En un día sin medianoche (un salto de horario a las 00:00, como en Santiago), "day" da el primer instante que existe (01:00). Es la herramienta para agrupar por período: summarize(rows, (r) => truncate(r.when, "month"), {…}) — ver Análisis de datos.

date_range(inicio, fin, paso?) → una lista de inicio a fin, inclusive. paso es "day" (default), "week", "month", "quarter", "year", en datetimes también "hour", "minute", "second", o cualquier duration positiva. Los pasos de calendario conservan la hora local a través del horario de verano; horas, minutos, segundos y durations son tiempo transcurrido. Inicio y fin tienen que ser del mismo tipo; pedirle "hour" a un date es un error.

Parseo y formato§

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"

Aleatoriedad con semilla — rng(seed), pura§

rng(seed) devuelve un generador: bit a bit numpy.random.default_rng(seed) (SeedSequence + PCG64), así que la misma semilla da los números de numpy en cualquier plataforma. Es puro — sin capacidad, permitido bajo --deterministic, en sandbox y en tests — lo que lo vuelve la herramienta para splits train/test, simulaciones y muestras reproducibles.

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)

Aleatoriedad del SO — deny-by-default§

Sin generador, la aleatoriedad viene del sistema operativo y necesita require random — incluso bajo run. La aleatoriedad es de lo que están hechos los tokens y los nonces, así que es una capacidad, no algo gratis.

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

Ni random() ni rng sirven para secretos: tokens, nonces y salts pasan por random_bytes/token, que leen el CSPRNG del SO. Más en Login y sesiones.