Skip to content

std.time

std.time is wall clock reads and a calendar over them, added in 1.4.1. One foreign call reads the clock; everything after that is ordinary Dusk over int64 seconds. You get the current time from now_ns, now_ms, or now_unix, turn a Unix timestamp into a Civil date with civil_from_unix, go back the other way with unix_from_civil, and render the result as ISO 8601 text with format_iso8601. Every value in the module is UTC. The module lives at lib/std/time.dusk.

@import std.time

Imported names are flat: after @import std.time you call now_unix, civil_from_unix, and the rest with no prefix. See stdlib overview for how imports work in general.

FunctionDescription
now_ns() -> int64Nanoseconds since the Unix epoch, UTC.
now_ms() -> int64Milliseconds since the Unix epoch, UTC.
now_unix() -> int64Seconds since the Unix epoch, UTC.

Only one of these actually crosses the boundary. now_ns binds cool_unix_now_ns, a C runtime shim over clock_gettime(CLOCK_REALTIME), and now_ms and now_unix are pure arithmetic on top of it, dividing by a million and a billion respectively. So the three share a single reading of the clock’s own resolution, and picking the coarser one costs you nothing but the precision you asked to drop.

clock_gettime(CLOCK_REALTIME) is the wall clock, not a monotonic counter. It tracks civil time, which means it can jump backward when the system clock is corrected. If you are timing a span of work rather than stamping an event, that is worth knowing: two now_ns reads can come back out of order across an adjustment.

Civil is a UTC calendar reading, one field per component, each a plain int64:

export struct Civil {
year: int64,
month: int64,
day: int64,
hour: int64,
minute: int64,
second: int64,
}

The year is proleptic Gregorian, so a year before 1 is zero or negative and the Gregorian rules run backward past the point history actually adopted them. Month lands in [1, 12], day in [1, the month's length], and hour, minute, and second in their ordinary ranges. There is no sub-second field; now_ns and now_ms carry finer precision on their own, and the calendar works in whole seconds.

FunctionDescription
civil_from_unix(secs: int64) -> CivilThe UTC calendar reading of a Unix timestamp, in whole seconds.
unix_from_civil(c: Civil) -> int64The Unix timestamp, in whole seconds, of a UTC calendar reading.
weekday(c: Civil) -> int64The day of the week, 0 for Sunday through 6 for Saturday.
format_iso8601(c: Civil) -> stringRenders c as YYYY-MM-DDTHH:MM:SSZ. The result is a fresh heap string you own.
parse_iso8601(s: string) -> (Civil, error)The strict inverse of format_iso8601.

civil_from_unix(0) is 1970-01-01T00:00:00, and a negative secs reads a date before the epoch. format_iso8601 pads every field, the year to four digits and the rest to two, and a year outside [0, 9999] still prints in full, just wider than four digits. The string comes back on the heap, so release it with the ordinary free when you are done with it.

None of this touches C. Dusk never reads a libc struct’s own layout, so there is no struct tm here and no call to gmtime. The conversion is Howard Hinnant’s day count arithmetic, days_from_civil and civil_from_days, ported to Dusk: it maps a year, month, and day triple to a count of days from the epoch and back with no lookup table at all, correct proleptically over the whole int64 range and exact through at least year 2400 in both directions. It was checked against Python’s own calendar for 200,000 random dates before the module shipped.

There is no time zone support in this module. Not a partial one, not a fixed offset you can pass, none at all. Every value std.time hands you is UTC, and the trailing Z in format_iso8601’s output is there to mark exactly that. If you want local time, you convert outside the module: read the offset from wherever your program gets it, add it to the seconds before you call civil_from_unix, and render the zone marker yourself rather than leaning on format_iso8601, which will always claim Z.

Dusk’s / and % truncate toward zero, the same rule C uses. That is the right behavior for most arithmetic and the wrong one for a calendar. Splitting a timestamp into a day count and a time of day means dividing by 86400, and for a timestamp before the epoch the dividend is negative: truncation would snap the quotient toward zero and leave a negative remainder, putting you on the wrong day with a nonsensical time of day inside it.

So the module carries floor_div and floor_mod internally, which land the quotient one lower and pull the remainder back into [0, b) when the division comes out negative. civil_from_unix(-1) reads 1969-12-31T23:59:59Z because of them, which is the second before the epoch, rather than something an hour ahead of it. Neither is exported; they are the module’s own plumbing, and you see them only in the fact that pre-epoch timestamps behave.

Hinnant’s algorithms themselves assume the truncating divide, which is what Dusk’s / and % already give, so the day count arithmetic uses the plain operators and only the seconds-to-days split reaches for the floor versions.

unix_from_civil does not validate its fields. A month past 12 or a day past its month’s length is not rejected, and there is no error to handle. The value rolls forward through the day count arithmetic instead: a Civil with month: 13 reads as January of the next year, and day: 32 in January reads as the first of February. This is how most civil calendar libraries treat an out of range field, as a relative offset rather than a fault, and it falls out of Hinnant’s arithmetic naturally rather than being bolted on.

That cuts both ways. It makes date math pleasant, since you can add a month to a Civil without normalizing it first, but it means a typo in a field never announces itself. If you need the fields validated, check them before you call.

The sample below pins its inputs to fixed timestamps so its output is exact. It calls now_unix, but only compares the result, never prints it, so nothing in the output moves between runs.

time_tour.dusk
@paradigm procedural
@import std.time
func main() -> int32 {
// A fixed timestamp, so the reading below is exact every run.
stamp: int64 = 1000000000
c: Civil = civil_from_unix(stamp)
println("{}-{}-{}", c.year, c.month, c.day) // 2001-9-9
// format_iso8601 pads every field and marks UTC with the trailing Z. The
// string is a fresh heap allocation you own.
iso: string = format_iso8601(c)
println(iso) // 2001-09-09T01:46:40Z
free(iso)
// The round trip back through unix_from_civil lands on the same second.
if unix_from_civil(c) == stamp {
println("round trip ok") // round trip ok
}
// floor_mod keeps a pre-epoch timestamp honest: -1 is the second before
// the epoch, not the second after it.
before: string = format_iso8601(civil_from_unix(-1))
println(before) // 1969-12-31T23:59:59Z
free(before)
// now_unix reads the wall clock, so its value is never printed here. Any
// real run happens after the timestamp above.
if now_unix() > stamp {
println("the clock has moved on") // the clock has moved on
}
return 0
}
  • Foreign functions: the boundary now_ns crosses, and why the calendar stays on the Dusk side of it.
  • Operators: the truncating / and % that floor_div and floor_mod work around.
  • std.string: the StringBuilder behind format_iso8601, and the rest of the text surface.
  • stdlib overview: the full module list and how imports resolve.