kex docs Standard Library 0.4.0-alpha kex.run ↗

Timeprelude

type Weekday

Calendar dates, wall-clock times, and instants.

Three civil types, each a plain record:

Date      a calendar day, no time and no zone       (2026-07-30)
Time      a time of day, no date and no zone        (14:03:00)
DateTime  both, plus a fixed offset from UTC        (2026-07-30T14:03:00`02:00)

Two span types connect them, and which one you want depends on whether the calendar gets a say:

Duration  fixed elapsed time, a count of seconds   (36.hours, 10.days)
Period    a calendar step, resolved by the calendar (1.months, 2.years)

36.hours is always 129600 seconds; 1.months is however long that particular month turns out to be. So date 1.months clamps January 31st to the last day of February, while date 30.days counts thirty days.

A time Measure such as 5.sec is a third thing and deliberately NOT a Duration: a Measure describes a measurement, a Duration describes elapsed time. The plural 5.seconds builds the Duration.

Values are built through their own module and used through methods:

let due = Date.of(2026, 7, 30).try           # Result<Date, TimeError>
due.weekday.name                             # "Thursday"
(due ` 10.days).iso                          # "2026-08-09"
(due ` 1.months).iso                         # "2026-08-30"
Time.now().iso                               # "2026-07-30T14:03:00`02:00"

Anything that reads the clock is mockable: see the test clock section in module Time for Time.frozenAt.

Zones are fixed offsets: UTC, an explicit +02:00, or whatever this machine's zone resolves to at a given instant. Named IANA zones and their DST rules are not modeled: Time.now() asks the host for the offset in effect at that moment, so it is right now, but it cannot say what the offset WILL be for some future local time.

The records and the two ADTs stay at file level so make blocks, callers, and every module here can see them.

Weekday names in ISO order (Monday is day 1).

Variants

  • Monday
  • Tuesday
  • Wednesday
  • Thursday
  • Friday
  • Saturday
  • Sunday

type TimeError

A field out of range, or text that is not a date/time.

Variants

  • InvalidDate(Integer, Integer, Integer)
  • InvalidTime(Integer, Integer, Integer)
  • InvalidFormat(String)

record Date

A calendar day: a year, a month and a day, with no time and no zone.

let due = Date.of(2026, 7, 30).try
due.iso              # => "2026-07-30"
due.weekday.name     # => "Thursday"
(due ` 10.days).iso  # => "2026-08-09"

Build one with `Date.of+, which validates, rather than with the record literal, which does not.

Fields

year
Integer
month
Integer
day
Integer

record Time

A time of day, with no date and no zone.

let t = Time.of(14, 3, 0).try
t.iso                  # => "14:03:00"
(t ` 2.hours).iso      # => "16:03:00"

Arithmetic wraps within the day: there is no date to carry into. Reach for `DateTime+ when the day rolling over matters.

Fields

hour
Integer
minute
Integer
second
Integer
nanosecond
Integer

record DateTime

An instant: a calendar date, a time of day, and a fixed offset from UTC.

let m = DateTime.parse("2026-07-30T14:03:00`02:00").try
m.iso        # => "2026-07-30T14:03:00`02:00"
m.utc.iso    # => "2026-07-30T12:03:00Z"

Two DateTime values that name the same instant compare equal whatever offsets they are written at: comparison goes through epochSeconds.

Fields

date
Date
time
Time
offset
Duration

record Period

A calendar span. Months and years have no fixed length: February is 28 days or 29, a year 365 or 366, so they cannot live in a Duration, which is a count of seconds and nothing else. A Period carries the calendar fields themselves and lets the calendar resolve them:

Date.of(2026, 1, 31).try ` 1.months        # 2026-02-28, not 2026-03-03
Date.of(2024, 2, 29).try ` 1.years         # 2025-02-28

Use a Duration for elapsed time (36.hours is always 129600 seconds) and a Period for calendar steps (1.months is however long that month is).

Fields

years
Integer
months
Integer
days
Integer

module Time

Building times of day, controlling the clock in tests, and the calendar arithmetic the rest of this file is written on.

function of

Builds a validated time of day.

Every field is range-checked, so a Time you hold is always a real time. Leap seconds are not modeled, so a second of 60 is rejected.

of(hour, minute, second)
Parameters
hour Integer
the hour, 0 through 23
minute Integer
the minute, 0 through 59
second Integer
the second, 0 through 59
nanosecond Integer
the sub-second part, 0 through 999999999; omitted for 0

Returns: Result<Time, TimeError> — the time, or why it is not one

Examples
Time.of(14, 3, 0).map { |t| t.iso }   # => Ok("14:03:00")
Time.of(25, 0, 0)                     # => Error(InvalidTime(25, 0, 0))

With sub-second precision

Time.of(14, 3, 0, 123456789).map { |t| t.iso }
# => Ok("14:03:00.123456789")

function midnight

Midnight, 00:00:00. The start of a day.

midnight()

Returns: Time — 00:00:00

Examples
Time.midnight().iso                   # => "00:00:00"
(Time.midnight() - 1.seconds).iso     # => "23:59:59"

function fromSecondsSinceMidnight

Builds a time of day from a count of seconds since midnight.

Wraps, so 86400 is midnight again and -1 is 23:59:59, which is what makes it total where Time.of is fallible.

Declared before the two-argument form: the interpreter resolves an overloaded module function to its LAST definition regardless of arity, so a delegating overload has to come first or it recurses into itself.

fromSecondsSinceMidnight(count)
Parameters
count Integer
seconds since midnight; wraps outside 0..86399
nanosecond Integer
the sub-second part; omitted for 0

Returns: Time — the time of day

Examples
Time.fromSecondsSinceMidnight(50580).iso   # => "14:03:00"
Time.fromSecondsSinceMidnight(86400).iso   # => "00:00:00"
Time.fromSecondsSinceMidnight(-1).iso      # => "23:59:59"

function parse

Parses an ISO 8601 time of day.

Accepts 14:03, 14:03:00, or 14:03:00.123456789. Anything else is InvalidFormat.

parse(text)
Parameters
text String
the text to parse

Returns: Result<Time, TimeError> — the time, or why it could not be read

Examples
Time.parse("14:03").map { |t| t.iso }   # => Ok("14:03:00")
Time.parse("xx")                        # => Error(InvalidFormat("xx"))

function now

The current time of day, in this machine's zone.

Reads the same clock primitive everything else here does, so it is pinned by Time.frozenAt in a test.

now()

Returns: Time — the current time of day

Examples
Time.now().iso   # => "14:03:00"

function utcNow

The current time of day in UTC, whatever this machine's zone is.

utcNow()

Returns: Time — the current UTC time of day

Examples
Time.utcNow().iso   # => "12:03:00"

function parseOffset

Parses an ISO 8601 zone designator into an offset.

Accepts `Z`, ``02:00`, `-05:30`, or the empty string (all meaning UTC for the first and last).

parseOffset(text)
Parameters
text String
the zone designator

Returns: Result<Duration, TimeError> — the offset, or why it could not be read

Examples
Time.parseOffset("`02:00").map { |d| d.wholeSeconds }   # => Ok(7200)
Time.parseOffset("Z").map { |d| d.wholeSeconds }        # => Ok(0)

function nanosOf

Anything that asks what time it is: Time.now, Date.today, DateTime.utcNow: reads one primitive, so pinning that primitive pins the whole calendar. This is what makes code that calls Date.today() testable: freeze the clock, assert against a date you chose.

Time.freeze(DateTime.parse("2026-07-30T14:03:00Z").try)
Date.today().iso                            # "2026-07-30": always
Time.release()

The clock is global, not per-process: a frozen clock stays frozen inside spawned processes, which is the only behavior that matches a real one. release is not automatic, so a test that freezes must also release: otherwise every later test in the run inherits the frozen clock.

Nanoseconds since the Unix epoch for a civil datetime. A plain function rather than a DateTime method: on BEAM a method named epochNanos flattens onto the same name as the DateTime.epochNanos() module function, and the arity-0 one wins: silently, answering for the host clock instead of for moment.

nanosOf(moment)

constant CLOCK_MIN_NANOS ?

The clock counts nanoseconds in a 64-bit integer, on both backends and in the host clock they stand in for. That is the whole of the instants it can name: 1677-09-21 to 2262-04-11. A Kex Integer keeps going past that: it promotes to arbitrary precision, so a date outside the range produces a number the clock cannot hold, and the check below is what stops it being truncated into some other instant entirely.

constant CLOCK_MAX_NANOS ?

function settable?

settable?(moment)

function freeze

Pins the clock: every reading returns this exact instant until release.

This is what makes code that calls Date.today() testable. Returns the moment it pinned, so Time.freeze(m).try both sets the clock and fails loudly on an instant the clock cannot represent.

Prefer Time.frozenAt, which releases for you: a test that fails between a freeze and its release leaves the clock frozen for everything after it.

freeze(moment)
Parameters
moment DateTime
the instant to pin the clock to

Returns: Result<DateTime, TimeError> — the pinned moment, or why it could not be

Examples
Time.freeze(DateTime.parse("2026-07-30T14:03:00Z").try)
Date.utcToday().iso   # => "2026-07-30", always
Time.release()

function travel

Moves the clock to an instant and lets it run from there.

Readings advance normally, they just start somewhere else. Use this over freeze when the code under test measures elapsed time: a frozen clock makes every interval zero.

travel(moment)
Parameters
moment DateTime
the instant to start the clock from

Returns: Result<DateTime, TimeError> — the moment set, or why it could not be

Examples
Time.travel(DateTime.parse("2026-07-30T14:03:00Z").try)
Time.release()

function frozenAt

Freezes the clock for the length of body, then releases it.

This is the form to reach for: freeze and release have to be paired by hand, and a test that returns early (or fails an assertion) between them leaves the clock frozen for every test that runs after it.

Result carries whatever body returned. An instant the clock cannot represent is an Error, and then the clock is never touched and the body never runs.

Not nestable: release restores the HOST clock, not whatever control was in effect on entry, so an inner scope ending un-freezes the outer one too.

frozenAt(moment, body)
Parameters
moment DateTime
the instant to pin the clock to
body Block
what to run with the clock frozen

Returns: Result<A, TimeError> — whatever body returned, or why the clock could not be set

function travellingFrom

Runs body with the clock started at moment, then releases it.

The same scoping as frozenAt, for travel: readings start at moment and advance normally, and the clock is released when body ends.

travellingFrom(moment, body)
Parameters
moment DateTime
the instant to start the clock from
body Block
what to run with the clock moved

Returns: Result<A, TimeError> — whatever body returned, or why the clock could not be set

Examples

Measuring elapsed time from a known start

Time.travellingFrom(DateTime.parse("2026-07-30T14:03:00Z").try) do
  runTheThing()
end.try

function release

Returns the clock to the host's.

Not automatic: a test that froze the clock must also release it, or every later test in the run inherits the frozen clock. frozenAt and travellingFrom do this for you.

release()

Returns: Void

Examples
after do
  Time.release()
end

function controlled?

Returns true while freeze or travel is in effect.

controlled?()

Returns: Booltrue when the clock is under test control

Examples
Time.controlled?()   # => false on the host clock

function frozen?

Returns true while freeze is in effect: not merely travel.

frozen?()

Returns: Booltrue when the clock is frozen

Examples
Time.frozen?()   # => false on the host clock

function leapYear?

Public because the make blocks below live at file level and reach them by qualification; they are equally useful on their own.

Returns true when year is a leap year in the proleptic Gregorian calendar.

leapYear?(year)
Parameters
year Integer
the calendar year

Returns: Booltrue for a leap year

Examples
Time.leapYear?(2024)   # => true
Time.leapYear?(1900)   # => false
Time.leapYear?(2000)   # => true

function daysInMonth

The number of days in a month.

Year first, matching Date.of(year, month, day) and every other date-shaped signature in this file.

A month outside 1..12 has no answer, so this is a Result rather than an Integer: the old version fell through its month tests and returned 28, which quietly turned Time.daysInMonth(1, 2026): the arguments the wrong way round: into a plausible-looking wrong number.

daysInMonth(year, month)
Parameters
year Integer
the calendar year, which decides February's length
month Integer
the month, 1 through 12

Returns: Result<Integer, TimeError> — the day count, or why the month is not one

Examples
Time.daysInMonth(2024, 2)   # => Ok(29)
Time.daysInMonth(2023, 2)   # => Ok(28)
Time.daysInMonth(2026, 13)  # => Error(InvalidDate(2026, 13, 1))

function daysInValidMonth

The number of days in a month, with the range check already done.

Every caller inside this file has a month it built or validated itself. Use daysInMonth for a month that came from outside.

daysInValidMonth(year, month)
Parameters
year Integer
the calendar year
month Integer
the month, assumed to be 1 through 12

Returns: Integer — the day count

Examples
Time.daysInValidMonth(2024, 2)   # => 29

function daysFromCivil

The number of days from 1970-01-01 to a calendar date, negative before it.

Howard Hinnant's civil-calendar algorithms: exact across the whole proleptic Gregorian range, and they need only truncating integer division: the semantics Kex's / already has.

daysFromCivil(year, month, day)
Parameters
year Integer
the calendar year
month Integer
the month, 1 through 12
day Integer
the day of the month

Returns: Integer — days since the Unix epoch

Examples
Time.daysFromCivil(1970, 1, 1)   # => 0
Time.daysFromCivil(2026, 7, 30)  # => 20664

function civilFromDays

The calendar date a count of days since 1970-01-01 lands on. The inverse of daysFromCivil.

civilFromDays(epochDay)
Parameters
epochDay Integer
days since the Unix epoch

Returns: Date — the calendar date

Examples
Time.civilFromDays(0).iso       # => "1970-01-01"
Time.civilFromDays(20664).iso   # => "2026-07-30"

function weekdayFromEpochDay

The weekday a count of days since 1970-01-01 falls on.

weekdayFromEpochDay(epochDay)
Parameters
epochDay Integer
days since the Unix epoch

Returns: Weekday — the day of the week

Examples
Time.weekdayFromEpochDay(0)   # => Thursday   (1970-01-01 was a Thursday)

function weekdayNumber

The ISO number of a weekday: Monday is 1, Sunday is 7.

weekday.number is the readable way to ask.

weekdayNumber(@Monday)
Parameters
weekday Weekday
the day of the week

Returns: Integer — its ISO number, 1 through 7

Examples
Time.weekdayNumber(Monday)   # => 1

function weekdayName

The English name of a weekday.

weekday.name is the readable way to ask.

weekdayName(@Monday)
Parameters
weekday Weekday
the day of the week

Returns: String — its English name

Examples
Time.weekdayName(Sunday)   # => "Sunday"

make Weekday

function errorMessage

Renders a TimeError as a sentence for the user.

A plain function rather than an Errorable implementation: a message method here joins the same BEAM dispatcher as ParseError's message FIELD and breaks it (spec/record_field_method_collision.kex).

errorMessage(@InvalidDate(y, m, d))
Parameters
error TimeError
the failure to describe

Returns: String — the message to show

Examples
Time.errorMessage(InvalidDate(2026, 13, 1))
# => "not a calendar date: 2026-13-1"

Reporting a rejected date

match Date.parse(input) do
  Ok(date) => IO.printLine(date.iso)
  Error(e) => IO.printError(Time.errorMessage(e))
end

function formatDate

formatDate(value)

function formatDateTime

formatDateTime(value)

function formatTime

formatTime(value)

function formatFraction

Fractional seconds, in the 3/6/9-digit groupings ISO 8601 output conventionally uses, whichever is the shortest that loses nothing. A whole second renders no fraction at all, so 14:03:00 is unchanged.

Without this the nanosecond field was kept on the value and compared, but never rendered: Time.parse("14:03:00.5") and Time.parse("14:03:00") produced different values that printed identically, and every parse/format round-trip silently dropped sub-second precision.

formatFraction(nanosecond)

function formatOffset

±HH:MM, the shape an ISO 8601 offset takes. UTC renders as "Z".

formatOffset(offset)

function withNanosecond

withNanosecond(moment, nanosecond)

function floorDiv

Kex's / truncates toward zero; instants before the epoch need the floor.

floorDiv(value, divisor)

function truncatedBy

truncatedBy(seconds, unit)

function pad2

pad2(value)

function padTo

Left-pad with zeros to a fixed width. A value already that wide is left alone rather than truncated: losing digits would be worse than a field one character too long.

padTo(value, width)

function padYear

Years keep four digits where they fit; ISO 8601 has no fixed spelling beyond that, so wider years render as-is.

padYear(value)

function digitsIn

Digits, with the failure reported against the WHOLE input rather than the fragment that failed: Time.parse("2:03 pm") should complain about "2:03 pm", not about "03 pm".

digitsIn(fragment, whole)

function digitsToInteger

digitsToInteger(text)

function parseFraction

".5" is 500000000ns: the digits are padded out to nanosecond scale.

parseFraction(text, whole)

function splitOffset

Splits "14:03:00+02:00" into its time and offset halves. A missing offset reads as UTC, matching what a zero offset formats back to.

splitOffset(text)

module Date

Building calendar dates, and asking what today is.

function of

Builds a validated calendar date.

The month and the day are both range-checked, and the day is checked against that month's actual length, so February 30th is an Error, and a Date you hold is always a real day. The record literal Date { ... } bypasses this, so prefer it for anything derived from input.

of(year, month, day)
Parameters
year Integer
the calendar year
month Integer
the month, 1 through 12
day Integer
the day of the month

Returns: Result<Date, TimeError> — the date, or why it is not one

Examples
Date.of(2026, 7, 30).map { |d| d.iso }   # => Ok("2026-07-30")
Date.of(2026, 13, 1)                     # => Error(InvalidDate(2026, 13, 1))
Date.of(2023, 2, 29)                     # => Error(InvalidDate(2023, 2, 29))

Taking the date or failing loudly

let due = Date.of(year, month, day).try

function fromEpochDay

The calendar date a count of days since 1970-01-01 lands on, negative before it.

fromEpochDay(day)
Parameters
day Integer
days since the Unix epoch

Returns: Date — the calendar date

Examples
Date.fromEpochDay(0).iso       # => "1970-01-01"
Date.fromEpochDay(20664).iso   # => "2026-07-30"

function parse

Parses an ISO 8601 calendar date, 2026-07-30.

The result is validated as well as parsed, so a well-formed but impossible date is InvalidDate rather than InvalidFormat.

parse(text)
Parameters
text String
the text to parse

Returns: Result<Date, TimeError> — the date, or why it could not be read

Examples
Date.parse("2026-07-30").map { |d| d.iso }   # => Ok("2026-07-30")
Date.parse("nope")                           # => Error(InvalidFormat("nope"))
Date.parse("2026-02-30")                     # => Error(InvalidDate(2026, 2, 30))

function now

Today's date, in this machine's zone.

Date.today() reads better in most code; now exists so every type in this file answers the same question the same way.

now()

Returns: Date — today's date

Examples
Date.now().iso   # => "2026-07-30"

function today

Today's date, in this machine's zone.

Pinned by Time.frozenAt in a test, like everything else that reads the clock.

today()

Returns: Date — today's date

Examples
Date.today().iso   # => "2026-07-30"

Testing code that depends on today

Time.frozenAt(DateTime.parse("2026-07-30T00:00:00Z").try) do
  Assert.equal(Date.utcToday().iso, "2026-07-30")
end.try

function tomorrow

The day after today, in this machine's zone.

tomorrow()

Returns: Date — tomorrow's date

Examples
Date.tomorrow().iso   # => "2026-07-31"

function yesterday

The day before today, in this machine's zone.

yesterday()

Returns: Date — yesterday's date

Examples
Date.yesterday().iso   # => "2026-07-29"

function utcNow

Today's date in UTC, whatever this machine's zone is.

utcNow()

Returns: Date — today's UTC date

Examples
Date.utcNow().iso   # => "2026-07-30"

function utcToday

Today's date in UTC. The same as Date.utcNow(), under the name that reads better.

utcToday()

Returns: Date — today's UTC date

Examples
Date.utcToday().iso   # => "2026-07-30"

module DateTime

Building instants, and asking what time it is now.

function of

Combines a date, a time of day and a UTC offset into an instant.

of(date, time, offset)
Parameters
date Date
the calendar date
time Time
the time of day
offset Duration
the offset from UTC

Returns: DateTime — the instant

Examples
DateTime.of(Date.of(2026, 7, 30).try,
            Time.of(9, 0, 0).try,
            Duration.zero()).iso
# => "2026-07-30T09:00:00Z"

At an offset

DateTime.of(date, time, Duration.utcOffset(2, 0))

function fromEpochSeconds

The instant a count of seconds since the Unix epoch names, rendered at UTC or at the offset you give.

Declared before the two-argument form: the interpreter resolves an overloaded module function to its LAST definition regardless of arity, so a delegating overload has to come first or it recurses into itself.

fromEpochSeconds(count)
Parameters
count Integer
seconds since the Unix epoch
offset Duration
the offset to render at; omitted for UTC

Returns: DateTime — the instant

Examples
DateTime.fromEpochSeconds(0).iso   # => "1970-01-01T00:00:00Z"

Rendered at a local offset

DateTime.fromEpochSeconds(0, Duration.utcOffset(2, 0)).iso
# => "1970-01-01T02:00:00+02:00"

function parse

Parses an ISO 8601 instant.

Accepts `2026-07-30T14:03:00`02:00`, the same with `Z`, or a bare civil datetime with no zone at all, which is read as UTC.

parse(text)
Parameters
text String
the text to parse

Returns: Result<DateTime, TimeError> — the instant, or why it could not be read

Examples
DateTime.parse("2026-07-30T14:03:00`02:00").map { |m| m.utc.iso }
# => Ok("2026-07-30T12:03:00Z")
DateTime.parse("nope")   # => Error(InvalidFormat("nope"))

Reading a timestamp out of a log line

DateTime.parse(line.take(20)).map { |m| m.epochSeconds }

function now

The current instant, in this machine's zone as it stands right now.

The offset is the one in effect at this instant, so it is right today. Named IANA zones are not modeled, so it cannot say what the offset WILL be for some future local time.

now()

Returns: DateTime — the current instant

Examples
DateTime.now().iso   # => "2026-07-30T14:03:00+02:00"

function utcNow

The current instant, at UTC.

The form to prefer when the value is stored, compared or transmitted: there is no zone to disagree about.

utcNow()

Returns: DateTime — the current instant, at UTC

Examples
DateTime.utcNow().iso   # => "2026-07-30T12:03:00Z"

function epochNanos

Nanoseconds since the Unix epoch, straight from the clock.

The rawest reading available, and the right one for measuring a short interval: no calendar work happens on the way.

epochNanos()

Returns: Integer — nanoseconds since the Unix epoch

Examples

Timing a piece of work

let started = DateTime.epochNanos()
doTheThing()
let elapsedMs = (DateTime.epochNanos() - started) / 1000000

make Integer

The plural spellings build a Duration; the singular ones from units.kex build a time Measure. 5.seconds is an elapsed span, 5.sec a measurement.

make Float

The same Duration constructors on Float, for fractional spans: 1.5.hours, 0.25.seconds.

make Duration

+

Adds two spans.

+(other)
Parameters
other Duration
the span to add

Returns: Duration — the total

Examples
(90.minutes + 30.minutes).wholeHours   # => 2

-

Subtracts a span. The result may be negative.

-(other)
Parameters
other Duration
the span to subtract

Returns: Duration — the difference

Examples
(1.hours - 90.minutes).negative?   # => true

*

Multiplies the span by a plain number.

3 * 1.days is not the same call because the receiver has to be the Duration, so it is spelled 1.days * 3.

*(factor)
Parameters
factor Number
the factor to multiply by

Returns: Duration — the scaled span

Examples
(90.minutes * 2).wholeHours   # => 3

/

Divides the span by a plain number.

/(divisor)
Parameters
divisor Number
the number to divide by

Returns: Duration — the scaled span

Examples
(90.minutes / 2).wholeMinutes   # => 45

shorterThan?

Returns true when this span is shorter than other.

Named for length rather than for order: before?/after? are about when something happened, and a Duration is not a point in time.

shorterThan?(other)
Parameters
other Duration
the span to compare against

Returns: Booltrue when this span is shorter

Examples
30.minutes.shorterThan?(1.hours)   # => true

Enforcing a timeout

if started.until(DateTime.utcNow()).longerThan?(30.seconds)
  IO.printError("timed out")
end

longerThan?

Returns true when this span is longer than other.

longerThan?(other)
Parameters
other Duration
the span to compare against

Returns: Booltrue when this span is longer

Examples
90.minutes.longerThan?(1.hours)   # => true

compareTo

Orders this span against another by length.

Delegates to Number.compare (algebra.kex), which orders the two Float second counts.

compareTo(other)
Parameters
other Duration
the span to compare against

Returns: OrderingLess, Equal or Greater

Examples
90.minutes.compareTo(60.minutes)   # => Greater

Sorting by length

spans.sort { |a, b| a.compareTo(b) == Less }

module Duration

Building elapsed spans, and UTC offsets.

function zero

A span of no time at all. Also the UTC offset.

zero()

Returns: Duration — the zero span

Examples
Duration.zero().zero?   # => true
date.at(time, Duration.zero()).iso   # => "...T09:00:00Z"

function milliseconds

A span of count milliseconds.

milliseconds(count)
Parameters
count Number
how many milliseconds

Returns: Duration — the elapsed span

Examples
Duration.milliseconds(1500).wholeSeconds   # => 1

function seconds

A span of count seconds.

seconds(count)
Parameters
count Number
how many seconds

Returns: Duration — the elapsed span

Examples
Duration.seconds(90).wholeMinutes   # => 1

function minutes

A span of count minutes.

minutes(count)
Parameters
count Number
how many minutes

Returns: Duration — the elapsed span

Examples
Duration.minutes(90).wholeHours   # => 1

function hours

A span of count hours.

hours(count)
Parameters
count Number
how many hours

Returns: Duration — the elapsed span

Examples
Duration.hours(2).wholeSeconds   # => 7200

function days

A span of count days, each a fixed 86400 seconds.

days(count)
Parameters
count Number
how many days

Returns: Duration — the elapsed span

Examples
Duration.days(2).wholeHours   # => 48

function weeks

A span of count weeks, each a fixed 604800 seconds.

weeks(count)
Parameters
count Number
how many weeks

Returns: Duration — the elapsed span

Examples
Duration.weeks(2).wholeDays   # => 14

function utcOffset

A whole-minute UTC offset, the only kind ISO 8601 can spell.

A negative hour or minute puts the whole offset west of UTC, so utcOffset(-5, 30) is five and a half hours behind UTC, not four and a half.

utcOffset(hours, minutes)
Parameters
hours Integer
the hour part of the offset
minutes Integer
the minute part of the offset

Returns: Duration — the offset

Examples
Duration.utcOffset(2, 0).wholeSeconds     # => 7200
Duration.utcOffset(-5, 0).wholeSeconds    # => -18000

Rendering an instant at another offset

moment.at(Duration.utcOffset(-5, 0)).iso

module Period

Building calendar spans.

function zero

A span of nothing.

zero()

Returns: Period — the zero span

Examples
Period.zero().iso     # => "P0D"
Period.zero().zero?   # => true

function of

A span of the given years, months and days together.

of(years, months, days)
Parameters
years Integer
whole years
months Integer
whole months
days Integer
whole days

Returns: Period — the calendar span

Examples
Period.of(1, 2, 3).iso   # => "P1Y2M3D"

function years

A span of count calendar years.

years(count)
Parameters
count Integer
how many years

Returns: Period — the calendar span

Examples
(Date.of(2024, 2, 29).try + Period.years(1)).iso   # => "2025-02-28"

function months

A span of count calendar months.

months(count)
Parameters
count Integer
how many months

Returns: Period — the calendar span

Examples
(Date.of(2026, 1, 31).try + Period.months(1)).iso   # => "2026-02-28"

function days

A span of count days, as a calendar step.

days(count)
Parameters
count Integer
how many days

Returns: Period — the calendar span

Examples
Period.days(10).iso   # => "P10D"

function weeks

A span of count weeks, recorded as that many times seven days.

weeks(count)
Parameters
count Integer
how many weeks

Returns: Period — the calendar span

Examples
Period.weeks(2).iso   # => "P14D"

make Period implements Inspectable, Showable

inspectValue

Renders the period structurally, for debugging output.

inspectValue(colors)
Parameters
colors Bool
whether to include ANSI color escapes

Returns: String — the rendered period

+

Adds two calendar spans, field by field.

+(other)
Parameters
other Period
the span to add

Returns: Period — the total

Examples
(Period.of(1, 2, 3) + 1.years).iso   # => "P2Y2M3D"

-

Subtracts a calendar span, field by field. Fields may go negative.

-(other)
Parameters
other Period
the span to subtract

Returns: Period — the difference

Examples
(Period.of(1, 2, 3) - 1.years).iso   # => "P2M3D"

*

Multiplies every field by factor.

*(factor)
Parameters
factor Integer
the factor to multiply by

Returns: Period — the scaled span

Examples
(Period.of(1, 2, 3) * 2).iso   # => "P2Y4M6D"

make Date implements Inspectable, Showable

inspectValue

Renders the date structurally, for debugging output.

inspectValue(colors)
Parameters
colors Bool
whether to include ANSI color escapes

Returns: String — the rendered date

+

Advances the date by a fixed span, whole days only.

A Duration with a sub-day remainder truncates toward zero, so `date ` 36.hours` advances exactly one day. Use a `Period` when the calendar should get a say.

+(span)
Parameters
span Duration
the elapsed span to add

Returns: Date — the later date

Examples
(Date.of(2026, 7, 30).try ` 10.days).iso   # => "2026-08-09"
(Date.of(2026, 7, 30).try + 36.hours).iso  # => "2026-07-31"

-

Moves the date back by a fixed span, whole days only.

-(span)
Parameters
span Duration
the elapsed span to subtract

Returns: Date — the earlier date

Examples
(Date.of(2026, 7, 30).try - 10.days).iso   # => "2026-07-20"

addDays

The date count days later. A negative count moves backwards.

addDays(count)
Parameters
count Integer
how many days to add

Returns: Date — the shifted date

Examples
Date.of(2026, 7, 30).try.addDays(1).iso    # => "2026-07-31"
Date.of(2026, 7, 30).try.addDays(-1).iso   # => "2026-07-29"

addWeeks

The date count weeks later. A negative count moves backwards.

addWeeks(count)
Parameters
count Integer
how many weeks to add

Returns: Date — the shifted date

Examples
Date.of(2026, 7, 30).try.addWeeks(2).iso   # => "2026-08-13"

addMonths

No date.tomorrow/date.yesterday methods: on BEAM a make-block method flattens onto the same name as the Date.tomorrow()/Date.yesterday() module functions above and one of the two has to win. The module functions win: Date.tomorrow() is the spelling people reach for, and date.addDays(1) already says the rest.

The date count calendar months later, with the day clamped into the target month.

One month after January 31st is the last day of February, not March 3rd. A negative count moves backwards.

addMonths(count)
Parameters
count Integer
how many months to add

Returns: Date — the shifted date

Examples
Date.of(2026, 1, 31).try.addMonths(1).iso    # => "2026-02-28"
Date.of(2026, 7, 30).try.addMonths(-1).iso   # => "2026-06-30"

addYears

The date count calendar years later, with the day clamped: February 29th plus one year is February 28th.

addYears(count)
Parameters
count Integer
how many years to add

Returns: Date — the shifted date

Examples
Date.of(2024, 2, 29).try.addYears(1).iso   # => "2025-02-28"

daysUntil

Whole days from this date to other, negative when other is earlier.

daysUntil(other)
Parameters
other Date
the date to measure to

Returns: Integer — the number of days

Examples
Date.of(2026, 7, 30).try.daysUntil(Date.of(2026, 8, 9).try)   # => 10
Date.of(2026, 8, 9).try.daysUntil(Date.of(2026, 7, 30).try)   # => -10

until

The span from this date to other, as a Duration of whole days.

until(other)
Parameters
other Date
the date to measure to

Returns: Duration — the elapsed span

Examples
Date.of(2026, 7, 30).try.until(Date.of(2026, 8, 9).try).wholeDays   # => 10

monthsUntil

Whole calendar months from this date to other, negative when other is earlier.

Truncated, not rounded: a partial month does not count, so January 15th to February 14th is 0 months.

The count is the exact inverse of addMonths, which is why the correction below asks addMonths rather than comparing day-of-month fields: January 31st plus one month IS February 28th, so January 31st to February 28th is one month, even though 28 < 31. Comparing the day fields answers 0 there and contradicts the addition this same file performs.

monthsUntil(other)
Parameters
other Date
the date to measure to

Returns: Integer — the number of whole months

Examples
Date.of(2026, 1, 15).try.monthsUntil(Date.of(2026, 2, 14).try)   # => 0
Date.of(2026, 1, 31).try.monthsUntil(Date.of(2026, 2, 28).try)   # => 1

yearsUntil

Whole calendar years from this date to other, negative when other is earlier. Truncated, like monthsUntil.

This is how to compute an age.

yearsUntil(other)
Parameters
other Date
the date to measure to

Returns: Integer — the number of whole years

Examples
Date.of(2020, 1, 1).try.yearsUntil(Date.of(2026, 7, 30).try)   # => 6

Someone's age today

born.yearsUntil(Date.today())

before?

Returns true when this date is earlier than other.

before?(other)
Parameters
other Date
the date to compare against

Returns: Booltrue when this date comes first

Examples
Date.of(2026, 7, 30).try.before?(Date.of(2026, 8, 1).try)   # => true

after?

Returns true when this date is later than other.

after?(other)
Parameters
other Date
the date to compare against

Returns: Booltrue when this date comes second

Examples
Date.of(2026, 8, 1).try.after?(Date.of(2026, 7, 30).try)   # => true

compareTo

Orders this date against another.

compareTo(other)
Parameters
other Date
the date to compare against

Returns: OrderingLess, Equal or Greater

Examples
Date.of(2026, 7, 30).try.compareTo(Date.of(2026, 8, 1).try)   # => Less

Sorting dates

dates.sort { |a, b| a.compareTo(b) == Less }

at

This date at a given time of day and offset, as a DateTime.

at(time, offset)
Parameters
time Time
the time of day
offset Duration
the offset from UTC

Returns: DateTime — the instant

Examples
Date.of(2026, 7, 30).try.at(Time.of(9, 0, 0).try, Duration.zero()).iso
# => "2026-07-30T09:00:00Z"

make Time implements Inspectable, Showable

inspectValue

Renders the time structurally, for debugging output.

inspectValue(colors)
Parameters
colors Bool
whether to include ANSI color escapes

Returns: String — the rendered time

before?

Returns true when this time of day is earlier than other.

before?(other)
Parameters
other Time
the time to compare against

Returns: Booltrue when this time comes first

Examples
Time.of(14, 3, 0).try.before?(Time.of(15, 0, 0).try)   # => true

after?

Returns true when this time of day is later than other.

after?(other)
Parameters
other Time
the time to compare against

Returns: Booltrue when this time comes second

Examples
Time.of(15, 0, 0).try.after?(Time.of(14, 3, 0).try)   # => true

+

Advances the time of day by a span, wrapping within the day.

A Time has no date to carry into, so 23:00 ` 2.hours is 01:00. Reach for `DateTime` when the day rolling over is something you need to see.

The nanosecond field rides along untouched: wholeSeconds truncates the span, so a sub-second Duration shifts nothing.

+(span)
Parameters
span Duration
the elapsed span to add

Returns: Time — the later time of day

Examples
(Time.of(14, 3, 0).try ` 2.hours).iso    # => "16:03:00"
(Time.of(23, 0, 0).try + 2.hours).iso    # => "01:00:00"

-

Moves the time of day back by a span, wrapping within the day.

-(span)
Parameters
span Duration
the elapsed span to subtract

Returns: Time — the earlier time of day

Examples
(Time.midnight() - 1.seconds).iso   # => "23:59:59"

addSeconds

The time of day count seconds later, wrapping within the day.

addSeconds(count)
Parameters
count Integer
how many seconds to add

Returns: Time — the shifted time of day

Examples
Time.of(14, 3, 0).try.addSeconds(60).iso   # => "14:04:00"

addMinutes

The time of day count minutes later, wrapping within the day.

addMinutes(count)
Parameters
count Integer
how many minutes to add

Returns: Time — the shifted time of day

Examples
Time.of(14, 3, 0).try.addMinutes(30).iso   # => "14:33:00"

addHours

The time of day count hours later, wrapping within the day.

addHours(count)
Parameters
count Integer
how many hours to add

Returns: Time — the shifted time of day

Examples
Time.of(23, 0, 0).try.addHours(2).iso   # => "01:00:00"

until

Elapsed time from this time of day to other, within the same day.

Negative when other is earlier. Sub-second precision is kept.

until(other)
Parameters
other Time
the time to measure to

Returns: Duration — the elapsed span

Examples
Time.of(14, 3, 0).try.until(Time.of(15, 3, 0).try).wholeMinutes   # => 60

compareTo

Orders this time of day against another, nanoseconds included.

compareTo(other)
Parameters
other Time
the time to compare against

Returns: OrderingLess, Equal or Greater

Examples
Time.of(14, 3, 0).try.compareTo(Time.of(15, 0, 0).try)   # => Less

make DateTime implements Inspectable, Showable

inspectValue

Renders the instant structurally, for debugging output.

inspectValue(colors)
Parameters
colors Bool
whether to include ANSI color escapes

Returns: String — the rendered instant

at

The same instant, rendered at another offset.

Nothing moves: the wall clock changes because the offset does, and epochSeconds is unchanged.

at(offset)
Parameters
offset Duration
the offset to render at

Returns: DateTime — the same instant, at that offset

Examples
DateTime.parse("2026-07-30T14:03:00+02:00").try
  .at(Duration.utcOffset(-5, 0)).iso
# => "2026-07-30T07:03:00-05:00"

+

Advances the instant by a fixed span, keeping its offset.

+(span)
Parameters
span Duration
the elapsed span to add

Returns: DateTime — the later instant

Examples
(DateTime.parse("2026-07-30T14:03:00Z").try + 90.minutes).iso
# => "2026-07-30T15:33:00Z"

-

Moves the instant back by a fixed span, keeping its offset.

-(span)
Parameters
span Duration
the elapsed span to subtract

Returns: DateTime — the earlier instant

Examples
(DateTime.utcNow() - 24.hours).iso   # yesterday, same wall clock

addDays

The instant count days later, keeping the wall clock and the offset.

addDays(count)
Parameters
count Integer
how many days to add

Returns: DateTime — the shifted instant

Examples
DateTime.parse("2026-07-30T14:03:00Z").try.addDays(1).iso
# => "2026-07-31T14:03:00Z"

addWeeks

The instant count weeks later, keeping the wall clock and the offset.

addWeeks(count)
Parameters
count Integer
how many weeks to add

Returns: DateTime — the shifted instant

Examples
DateTime.parse("2026-07-30T14:03:00Z").try.addWeeks(1).iso
# => "2026-08-06T14:03:00Z"

addMonths

The instant count calendar months later, with the day clamped into the target month.

addMonths(count)
Parameters
count Integer
how many months to add

Returns: DateTime — the shifted instant

Examples
DateTime.parse("2026-01-31T09:00:00Z").try.addMonths(1).iso
# => "2026-02-28T09:00:00Z"

addYears

The instant count calendar years later, with the day clamped.

addYears(count)
Parameters
count Integer
how many years to add

Returns: DateTime — the shifted instant

Examples
DateTime.parse("2024-02-29T09:00:00Z").try.addYears(1).iso
# => "2025-02-28T09:00:00Z"

until

Elapsed time from this instant to other, negative when other is earlier. Sub-second precision is kept.

until(other)
Parameters
other DateTime
the instant to measure to

Returns: Duration — the elapsed span

Examples
started.until(DateTime.utcNow()).wholeSeconds

Enforcing a deadline

if started.until(DateTime.utcNow()).longerThan?(30.seconds)
  IO.printError("timed out")
end

before?

Returns true when this instant is earlier than other, whatever offsets they are written at.

before?(other)
Parameters
other DateTime
the instant to compare against

Returns: Booltrue when this instant comes first

Examples
DateTime.parse("2026-07-30T14:03:00+02:00").try
  .before?(DateTime.parse("2026-07-31T00:00:00Z").try)
# => true

after?

Returns true when this instant is later than other.

after?(other)
Parameters
other DateTime
the instant to compare against

Returns: Booltrue when this instant comes second

Examples
DateTime.utcNow().after?(started)   # => true

compareTo

Orders this instant against another, by instant rather than by wall clock, so 12:00Z and 14:00`02:00 compare `Equal`.

Named compareTo rather than compare: a make-block compare is shadowed by the builtin comparison dispatch and fails at runtime on both backends.

compareTo(other)
Parameters
other DateTime
the instant to compare against

Returns: OrderingLess, Equal or Greater

Examples
DateTime.parse("2026-07-30T12:00:00Z").try
  .compareTo(DateTime.parse("2026-07-30T14:00:00`02:00").try)
# => Equal

Sorting events by when they happened

events.sort { |a, b| a.at.compareTo(b.at) == Less }