first_cycle : Cycle
The cycle an app starts on: count zero, no elapsed time.
App.Input.for_tests uses it, and a test that needs a second cycle says
so: input.with_time({ ..Time.first_cycle, cycle_count: 1 }).
Helpers for working with the cycle's simulation clock, and with the calendar the world outside the app keeps.
Animation and physics use input.time, not wall time. The host samples one
Cycle per call to update!, and every app that moves something should
derive that movement from it: a value read from the calendar or a
wall-clock timer is not the timeline the platform paces, replays, or holds
to a fixed step while a capture records.
For the common "move per cycle" case, use input.time.elapsed_seconds
directly -- simulation seconds since the preceding input -- without touching
the helpers below.
Timestamp and now! are the other clock: what time it is in the world,
for stamping a filename, rendering a date, or correlating a log line with
something outside the process. It is explicitly nondeterministic, and it is
never a substitute for Cycle. now! is legal in init!, update!, and
tasks; it is refused in render!, which is given the model rather than an
input and should draw the instant the model already decided on. It does not
wait, so it needs no task of its own.
The Cycle helpers live in the companion roc-ray-types package so
reusable packages can depend on them without depending on this platform, and
this module re-exports them. Timestamp is this platform's own: reading the
world's clock is an effect, so it lives where the effects do.
first_cycle : Cycle
The cycle an app starts on: count zero, no elapsed time.
App.Input.for_tests uses it, and a test that needs a second cycle says
so: input.with_time({ ..Time.first_cycle, cycle_count: 1 }).
to_seconds : U64 -> F32
Convert a nanosecond duration to seconds.
expect Time.to_seconds(500_000_000) == 0.5
delta_seconds : U64, U64 -> F32
Seconds elapsed between two clock samples. The second must not be earlier than the first.
Handy for deriving a delta over more than one cycle from
Time.Cycle.simulation_nanos:
dt = Time.delta_seconds(model.last_tick, input.time.simulation_nanos)
now! : () => Timestamp
Read the world's clock.
Legal in init!, update!, and tasks; refused in render!. render! is
given the model rather than an input, and should draw the instant the
model already decided on.
Two calls in one update! can answer differently, and an app that wants
one instant for a whole cycle should read it once and keep it in the model.
Nothing else about the platform changes with it: the calendar is explicitly
nondeterministic, it is not what a capture paces, and it is not what
animation should move on.
Cycle : Cycle
When one cycle happened, and how much time it covers.
cycle_count counts cycles from 0, simulation_nanos is the
simulation clock the platform advances, monotonic_nanos is the host's
own monotonic clock at the sample, and elapsed_seconds is the time this
cycle covers, which is what animation and simulation multiply by.
Declared in the roc-ray-types package's Time and re-exported here;
App.Input carries one as input.time.
Time.Timestamp :: # (opaque)
An instant on the world's clock, as seconds and nanoseconds since the Unix epoch.
Always normalized: nanosecond is less than 1,000,000,000, and seconds
are floor-based, so one nanosecond before the epoch is
{ seconds: -1, nanosecond: 999_999_999 } rather than a negative
fraction of second zero. That is what makes ordering and differences
work the same on both sides of 1970.
A timestamp carries no time zone, no calendar policy, and no
leap-second model. Everything this module formats is UTC; local time and
calendar arithmetic belong to a pure Roc package, and a Timestamp is
the value to hand it.
is_eq : _
Compare two of these values.
epoch : Timestamp
The Unix epoch, 1970-01-01T00:00:00Z.
A neutral value for a model field that has not read the clock yet, and the base every other instant is measured from.
from_parts : { seconds : I64, nanosecond : U32 } -> Try(Timestamp, [InvalidNanosecond])
Build a timestamp from its two parts.
InvalidNanosecond is a whole second or more of fractional part,
which is a value that does not describe an instant rather than a
clock that failed.
from_nanos_since_epoch : I128 -> Try(Timestamp, [OutOfRange])
Build a timestamp from signed nanoseconds since the epoch.
OutOfRange is an instant further from 1970 than a signed 64-bit
count of seconds reaches, which is about 292 billion years either
way.
seconds_since_epoch : Timestamp -> I64
Whole seconds since the epoch, rounded towards the epoch's past.
subsecond_nanoseconds : Timestamp -> U32
The fractional nanoseconds within this timestamp's own second.
nanos_since_epoch : Timestamp -> I128
Signed nanoseconds since the epoch.
difference_nanos : Timestamp, Timestamp -> I128
Nanoseconds from the first instant to the second, negative when the second is the earlier one.
to_iso_8601 : Timestamp -> Str
Format as an ISO 8601 instant in UTC, to the second.
expect Time.Timestamp.epoch.to_iso_8601() == "1970-01-01T00:00:00Z"
The Z is always literal: this platform formats UTC and nothing
else, so there is no offset to get wrong. Years are padded to four
digits, and a year before the common era is written with a leading
minus and as many digits as it needs.
to_iso_8601_nanos : Timestamp -> Str
Format as an ISO 8601 instant in UTC, to the nanosecond.
The fractional part is always nine digits, so timestamps sort as strings in the same order they sort as instants.
to_file_stamp : Timestamp -> Str
Format as an instant that is legal in a filename on every platform.
The same shape as to_iso_8601 with - where the colons would be,
because a colon cannot appear in a path on Windows:
expect Time.Timestamp.epoch.to_file_stamp() == "1970-01-01T00-00-00Z"
Pure types and receivers: roc-ray-types Time