Skip to content
Kavachकवच

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
measure.kav: Kavach code, then what running it prints
  • 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
every.kav: Kavach code, then what running it prints

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
durations.kav: Kavach code, then what running it prints
WrittenIs
n.msn milliseconds
n.secondsn * 1_000 ms
n.minutesn * 60_000 ms
n.hoursn * 3_600_000 ms
n.daysn * 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.
months.kav: Kavach code, then what running it prints

Every function

FunctionGives
time.now_ms() -> IntMilliseconds since 1970-01-01 UTC
time.monotonic_ms() -> IntMilliseconds 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) and monotonic_ms use 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
freeze.kav: Kavach code, then what running it prints
  • Calling it again jumps the clock forward or back, as when a person changes the machine's clock. monotonic_ms doesn't jump: it counts only what the test slept.
  • The tests kavach test writes for you, and every example, 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 test block, 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)
freeze-outside.kav: Kavach code, then what running it prints

Dates

TypeIs
DateA day of the calendar, with no time and no zone: year, month (1 to 12), day (1 to the month's last)
TimeA time of day, with no date and no zone: hour (0 to 23), minute, second, ms
DateTimeA date and a time, with no zone: what a calendar and a wall clock show
WeekdayMonday 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")
dates.kav: Kavach code, then what running it prints
  • 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_date fails with Invalid, 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)
impossible.kav: Kavach code, then what running it prints

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 methodGives
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
zones.kav: Kavach code, then what running it prints
Function or methodGives
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
zone-skipped.kav: Kavach code, then what running it prints

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
zone-repeated.kav: Kavach code, then what running it prints
  • 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, and time.zone_offset gives none:
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
zone-unknown.kav: Kavach code, then what running it prints

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")
zone-fixed.kav: Kavach code, then what running it prints
  • 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) and z.iso(ms) take a moment within ±30_000_000_000_000_000 ms 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:

FunctionReadsGives
time.parse_time(text)09:05, 09:05:30, 09:05:30.250A Time
time.parse_datetime(text)2026-11-04T09:05:30, with a time as aboveA DateTime
time.parse_moment(text)2026-11-04T09:05:30Z, 2026-11-04T14:35:30+05:30A 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
text.kav: Kavach code, then what running it prints

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")
text-refused.kav: Kavach code, then what running it prints
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
expanded-years.kav: Kavach code, then what running it prints
  • Numbers have exactly the digits shown, so 9:05 and 2026-11-4 fail, and so does a sign where a digit should be.
  • A year before 0 or after 9999 is written as iso writes it, with a sign or more digits, as ISO 8601's expanded years are: -0005-01-01, 12345-06-07 or +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: .1239 is 123 ms.
  • A leap second (23:59:60) fails: a Time has no such second.
  • In JSON, a Date, Time and DateTime are this text too, with a DateTime in UTC: see Dates and times.
  • parse_moment reads back any moment z.iso wrote, 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)
clock-contract.kav: Kavach code, then what running it prints