This update adds seven types to Ritchie. Four of them are built-in temporal types, each for one idea:
Dateis a calendar day, with no time or offset.Timeis a time of day, with no date.DateTimeis an instant, kept with the UTC offset it was written in.Durationis elapsed time, such as45mor1h30m.
Each has its own literal form, and there are no implicit conversions between them. The other three are:
r::Clock, the standard library's clock, which reads the current time.r::Random, the standard library's pseudorandom number generator.BitSet, a 64-bit pattern, which is whatr::Randomreturns for random bits.
Instants, Civil Dates and Times of Day
A quick tour:
main() {
DateTime promised = 2026-10-09T09:00:00+05:30
DateTime received = DateTime.fromUnix(1791516600)
log(received) // 2026-10-09T03:30:00Z
log(received == promised) // true
log(received.withOffset(10h)) // 2026-10-09T13:30:00+10:00
log(promised + 36h) // 2026-10-10T21:00:00+05:30
[DateTime: String] events = [
2026-10-09T01:15:00Z: "packed",
2026-10-09T02:40:00Z: "shipped",
promised: "due"
]
events[received] = "delivered"
log(count(events)) // 3
log(events[promised]) // delivered
Date start = d2026-01-31
log(start.addMonths(2)) // d2026-03-31
log(start.addMonths(1).addMonths(1)) // d2026-03-28
Time standup = t23:30:00
log(standup + 45m) // t00:15:00
log(standup.until(t09:00:00)) // 9h30m
}promised and received arrive in different forms, one as text with a +05:30 offset and one as Unix seconds, but they are the same instant. DateTime compares and orders by instant, and the offset only affects display. So the two values are equal, withOffset changes how a value prints without changing what it equals, and writing to events[received] updates the entry for promised, leaving the dictionary with three entries. unix() converts back to seconds and nanoseconds.
Time essentially represents the 24-hour clock that wraps around midnight. Time.since measures the Duration since a previous occurrence of another time and Time.until measures forward to the next occurrence of the other time.
Date supports the methods addDays, addWeeks, addMonths, addYears and daysUntil. When the target month is shorter, addMonths stops at its last day. So one step of two months from 31 January lands on 31 March, but two steps of one month land on 28 March. addYears follows the same rule, while addDays and addWeeks move by an exact number of days. All four add methods take negative numbers and move backwards, so d2026-03-31.addMonths(-1) gives d2026-02-28. since is not yet implemented for Date, but daysUntil with an earlier date returns a negative value.
A DateTime carries a numeric offset. Zone rules change over time, so Ritchie keeps named zones such as Australia/Melbourne out of the value and records the offset that was actually used.
Adaptive Duration Precision
A Duration is always eight bytes, and it picks its precision from its size. Up to about 146 years it counts nanoseconds. Beyond that it counts milliseconds, which extends its range to about 146 million years. highRes() and lowRes() report which precision a value is using.
main() {
Duration third = 1ms / 3
log(third) // 333us333ns
log(third.highRes()) // true
Duration lifetime = 53375d
log(lifetime.highRes()) // true
log((lifetime + 1d).highRes()) // false
Duration era = 60000000d
log(era + 1ns == era) // true
log(1h == 60m) // true
log(90m / 1h) // 1.5
}The switch is automatic, so a program never has to pick a unit for a span of time. The trade-off is at the far end of the range: a nanosecond added to sixty million days is lost, because a value that large keeps only milliseconds. Precision doesn't affect comparison, so 1h == 60m holds whichever precision each side uses, and dividing one Duration by another gives a Float.
Temporal Safety Features
Date and time bugs tend to show up late, on the last day of a month or on 29 February. Ritchie catches as many of them as it can before the program runs. A literal must be a real calendar date, time or offset, and arithmetic must make sense for the types involved, so these lines don't compile:
main() {
Date due = d2026-02-30
// "d2026-02-30" is not a well-formed Date literal
Date next = d2026-10-09 + 24h
// "+" does not apply to a Date and a Duration value
}When a failure depends on runtime values, such as moving a DateTime past year 9999, the operation raises OutOfRange! for the program to handle.
Reading the Clock
The standard library's r::Clock reads the current time:
openedAt(r::Clock clock) -> DateTime {
return clock.time()
}
main() {
log(r::Clock.time()) // 2026-09-24T02:00:00.418273516Z
r::Clock local = r::Clock.withOffset(10h)
log(local) // r::Clock { offset: +10:00 }
log(local.offset()) // 10h
log(openedAt(local)) // 2026-09-24T12:00:00.418301942+10:00
}r::Clock.time() returns UTC with nanosecond precision. It is a wall-clock reading, so it can jump if the host's clock is adjusted, and it is not a reliable way to measure elapsed time.
r::Clock.withOffset makes a clock value that reads the same instant and presents it at a fixed offset. The offset stays fixed, so it won't follow daylight saving. A clock value can be stored in a field or passed as a parameter, as openedAt does, so code that needs the time can be handed the clock it should use.
There is no r::Clock.sleep() yet, which is an embarrassing omission. It will be available in a future update.
Random Numbers
The same release adds r::Random, a pseudorandom generator in the standard library:
main() {
r::Random dice = r::Random.new()
log(dice.range(1, 7)) // 6
log(dice.ratio()) // 0.6130648292654095
log(dice.bits()) // 0x1ba193ee66884427
Byte[32] seed = fill(Byte.fromInt(7), 32)
r::Random a = r::Random.seed(seed)
r::Random b = r::Random.seed(seed)
log(a.range(1, 7) == b.range(1, 7)) // true
log(a.range(10, 0)) // 6
}r::Random.new() starts a generator from the environment's entropy. range(from, to) includes from and excludes to, so range(1, 7) rolls a die, and it counts down when to is below from. ratio() returns a Float from 0.0 up to but excluding 1.0, and bits() returns 64 random bits as a BitSet.
r::Random.seed takes 32 bytes and gives a reproducible generator. The same seed and the same calls give the same results, which is useful in tests. That holds within one compiler release, and the numbers may change between releases.
Like a clock, a generator is a value that can be passed to the code that needs it.
