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​

CallAnswers
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()
jaiscript
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);
output
true true true

Waiting 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.

MethodDoes
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
jaiscript
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());
output
true true true
copy running: false, original running: true

Declaring 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.

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 (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}'.

FieldFormat 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.

jaiscript
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());
output
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.000Z

Arithmetic, comparison and zones​

MemberDoes
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 - bthe 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.

jaiscript
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());
output
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 true

Formatting​

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:

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("${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); }
output
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 value

The 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.

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