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.timeImported 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.
Reading the clock
Section titled “Reading the clock”| Function | Description |
|---|---|
now_ns() -> int64 | Nanoseconds since the Unix epoch, UTC. |
now_ms() -> int64 | Milliseconds since the Unix epoch, UTC. |
now_unix() -> int64 | Seconds 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.
The Civil struct
Section titled “The Civil struct”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.
Converting
Section titled “Converting”| Function | Description |
|---|---|
civil_from_unix(secs: int64) -> Civil | The UTC calendar reading of a Unix timestamp, in whole seconds. |
unix_from_civil(c: Civil) -> int64 | The Unix timestamp, in whole seconds, of a UTC calendar reading. |
weekday(c: Civil) -> int64 | The day of the week, 0 for Sunday through 6 for Saturday. |
format_iso8601(c: Civil) -> string | Renders 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.
UTC only
Section titled “UTC only”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.
Why floor_div and floor_mod exist
Section titled “Why floor_div and floor_mod exist”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.
Out of range fields
Section titled “Out of range fields”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.
A checked example
Section titled “A checked example”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.
@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}See also
Section titled “See also”- Foreign functions: the boundary
now_nscrosses, and why the calendar stays on the Dusk side of it. - Operators: the truncating
/and%thatfloor_divandfloor_modwork around. - std.string: the
StringBuilderbehindformat_iso8601, and the rest of the text surface. - stdlib overview: the full module list and how imports resolve.