Time: Clocks, Stopwatches & Dates​

Everything to do with time lives in one namespace, time::, part of the standard library a host installs with jai::stdlib::register_all. It has three layers: clocks that answer "how long" and "what time is it", a stopwatch type for measuring intervals, and a date type for the calendar. Every one of them reads the engine's single time source, once per call and only when called, and the host can replace that source with a virtual clock (The time source in the Embedding guide), which is how tests, replays and deterministic simulations drive a script's sense of time.

Clocks​

CallAnswers
time::now()seconds (float) on the engine's monotonic clock, counted from the engine's construction; never goes backwards
time::engine_elapsed()the same clock, by its descriptive name
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()seconds (float) since the Unix epoch, from the wall clock
time::unix_ms()whole milliseconds (int) since the Unix epoch
time::date_now(template)the current local date and time as a string, formatted by a template
jaiscript
float t0 = time::now();
sleep(0.02);
float waited = time::now() - t0;
print("waited at least 20 ms: {}", waited >= 0.02);
print("this execute has run at least that long: {}", time::execute_elapsed() >= waited);
float unix_seconds = time::unix_time();
print("unix_ms reads the same clock, in milliseconds: {}", time::unix_ms() + 1 > unix_seconds * 1000.0);
output
waited at least 20 ms: true
this execute has run at least that long: true
unix_ms reads the same clock, in milliseconds: true

Use the monotonic clocks (now, execute_elapsed, stopwatches) to measure durations: the wall clock can jump when the system clock is set. Use the wall clock (unix_time, dates) to say what time it is.

Waiting is the language's own sleep(seconds) (Coroutines), which reads the same time source: in a run the host started with engine::start it suspends the run and hands control back to the host until the time has passed, and in a plain execute() it blocks the thread, counting against the execution budget like any other time.

The engine's own deadlines run on the real clock. The execution budget and the window between import check passes (Modules) always measure real time, so a host's virtual clock can neither stall nor trip them. The engine keeps a sample of the real clock, taken at each call from the host and at the budget's periodic checks; while the time source is the system clock, a time:: reading (and waking from a sleep that blocks) refreshes that sample too, and a run resumed after a sleep is a call from the host, which is what keeps a sleep-paced frame loop's hot reload prompt. A replaced source never touches it.

Stopwatches​

time::stopwatch is a type like any class: declare one, and call its methods. All results are float seconds.

MethodDoes
start()starts timing from zero; on a running or paused stopwatch it restarts from zero
elapsed()running time so far, without stopping
delta()time since the previous delta() (or since the start), and remembers this moment; on a stopwatch that is not started it starts it and returns 0.0
pause()stops the time from accumulating and returns the elapsed time; a second pause() changes nothing
resume()accumulates again and returns the elapsed time; on a stopwatch never started it starts it, on a running one it changes nothing
stop()returns the elapsed time, then stops and resets both the elapsed time and the delta to zero
is_running() / is_paused()its state

Time spent paused never counts toward elapsed() or delta(). A stopwatch is a value, like every JaiScript value: a copy is a separate stopwatch from then on, and a stopwatch field copies with its object.

jaiscript
time::stopwatch frame;
frame.delta();                      // the first delta starts the stopwatch and returns 0.0
sleep(0.01);
float dt = frame.delta();           // seconds since the previous delta
float at_pause = frame.pause();    // pause returns the elapsed time
sleep(0.05);                        // paused: none of this counts
print("the pause stood still: {}", frame.elapsed() == at_pause);
frame.resume();
print("dt covered the sleep: {}", dt >= 0.01);
print("resumed from where it paused: {}", frame.elapsed() >= at_pause);
print("running: {}, paused: {}", frame.is_running(), frame.is_paused());
auto lap = frame;                   // a copy has its own state
lap.stop();
print("copy running: {}, original running: {}", lap.is_running(), frame.is_running());
output
the pause stood still: true
dt covered the sleep: true
resumed from where it paused: true
running: true, paused: false
copy running: false, original running: true

Declaring a stopwatch reads no clock, and each method reads the monotonic clock at most once: a paused stopwatch answers elapsed(), delta(), pause() and stop() without reading it at all.

Dates​

time::date is a calendar date and a time of day to the millisecond, in one of two zones: UTC or the machine's local zone. The zone is part of the value and is always chosen explicitly:

ConstructorMakes
time::local_now() / time::utc_now()now, from the wall clock
time::date_from_unix(seconds) / time::date_from_unix(seconds, true)the local (or, with true, UTC) date of a Unix time; fractions of a second round to the millisecond
time::local_date(y, m, d) / time::utc_date(y, m, d)a date from fields: year, month, day, then optionally hour, minute, second, millisecond; every field is validated
time::date d;the Unix epoch, 1970-01-01T00:00:00.000Z

Dates run from year 1 to year 9999. 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​

year, month (1-12), day, hour (0-23), minute, second and millisecond read and write through ordinary member access. Every store is checked: a value out of its range, or a store that would leave the day past the end of its month, raises a catchable error and leaves the date unchanged. To move 2024-03-31 to April, set the day first. weekday (0 = Sunday through 6 = Saturday), yearday (1 = January 1) and utc are read-only.

jaiscript
time::date d = time::utc_date(2024, 2, 29);
print("{}-{}-{} is weekday {} and day {} of the year", d.year, d.month, d.day, d.weekday, d.yearday);
d.day = 1;
d.month = 3;
print(d.to_string());
try { d.day = 31; d.month = 4; } catch (e) { print(e); }
print(d.to_string());
output
2024-2-29 is weekday 4 and day 60 of the year
2024-03-01T00:00:00.000Z
time::date: day 31 does not exist in 2024-04
2024-03-31T00:00:00.000Z

to_string() is ISO 8601: 2024-03-31T00:00:00.000Z for a UTC date, the same without the Z for a local one.

Arithmetic and comparison​

Every add_ method returns a new date and leaves the receiver alone. They come in two kinds:

  • Calendar steps: add_days(n), add_months(n) and add_years(n) take whole numbers and keep the time of day. A month or year step that lands past the end of the target month clamps to its last day: January 31 plus one month is February 29 in a leap year and February 28 otherwise, and February 29 plus one year is February 28.
  • Elapsed time: add_hours(n), add_minutes(n) and add_seconds(n) take any number (add_seconds(1.5)) and move the instant by exactly that much, rounded to the millisecond.

In UTC the two kinds agree. In the local zone they differ across a daylight-saving change: add_days(1) is the same time tomorrow, add_hours(24) is twenty-four hours later.

Dates compare as instants with <, <=, ==, !=, > and >=, so a local date and a UTC date of the same moment are equal. a - b is the difference in float seconds, and d.between(a, b) asks whether d lies in the inclusive range between two dates given in either order.

jaiscript
time::date launch = time::utc_date(2024, 1, 31, 9, 30);
print(launch.to_string());
print(launch.add_months(1).to_string());       // Feb 31 does not exist: the day clamps to Feb 29
print(launch.add_days(30).format("${weekday_name}, ${month_name} ${day}"));
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));
output
2024-01-31T09:30:00.000Z
2024-02-29T09:30:00.000Z
Friday, March 1
2024-02-01T21:30:00.000Z
31622400.0 seconds apart
true true true

Zones and Unix time​

d.unix() is the date's instant in float seconds since the Unix epoch and d.unix_ms() in whole milliseconds. to_utc() and to_local() return the same instant in the other zone.

jaiscript
time::date u = time::date_from_unix(1700000000, true);
time::date l = u.to_local();
print(u.to_string());
print("same instant: {}, local is UTC: {}", u == l, l.utc);
print("round trip: {}", time::date_from_unix(l.unix()).to_string() == l.to_string());
print("now, both ways: {}", time::local_now().to_utc().format("${year}") == time::utc_now().format("${year}"));
output
2023-11-14T22:13:20.000Z
same instant: true, local is UTC: false
round trip: true
now, both ways: true

Formatting​

d.format(template) and time::date_now(template) fill a template string whose placeholders are ${name}, optionally with a format spec: ${name:spec} takes exactly the specs a template string splice takes (${day:02}, ${month_name:>10}, ${year:x}). The template is an ordinary string, so write it in double quotes: the placeholders are resolved by the call, not by the language at the call site. Everything outside a ${...} is copied as it is.

PlaceholderValue
year, month, dayints (month is 1-12)
month_name, month_shortSeptember, Sep
weekday_name, weekday_shortSunday, Sun
hours, military_hoursthe 12-hour clock (12, 1-11) and the 24-hour clock (0-23)
minutes, seconds, millisecondsints
am_pmAM or PM
jaiscript
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("${year}-${month:02}-${day:02}T${military_hours:02}:${minutes:02}:${seconds:02}.${milliseconds:03}"));
print(d.format("[${month_name:>10}] [${weekday_name:*^11}]"));
try { d.format("${fortnight}"); } catch (e) { print(e); }
print(time::date_now("${year}") == time::local_now().format("${year}"));
output
Sun 27 Sep 2026 2:03 PM
2026-09-27T14:03:05.007
[ September] [**Sunday***]
time::date::format: unknown placeholder '${fortnight}'
true

An unknown placeholder, an unterminated ${, a spec outside the template-string subset and a spec that does not fit the value (${month_name:05}) all raise a catchable error that names the problem. date_now formats the local time; for UTC, format time::utc_now().

The types by their names​

time::date and time::stopwatch are spelled with their namespace everywhere a class name goes: declarations, parameters and returns, fields, container element types, cast<time::date>(x), shared_ptr<time::date> and new time::date(). time::date() and time::stopwatch() construct them as expressions.

jaiscript
class Session {
    time::date started = time::utc_date(2026, 1, 1, 8);
    time::stopwatch clock;
}
function length_of(time::date a, time::date b) -> float { return b - a; }
array<time::date> marks = [time::utc_date(2026, 1, 1, 9), time::utc_date(2026, 1, 1, 10, 30)];
Session s;
for (time::date m : marks) { print("{} s after start", length_of(s.started, m)); }
print(type_of(s.clock), " ", s.clock.is_running());
output
3600.0 s after start
9000.0 s after start
time::stopwatch false

The strict checker knows the namespace: a time:: call with the wrong number of arguments ('time::now' expects 0 argument(s), got 1) or an argument it cannot take (time::date_from_unix("soon")) fails the load, results carry their types, and a member neither type has (no member 'nope' in class 'time::date') is a load error like any class's.

Time and parallel loops​

A parallel_for gives the same result at every worker count. A clock read cannot keep that promise (the answer would depend on scheduling), so a parallel body that calls time:: is rejected before any worker starts, with the usual catchable explanation:

jaiscript
array<float> stamps = [0.0, 0.0, 0.0];
try {
    parallel_for (auto& s : stamps) { s = time::now(); }
} catch (e) {
    print(e);
}
output
parallel_for: time:: calls are not allowed in a parallel body (engine-owned clock) at 3:49

Read the clock before the loop and hand the value in.

Where to next​

One chapter remains: Math covers the vector and color types (math::vec2 to math::vec4, the integer vectors, math::color) and the geometry library. After that: Embedding covers the C++ side (running scripts, binding your classes with dynamic_binder, signals, safety rails, the time source, and the performance model); Examples walks complete programs written in the language; and the Reference is the everything-on-one-page lookup when you know what you're looking for.