Duration
A duration is a span of time — how long, not when. A lap of 83.4 seconds, a timeout of five minutes and a working week are all durations, and the Time package gives them one type, Duration, so they add, subtract and print the same way whatever unit they started in.
This lesson also puts optionals to work. Some steps on a duration can overflow, and the package says so in their types: they return Duration?, and the program decides what to do when the answer is none.
The program imports the type and lists Time as a dependency:
import Time::Duration;
Building a duration
A Duration keeps whole seconds plus a nanosecond remainder. Small units are converted exactly, and {} prints the result as decimal seconds:
let lap = Duration::FromMilliseconds(83400);
PrintLine("one lap {}", lap);
PrintLine("in parts {} s and {} ns", lap.WholeSeconds(), lap.SubsecondNanoseconds());
83 400 ms prints as 83.4s; underneath it is 83 whole seconds and 400 000 000 nanoseconds.
Every count of seconds, milliseconds, microseconds or nanoseconds fits, so those constructors always succeed. Minutes and hours are different: an int64 count of hours, times 3600, can be too large for an int64 count of seconds.
| Constructor | Returns | Why |
|---|---|---|
FromSeconds, FromMilliseconds, FromMicroseconds, FromNanoseconds | Duration | every count of these fits |
FromMinutes, FromHours | Duration? | multiplying up to seconds can overflow |
Zero() | Duration | the empty span |
Arithmetic that can overflow
Plus, Minus and Times can overflow the same way, so they answer Duration? too. Where an overflow would be a surprise, ?? supplies a fallback and the program carries on with a plain Duration:
let race = lap.Times(12) ?? Duration::Zero();
PrintLine("twelve laps {}", race);
PrintLine("in ms {}", race.TotalMilliseconds() ?? -1);
Asking for the total in milliseconds is a question that can overflow as well — a huge duration has more milliseconds than an int64 holds — so TotalMilliseconds returns int64?.
Going below zero is not an overflow. A duration may be negative, and IsNegative tells you so:
let limit = Duration::FromSeconds(1000);
match limit.Minus(race) {
margin? => PrintLine("limit - race {} negative: {}", margin, margin.IsNegative()),
none => PrintLine("too far apart to subtract")
}
The race took 1000.8 s against a limit of 1000 s, so the margin is -0.8s.
Several steps, one question
A clock reading such as 1 h 30 min 15 s takes four steps, and three of them may overflow. Inside a function that itself returns Duration?, ? handles each one: the first none ends the function with none, and otherwise the value is unwrapped and the next step runs.
func Span(hours: int64, minutes: int64, seconds: int64) -> Duration? {
let fromHours = Duration::FromHours(hours)?;
let fromMinutes = Duration::FromMinutes(minutes)?;
let both = fromHours.Plus(fromMinutes)?;
return both.Plus(Duration::FromSeconds(seconds));
}
flowchart LR
h["FromHours"] -- "value" --> m["FromMinutes"]
m -- "value" --> p1["Plus"]
p1 -- "value" --> p2["Plus seconds"]
p2 --> r(["Duration?"])
h -- "none" --> n(["none"])
m -- "none" --> n
p1 -- "none" --> nThe last line needs no ?: Plus already returns Duration?, which is exactly what Span returns.
When it does not fit
The largest int64 is about 9.2 × 10¹⁸. As a count of hours that is around a million billion years, and converted to seconds it does not fit, so FromHours answers none rather than a wrong number:
match Duration::FromHours(9223372036854775807) {
huge? => PrintLine("huge = {}", huge),
none => PrintLine("9223372036854775807 hours does not fit, so FromHours gave none")
}
The program
The whole lesson is one package in the Examples repository. Its comments explain every step.
// A duration is a span of time: how long, not when. The Time package keeps one as whole seconds
// plus a nanosecond remainder, so a millisecond and a working week are the same type, add
// exactly, and print with `{}` as decimal seconds.
//
// Building one from seconds, milliseconds or nanoseconds always works, because every count of
// those fits. Building one from minutes or hours does not: an `int64` count of hours times 3600
// can overflow an `int64` count of seconds. So `FromMinutes` and `FromHours` return `Duration?`,
// and so do `Plus`, `Minus` and `Times`, which can overflow the same way. A step that might not
// fit says so in its type, and the program decides what to do about it.
import Io::PrintLine;
import Time::Duration;
// A clock reading such as 1 h 30 min 15 s as one duration. Each step may overflow, so each is
// followed by `?`, and the first one that does makes the whole answer `none`.
func Span(hours: int64, minutes: int64, seconds: int64) -> Duration? {
let fromHours = Duration::FromHours(hours)?;
let fromMinutes = Duration::FromMinutes(minutes)?;
let both = fromHours.Plus(fromMinutes)?;
return both.Plus(Duration::FromSeconds(seconds));
}
func Main() -> int {
// Small units never fail, so they return a plain `Duration`.
let lap = Duration::FromMilliseconds(83400);
PrintLine("one lap {}", lap);
PrintLine("in parts {} s and {} ns", lap.WholeSeconds(), lap.SubsecondNanoseconds());
// Arithmetic answers an optional; `??` supplies a fallback for the overflow nobody expects.
let race = lap.Times(12) ?? Duration::Zero();
PrintLine("twelve laps {}", race);
PrintLine("in ms {}", race.TotalMilliseconds() ?? -1);
// Going below zero is not an overflow: a duration may be negative.
let limit = Duration::FromSeconds(1000);
match limit.Minus(race) {
margin? => PrintLine("limit - race {} negative: {}", margin, margin.IsNegative()),
none => PrintLine("too far apart to subtract")
}
// Building from hours, the case the lesson is about.
match Span(1, 30, 15) {
span? => PrintLine("1 h 30 min 15 s = {}", span),
none => PrintLine("1 h 30 min 15 s does not fit")
}
// As a count of hours, the largest `int64` is about a million billion years, and as seconds it
// does not fit.
match Duration::FromHours(9223372036854775807) {
huge? => PrintLine("huge = {}", huge),
none => PrintLine("9223372036854775807 hours does not fit, so FromHours gave none")
}
return 0;
}
Besides Io, its Rux.toml lists Time under [Dependencies].
Run it
cd Examples/Utilities/Duration
rux run
one lap 83.4s
in parts 83 s and 400000000 ns
twelve laps 1000.8s
in ms 1000800
limit - race -0.8s negative: true
1 h 30 min 15 s = 5415s
9223372036854775807 hours does not fit, so FromHours gave none
Common mistakes
FromHours returns Duration?, not Duration. Writing let fromHours: Duration = Duration::FromHours(hours); fails with error: cannot assign 'Duration?' to 'Duration'. Unwrap it first — with ?, ?? or a match.Without the
??, race is a Duration?, and printing it fails with error: argument 2 to 'PrintLine' has type 'Duration?', but variadic parameter 'args' requires 'Display'. Methods are refused too: type 'Duration?' has no field 'TotalMilliseconds'.? in a function that cannot pass none on.? hands the absence to the caller, so the enclosing function must return an optional. In Main, which returns int, Duration::FromMinutes(17)? fails with error: '?' propagates the absence of 'Duration?', but the enclosing function returns 'int'. Use ?? there, or move the steps into a function like Span.The constructors count whole units, so
Duration::FromSeconds(1.5) fails with error: argument 1 to 'Duration::FromSeconds' has type 'float64', but parameter 'seconds' requires 'int64'. Pick a smaller unit instead: Duration::FromMilliseconds(1500).Try it yourself
- Print
racewith{:.3}and then with{:.0}. A precision names how many digits of the fraction to write — does it round or cut off? - Call
Span(0, 90, 0). How many seconds are 90 minutes? - Import
ParseDuration, read the text"1000.8s", and check withEqualsthat it is the same duration asrace. - Turn the negative
marginround withNegated(), which also returnsDuration?.
Learn more
- Optional, Coalesce and Optional propagate — the three tools this lesson uses on every
Duration? - Checked arithmetic — the same idea, overflow reported instead of wrapped, on plain integers
- Stopwatch — measuring a duration with a clock
- Launch — a checkpoint project that counts down with durations
Overview
Durations, clocks and calendar dates, repeatable and unguessable random numbers, fast hashes and UUIDs: nine lessons on the small standard packages most programs reach for.
20.2 Stopwatch
Measure elapsed time with Instant, a clock that only moves forward, and Since, which refuses a reading pair given the wrong way round.