Time
Also known as: clock date datetime calendar timezone time zone tz iana utc offset daylight saving dst sleep timer interval cron duration parse iso 8601 rfc 3339 timestamp from string
● Intermediate8 minBefore this: For LoopSpec 14.4
std.time tells the time, measures how long something takes, waits, repeats work at an interval, works with dates and time zones, and reads and writes times as text.
use std.time
fn main() {
const start = time.monotonic_ms()
time.sleep(200.ms)
const took = time.monotonic_ms() - start
print("took at least 200 ms: {took >= 200}")
print("a date after 2026 began: {time.now_ms() > 1_767_225_600_000}")
}The program ran and printed:
kavach run measure.kav
took at least 200 ms: truea date after 2026 began: true
time.now_ms()is the time of day, in milliseconds since 1970. The clock can be changed, so don't measure with it.time.monotonic_ms()never goes back: subtract two readings to measure a duration.
Every few moments
use std.time
fn main() {
for tick in time.every_ms(100) {
print("tick {tick}")
if tick == 3 {
break
}
}
}The program ran and printed:
kavach run every.kav
tick 0tick 1tick 2tick 3
Each round starts 100 ms after the one before. A round that takes longer makes the next start at once: rounds are never skipped or doubled.
Durations
use std.time
fn main() {
const backup = 2.days
print(backup.ms)
print(90.seconds.ms)
const retries = 3
print((retries * 5).minutes.ms)
}The program ran and printed:
kavach run durations.kav
17280000090000900000
| Written | Is |
|---|---|
n.ms | n milliseconds |
n.seconds | n * 1_000 ms |
n.minutes | n * 60_000 ms |
n.hours | n * 3_600_000 ms |
n.days | n * 86_400_000 ms |
A Duration is a number of milliseconds, so 1.days is always exactly 24 hours: daylight saving and time zones don't change it. There is no .months, because months differ in length: for those, use a date's add_months, below. Writing one is refused, with what to write instead:
use std.time
fn main() {
print(2.months.ms)
}The compiler refused the program:
kavach run months.kav
error: `2` has no field `months`. The units that make a `Duration` are `ms`, `seconds`, `minutes`, `hours`, `days`. Months and years have no fixed length: count days instead.Every function
| Function | Gives |
|---|---|
time.now_ms() -> Int | Milliseconds since 1970-01-01 UTC |
time.monotonic_ms() -> Int | Milliseconds from a clock that never goes back; use it to measure durations |
time.sleep_ms(ms) | Waits ms milliseconds |
time.sleep(d) | Waits for the Duration d |
time.every_ms(ms) | An interval: a for loop over it runs a round every ms milliseconds, counting 0, 1, 2, …, until the loop ends with break or return |
time.every(d) | The same, every Duration d |
time.after(d, f) | A Task (9.1) that waits for d and then calls f; like any task, it belongs to the function that called time.after, and is cancelled when that function returns (9.2) |
Reading the clock needs no capability: it changes nothing outside the program. Waiting, with sleep, every_ms or a timer, needs Clock. In the tests kavach test writes for you, a fake clock makes reading the time, sleep, after and each round of every instant and the same every time. A test block sets one with time.freeze, below.
When the clock changes
- Waits (
sleep,every,after, timeouts) andmonotonic_msuse the system's clock that never jumps, as Rust, Go, Java and Python do: changing the clock or the time zone doesn't move them. - Whether time the machine slept counts is the system's own rule: it doesn't on Linux and macOS, and it does on Windows.
- There is no wait until a time of day, and no cron schedule. A job at 09:00 every day belongs to the system's scheduler (cron, systemd timers, Task Scheduler), which starts the program.
The clock in tests
A test block that needs a date calls time.freeze(ms). From then on the clock is fake: now_ms gives ms, and sleep moves it on at once, so 30 days pass in no time:
use std.time
type Trial {
started: Int
fn ended(self) -> Bool
require self.started >= 0
example Trial(started: 1_767_225_600_000).ended() == false
{
time.now_ms() - self.started >= 30.days.ms
}
}
test "a trial ends after 30 days" {
time.freeze(1_767_225_600_000) // 2026-01-01 00:00 UTC
const trial = Trial(started: time.now_ms())
time.sleep(29.days)
assert not trial.ended()
time.sleep(1.days)
assert trial.ended()
}The program ran and printed:
kavach run freeze.kav
✓ Trial.ended: example Trial(started: 1_767_225_600_000).ended() == false✓ test "a trial ends after 30 days"2 passed, 0 failedseed 7✓ Trial.ended 200 cases1 functions 1 ✓ 0 ✗ 0 weakly tested
- Calling it again jumps the clock forward or back, as when a person changes the machine's clock.
monotonic_msdoesn't jump: it counts only what the test slept. - The tests
kavach testwrites for you, and everyexample, need no call: their clock is already frozen at 2026-01-01 00:00 UTC. That is the moment the example above starts its trial. - Anywhere but directly in a
testblock, it is an error:
use std.time
fn main() {
time.freeze(0)
print(time.now_ms())
}The compiler refused the program:
kavach run freeze-outside.kav
error: `time.freeze` is only for tests: call it directly in a `test` block (spec 14.4)Dates
| Type | Is |
|---|---|
Date | A day of the calendar, with no time and no zone: year, month (1 to 12), day (1 to the month's last) |
Time | A time of day, with no date and no zone: hour (0 to 23), minute, second, ms |
DateTime | A date and a time, with no zone: what a calendar and a wall clock show |
Weekday | Monday to Sunday |
use std.time
fn main() ! Error {
const due = time.Date(year: 2024, month: 1, day: 31)
print(due.add_months(1).iso())
print(due.weekday())
print(due.add_days(30).iso())
print(due.days_until(time.Date(year: 2024, month: 12, day: 25)))
print(time.utc(0).iso())
print(time.is_leap_year(1900))
print(time.is_leap_year(2000))
print(time.days_in_month(2026, 2))
const day = try time.parse_date("2026-11-04")
print(day.weekday())
const bad = time.parse_date("2026-02-30") catch e => {
print("refused: {e}")
return
}
print(bad.iso())
}The program ran and printed:
kavach run dates.kav
2024-02-29Wednesday2024-03-013291970-01-01T00:00:00falsetrue28Wednesdayrefused: Invalid(text: "2026-02-30")
- Months are 1 to 12, never from 0 as in JavaScript.
- A month too short for the day gives its last day: January 31 and one month is February 29 in 2024, a leap year, and February 28 in 2026.
- Every year uses the Gregorian calendar: 1900 is not a leap year, 2000 is.
- A date that can't exist is refused.
parse_datefails withInvalid, and making one stops the program:
use std.time
fn main() {
const month = 2
const day = time.Date(year: 2026, month: month, day: 30)
print(day.iso())
}The program ran, then stopped with a panic:
kavach run impossible.kav
panic: contract failed: invariant day >= 1 and day <= days_in_month(year, month)A DateTime has no time zone: it is what a calendar and a wall clock show. A moment, milliseconds since 1970 as now_ms gives it, is another thing, so the two can't be mixed by accident. time.utc(ms) turns a moment into its DateTime in UTC, and dt.utc_ms() back. A named time zone, below, does the same for any place.
| Function or method | Gives |
|---|---|
time.today(), time.now() | Today's Date, and the DateTime now, in UTC |
time.utc(ms), dt.utc_ms() | A moment as a DateTime in UTC, and back |
time.parse_date(text) | The Date of 2026-11-04; fails with Invalid(text) for anything else, an impossible date (2026-02-30) too |
time.is_leap_year(year), time.days_in_month(year, month) | As their names say |
d.weekday() | Its Weekday |
d.add_days(n), d.add_months(n) | The date n days or months later (earlier for a negative n); a month too short for the day gives its last day: January 31 and one month is February 28 or 29 |
d.days_until(other) | Days from d to other, negative when other comes first |
d.iso(), t.iso(), dt.iso() | 2026-11-04, 09:05:30 (with .250 when there are milliseconds), 2026-11-04T09:05:30 |
Time zones
A Zone is a named time zone, such as Asia/Kolkata or America/New_York: what the clocks there show at each moment, daylight saving time included. Like reading the clock, it needs no capability. One moment, in three places:
use std.time
fn main() ! Error {
const call = 1_782_907_200_000 // 2026-07-01, 12:00 UTC
for name in ["Asia/Kolkata", "Europe/Berlin", "America/New_York"] {
const z = try time.zone(name)
print("{name}: {z.at(call).iso()}, UTC offset {z.offset_seconds(call) / 60} minutes")
}
}The program ran and printed:
kavach run zones.kav
Asia/Kolkata: 2026-07-01T17:30:00, UTC offset 330 minutesEurope/Berlin: 2026-07-01T14:00:00, UTC offset 120 minutesAmerica/New_York: 2026-07-01T08:00:00, UTC offset -240 minutes
| Function or method | Gives |
|---|---|
time.zone(name) | The Zone named name, or the fixed offset +05:30; fails with UnknownZone(name) for a name the database doesn't have and for an offset that can't be (+24:00, +5:30) |
z.at(ms) | The DateTime the clocks in z show at the moment ms |
z.moment(dt) | The moment the clocks in z show dt; a skipped or repeated time is resolved as below |
z.resolve(dt) | Which moments show dt: Exact(ms), Skipped(earlier, later) or Repeated(earlier, later), a Local |
z.offset_seconds(ms) | How far z is ahead of UTC at the moment ms, in seconds (negative west of Greenwich) |
z.iso(ms) | The moment ms as the clocks in z show it, with the offset: 2026-11-04T14:35:30+05:30, and Z for an offset of zero (see "Times as text") |
time.zone_offset(name, ms) | The same for a named zone of the database, none for any other name: what the others are built on |
When daylight saving time starts, the clocks skip an hour, so some times of day never happen there. z.moment(dt) moves a skipped time later by the gap, as iCalendar, Java and JavaScript's Temporal do. To choose otherwise, match on z.resolve(dt):
use std.time
fn main() ! Error {
const new_york = try time.zone("America/New_York")
// That night, the clocks went from 02:00 straight to 03:00.
const night = time.Date(year: 2026, month: 3, day: 8)
const alarm = time.DateTime(date: night, time: time.Time(hour: 2, minute: 30, second: 0, ms: 0))
print("moment: {new_york.at(new_york.moment(alarm)).iso()}")
match new_york.resolve(alarm) {
time.Exact(_) => print("02:30 happened once"),
time.Skipped(earlier, _) => print("an hour early instead: {new_york.at(earlier).iso()}"),
time.Repeated(_, _) => print("02:30 happened twice"),
}
}The program ran and printed:
kavach run zone-skipped.kav
moment: 2026-03-08T03:30:00an hour early instead: 2026-03-08T01:30:00
When it ends, the clocks go back, so some times happen twice. z.moment(dt) takes the first:
use std.time
fn main() ! Error {
const new_york = try time.zone("America/New_York")
// That night, the clocks went back from 02:00 to 01:00.
const night = time.Date(year: 2026, month: 11, day: 1)
const twice = time.DateTime(date: night, time: time.Time(hour: 1, minute: 30, second: 0, ms: 0))
match new_york.resolve(twice) {
time.Repeated(earlier, later) => {
print("first 01:30: {new_york.offset_seconds(earlier) / 3600} h from UTC")
print("second 01:30: {new_york.offset_seconds(later) / 3600} h from UTC")
print("moment gives the first: {new_york.moment(twice) == earlier}")
}
_ => print("01:30 happened once"),
}
}The program ran and printed:
kavach run zone-repeated.kav
first 01:30: -4 h from UTCsecond 01:30: -5 h from UTCmoment gives the first: true
- Kavach never reads the machine's own time zone. A program names the zone it means, so it runs the same on every machine.
- The zone rules come from the operating system on Linux and macOS, and from a copy built into the program on Windows.
- An unknown name fails with
UnknownZone, andtime.zone_offsetgivesnone:
use std.time
fn main() {
const z = time.zone("Mars/Olympus") catch e => {
print("refused: {e}")
print(time.zone_offset("Mars/Olympus", 0))
return
}
print(z.name)
}The program ran and printed:
kavach run zone-unknown.kav
refused: UnknownZone(name: "Mars/Olympus")none
A fixed offset
A Zone can also be a fixed offset from UTC, such as +05:30, which never changes. z.iso(ms) writes a moment as the clocks in the zone show it, with the offset:
use std.time
fn main() ! Error {
const fixed = try time.zone("+05:30")
print(fixed.iso(0))
print((try time.zone("UTC")).iso(0))
// Kolkata's oldest offset has seconds.
const kolkata = try time.zone("Asia/Kolkata")
const long_ago = time.DateTime(date: time.Date(year: 1874, month: 12, day: 1), time: time.Time(hour: 0, minute: 0, second: 0, ms: 0))
print(kolkata.iso(kolkata.moment(long_ago)))
for name in ["+24:00", "+5:30"] {
const _zone = time.zone(name) catch e => {
print("refused: {e}")
continue
}
}
}The program ran and printed:
kavach run zone-fixed.kav
1970-01-01T05:30:00+05:301970-01-01T00:00:00Z1874-12-01T00:00:00+05:21:10refused: UnknownZone(name: "+24:00")refused: UnknownZone(name: "+5:30")
- An offset of zero is written
Z. - An offset has seconds only when it needs them, as some zones' oldest offsets do.
- An offset that can't be fails with
UnknownZone, like an unknown name. z.at(ms)andz.iso(ms)take a moment within ±30_000_000_000_000_000ms of 1970, as their contract says.
Times as text
time.parse_date and three more read the formats iso writes. A Time and a DateTime have no zone, so their text can't have one. A moment needs a zone, so its text must:
| Function | Reads | Gives |
|---|---|---|
time.parse_time(text) | 09:05, 09:05:30, 09:05:30.250 | A Time |
time.parse_datetime(text) | 2026-11-04T09:05:30, with a time as above | A DateTime |
time.parse_moment(text) | 2026-11-04T09:05:30Z, 2026-11-04T14:35:30+05:30 | A moment, in milliseconds |
use std.time
fn main() ! Error {
const at = try time.parse_moment("2026-11-04T14:35:30+05:30")
print(at)
const berlin = try time.zone("Europe/Berlin")
print(berlin.iso(at))
print((try time.parse_time("09:05")).iso())
print((try time.parse_datetime("2026-11-04T09:05:30.250")).iso())
print((try time.parse_time("09:05:30.1239")).ms)
}The program ran and printed:
kavach run text.kav
17937831300002026-11-04T10:05:30+01:0009:05:002026-11-04T09:05:30.250123
Each of them fails with Invalid(text) for anything else:
use std.time
fn main() {
for text in ["2026-11-04T09:05:30", "2026-11-04 09:05:30Z"] {
const _moment = time.parse_moment(text) catch e => {
print("moment: {e}")
continue
}
}
for text in ["2026-11-04T09:05:30+05:30", "2026-11-04T9:05:30"] {
const _wall = time.parse_datetime(text) catch e => {
print("datetime: {e}")
continue
}
}
for text in ["9:05", "23:59:60"] {
const _time = time.parse_time(text) catch e => {
print("time: {e}")
continue
}
}
for text in ["2026-11-4", "2026-+1-04"] {
const _date = time.parse_date(text) catch e => {
print("date: {e}")
continue
}
}
}The program ran and printed:
kavach run text-refused.kav
moment: Invalid(text: "2026-11-04T09:05:30")moment: Invalid(text: "2026-11-04 09:05:30Z")datetime: Invalid(text: "2026-11-04T09:05:30+05:30")datetime: Invalid(text: "2026-11-04T9:05:30")time: Invalid(text: "9:05")time: Invalid(text: "23:59:60")date: Invalid(text: "2026-11-4")date: Invalid(text: "2026-+1-04")
use std.time
fn main() ! Error {
const old = try time.parse_date("-0005-01-01")
print(old.iso())
const far = try time.parse_date("+12345-06-07")
print(far.iso())
print((try time.parse_date("12345-06-07")) == far)
}The program ran and printed:
kavach run expanded-years.kav
-0005-01-0112345-06-07true
- Numbers have exactly the digits shown, so
9:05and2026-11-4fail, and so does a sign where a digit should be. - A year before 0 or after 9999 is written as
isowrites it, with a sign or more digits, as ISO 8601's expanded years are:-0005-01-01,12345-06-07or+12345-06-07. - A fraction of a second has 1 to 9 digits. Kavach keeps milliseconds, so digits past the third are dropped, not rounded:
.1239is 123 ms. - A leap second (
23:59:60) fails: aTimehas no such second. - In JSON, a
Date,TimeandDateTimeare this text too, with aDateTimein UTC: see Dates and times. parse_momentreads back any momentz.isowrote, seconds in the offset included.- Other formats, such as
4 Nov 2026, are not read: they are an open question.
The time in a contract
Reading the clock needs no capability, but a contract may not do it: its answer would change from run to run.
use std.time
fn expires(at_ms: Int) -> Bool
require at_ms > time.now_ms()
{
true
}
fn main() {
print(expires(0))
}The compiler refused the program:
kavach run clock-contract.kav
error: a contract can't call `time.now_ms`: it reads the clock, and contracts must give the same answer every time (spec 7.5)