Time

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.

Timestamp

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.

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"