Time: Clocks, Stopwatches & Dates
The time:: namespace, part of the standard library a host installs with jai::stdlib::register_all, has clocks, a time::stopwatch type and a time::date type.
- Everything reads the engine's one time source, once per call. A host can replace it with a virtual clock (The time source) for tests, replays and deterministic simulations.
- Measure durations with the monotonic clock (
now,execute_elapsed, stopwatches). The wall clock (unix_time, dates) can jump when the system clock is set. - Stopwatches and dates are values: a copy is separate from then on.
- The execution budget and the window between import checks (Modules) always measure real time, whatever the time source.
Clocks
| Call | Answers |
|---|---|
time::now(), time::engine_elapsed() | float seconds on the monotonic clock since the engine was constructed (two names, one clock) |
time::execute_elapsed() | seconds since the running host entry began: the execute() the host called, or the script function or coroutine it invoked while no script was running. Nested executes, includes and imports belong to the entry that runs them |
time::unix_time(), time::unix_ms() | wall-clock time since the Unix epoch: float seconds, whole int milliseconds |
time::date_now(template) | the current local date and time as a string, formatted by a template; for UTC, format time::utc_now() |
float t0 = time::now();
sleep(0.02);
float waited = time::now() - t0;
print(waited >= 0.02, " ", time::execute_elapsed() >= waited, " ", time::unix_ms() + 1 > time::unix_time() * 1000.0);true true trueWaiting is the language's sleep(seconds) (Coroutines), on the same time source: it suspends a run the host started with engine::start, and blocks the thread in a plain execute(), counting against the execution budget.
Stopwatches
Results are float seconds, and time spent paused never counts.
| Method | Does |
|---|---|
start() | starts timing from zero, also when running or paused |
elapsed() | running time so far, without stopping |
delta() | time since the previous delta() (or the start); on a stopwatch that is not started it starts it and returns 0.0 |
pause() | stops accumulating and returns the elapsed time; a second pause() changes nothing |
resume() | accumulates again and returns the elapsed time; starts a stopwatch never started, changes nothing on a running one |
stop() | returns the elapsed time, then resets the elapsed time and the delta to zero |
is_running(), is_paused() | its state |
time::stopwatch frame;
frame.delta(); // the first delta starts it and returns 0.0
sleep(0.01);
float dt = frame.delta();
float at_pause = frame.pause();
sleep(0.02); // paused: none of this counts
print(dt >= 0.01, " ", frame.elapsed() == at_pause, " ", frame.is_paused());
frame.resume();
auto lap = frame; // a copy has its own state
lap.stop();
print("copy running: {}, original running: {}", lap.is_running(), frame.is_running());true true true
copy running: false, original running: trueDeclaring a stopwatch reads no clock, each method reads it at most once, and a paused stopwatch answers elapsed(), delta(), pause() and stop() without reading it.
Dates
time::date is a calendar date and a time of day to the millisecond, from year 1 to year 9999, in UTC or the machine's local zone. The zone is part of the value and is always chosen explicitly.
| Constructor | Makes |
|---|---|
time::local_now(), time::utc_now() | now, from the wall clock |
time::date_from_unix(seconds), time::date_from_unix(seconds, true) | the local (with true, UTC) date of a Unix time, rounded to the millisecond |
time::local_date(y, m, d), time::utc_date(y, m, d) | a date from fields, optionally followed by hour, minute, second, millisecond; every field is validated |
time::date d; | the Unix epoch, 1970-01-01T00:00:00.000Z |
Arithmetic, comparison and formatting work in both zones; converting a local date to or from an instant is limited to the range the platform's local-time support covers.
Fields and format placeholders
Member fields and format placeholders are spelled differently for the time of day: the field is d.hour, the placeholder ${military_hours}. A field name that is no placeholder raises time::date::format: unknown placeholder '${hour}'.
| Field | Format placeholder |
|---|---|
year | ${year} |
month (1-12) | ${month}; names ${month_name} (September), ${month_short} (Sep) |
day | ${day} |
hour (0-23) | ${military_hours} (0-23); ${hours} is the 12-hour clock (12, 1-11) with ${am_pm} (AM or PM) |
minute | ${minutes} |
second | ${seconds} |
millisecond | ${milliseconds} |
weekday (0 = Sunday to 6 = Saturday, read-only) | names only: ${weekday_name} (Sunday), ${weekday_short} (Sun) |
yearday (1 = January 1, read-only), utc (bool, read-only) | none |
Every store is checked: a value out of range, or one that would leave the day past the end of its month, raises a catchable error and leaves the date unchanged, so to move 2024-03-31 to April, set the day first. to_string() is ISO 8601, with a trailing Z only for a UTC date.
time::date d = time::utc_date(2024, 3, 31, 14, 3, 5);
print("{} {} {} / weekday {} yearday {}", d.hour, d.minute, d.second, d.weekday, d.yearday);
print(d.format("${military_hours} ${minutes} ${seconds} / ${hours} ${am_pm}"));
try { d.format("${hour}:${minute}"); } catch (e) { print(e); }
try { d.month = 4; } catch (e) { print(e); } // April has no 31st
try { d.hour = 24; } catch (e) { print(e); }
d.day = 1;
d.month = 4;
print(d.to_string());14 3 5 / weekday 0 yearday 91
14 3 5 / 2 PM
time::date::format: unknown placeholder '${hour}'
time::date: day 31 does not exist in 2024-04
time::date: hour must be 0 to 23, got 24
2024-04-01T14:03:05.000ZArithmetic, comparison and zones
| Member | Does |
|---|---|
add_days(n), add_months(n), add_years(n) | calendar steps: whole numbers, the time of day is kept. A month or year step past the end of the target month clamps to its last day (January 31 plus one month is February 29 or 28; February 29 plus one year is February 28) |
add_hours(n), add_minutes(n), add_seconds(n) | elapsed time: any number (add_seconds(1.5)), the instant moves by exactly that much, rounded to the millisecond |
< <= == != > >= | compare instants: a local date and a UTC date of the same moment are equal |
a - b | the difference in float seconds |
d.between(a, b) | whether d lies in the inclusive range, the bounds in either order |
d.unix(), d.unix_ms() | the instant since the Unix epoch: float seconds, whole milliseconds |
to_utc(), to_local() | the same instant in the other zone |
Every add_ method returns a new date and leaves the receiver alone. The two kinds differ in the local zone across a daylight-saving change: add_days(1) is the same time tomorrow, add_hours(24) is twenty-four hours later.
time::date launch = time::utc_date(2024, 1, 31, 9, 30);
print(launch.add_months(1).to_string()); // February 31 does not exist: the day clamps
print(launch.add_hours(36).to_string());
time::date next = launch.add_years(1);
print("{} seconds apart", next - launch);
print("{} {} {}", launch < next, launch == next.add_years(-1), launch.between(next, launch));
time::date u = time::date_from_unix(1700000000, true);
time::date l = u.to_local();
print(u.to_string(), " ", u == l, " ", time::date_from_unix(l.unix()).to_string() == l.to_string());2024-02-29T09:30:00.000Z
2024-02-01T21:30:00.000Z
31622400.0 seconds apart
true true true
2023-11-14T22:13:20.000Z true trueFormatting
d.format(template) and time::date_now(template) fill the ${name} placeholders of the table above and copy everything else. ${name:spec} takes the specs a template string splice takes. The template is an ordinary double-quoted string, resolved by the call. Every mistake raises a catchable error:
time::date d = time::utc_date(2026, 9, 27, 14, 3, 5, 7);
print(d.format("${weekday_short} ${day:02} ${month_short} ${year} ${hours}:${minutes:02} ${am_pm}"));
print(d.format("${military_hours:02}:${minutes:02}:${seconds:02}.${milliseconds:03} [${month_name:>10}] [${weekday_name:*^11}] [${year:x}]"));
print(time::date_now("${year}") == time::local_now().format("${year}"));
try { d.format("${fortnight}"); } catch (e) { print(e); }
try { d.format("${day"); } catch (e) { print(e); }
try { d.format("${day:q}"); } catch (e) { print(e); }
try { d.format("${month_name:05}"); } catch (e) { print(e); }Sun 27 Sep 2026 2:03 PM
14:03:05.007 [ September] [**Sunday***] [7ea]
true
time::date::format: unknown placeholder '${fortnight}'
time::date::format: unterminated placeholder '${day'
time::date::format: unsupported format spec ':q' in '${day:q}'
time::date::format: format spec ':05' does not apply to string valueThe types by their names
time::date and time::stopwatch are spelled with their namespace wherever a class name goes: declarations, parameters and returns, fields, container element types, cast<time::date>(x), shared_ptr<time::date>, new time::date(), and time::date() as an expression.
The strict checker knows the namespace: a call with the wrong number of arguments ('time::now' expects 0 argument(s), got 1), an argument it cannot take (time::date_from_unix("soon")) and a member neither type has (no member 'nope' in class 'time::date') fail the load.
Time and parallel loops
A clock read would make a parallel loop's result depend on scheduling, so a parallel body that calls time:: is rejected before any worker starts. Read the clock before the loop and hand the value in.
array<float> stamps = [0.0, 0.0, 0.0];
try {
parallel_for (auto& s : stamps) { s = time::now(); }
} catch (e) {
print(e);
}parallel_for: time:: calls are not allowed in a parallel body (engine-owned clock) at 3:49