datetime
stdlib/extra/datetime.hk: a pure-Hanki calendar, clock, and duration library.
An offset-based datetime library in the spirit of Rust's chrono, JavaScript's Temporal, and Python's arrow. Everything here is pure arithmetic over the arbitrary-precision int tier. Howard Hinnant's civil-calendar algorithms make the day/month/year conversions total and exact (no overflow, no year bounds), int's // being floor division and its % taking the divisor's sign. The only effectful seam is the host, delegated to time: the wall clock via time.now_ms!(), and the machine's own UTC offset via time.local_offset_seconds_at!().
The model, smallest to largest:
Instant: an absolute point on the UTC timeline (epoch nanoseconds).Duration: a signed elapsed span in nanoseconds, never calendar months or years.Date: a proleptic-Gregorian calendar day, no time, no zone.Time: a wall-clock time of day, no date.DateTime: aDateand aTime, still no offset.Offset: a fixed offset from UTC (+05:30,Z).OffsetDateTime: anInstantviewed through anOffset, the RFC 3339 timestamp.
Scope: fixed UTC offsets only. Named IANA zones (America/New_York) and their daylight-saving transitions need an embedded, yearly-updated tz database, and are left to a community library. What remains is the chrono-core equivalent, complete for RFC 3339 and ISO 8601. local_offset_at! is not an exception to that: it asks the host for its own offset, the one zone the machine already knows, and hands back a fixed Offset like any other. No zone can be named, and none is stored.
Internal precision is nanoseconds everywhere, even though the host clock resolves to milliseconds.
nanosper_second
_nanos_per_second: int = 1000000000
nanosper_minute
_nanos_per_minute: int = 60000000000
nanosper_hour
_nanos_per_hour: int = 3600000000000
nanosper_day
_nanos_per_day: int = 86400000000000
secondsper_day
_seconds_per_day: int = 86400
maxnanosecond
_max_nanosecond: int = 999999999
minseam_second
_min_seam_second: int = -9223372036854775808
The widest epoch second the time seam takes. An Instant counts unbounded int nanoseconds, and a second outside this band would truncate on the way through to_i64 and come back with a plausible offset for an instant nobody asked about.
maxseam_second
_max_seam_second: int = 9223372036854775807
RangeError
struct RangeError
field: string
value: int
min: int
max: int
end
A component handed to a validating constructor fell outside its allowed range. field names the component, value is what was supplied, and [min, max] is the inclusive band it had to land in.
impl Display<RangeError>
to_string
def to_string(self) -> string
Renders the failure as <field> <value> out of range <min>..=<max>.
RangeError(field="day", value=30, min=1, max=28).to_string() => "day 30 out of range 1..=28"
impl Eq<RangeError>
eq?
def eq?(self, other: Self) -> bool
Two range failures are equal when the field, the value, and both bounds match.
RangeError(field="day", value=30, min=1, max=28).eq?(RangeError(field="day", value=30, min=1, max=28)) => true
Weekday
type Weekday
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
Sunday
end
A day of the week. number() follows ISO 8601 (Monday = 1 … Sunday = 7).
impl Weekday
number
prop number(self) -> int
The ISO 8601 weekday number: Monday = 1 … Sunday = 7.
Monday.number => 1
Sunday.number => 7
from_number
def from_number(n: int) -> Option<Weekday>
The Weekday for an ISO number (1 = Monday … 7 = Sunday); None outside 1..=7.
Weekday.from_number(6) => Some(Saturday)
Weekday.from_number(0) => None
name
prop name(self) -> string
The full English name.
Wednesday.name => "Wednesday"
short_name
prop short_name(self) -> string
The three-letter English abbreviation.
Wednesday.short_name => "Wed"
impl Display<Weekday>
to_string
def to_string(self) -> string
Renders the day's full English name.
Friday.to_string() => "Friday"
impl Eq<Weekday>
eq?
def eq?(self, other: Self) -> bool
Two weekdays are equal when they are the same day.
Monday.eq?(Monday) => true
Monday.eq?(Tuesday) => false
Month
type Month
January
February
March
April
May
June
July
August
September
October
November
December
end
A calendar month. number() is 1 (January) … 12 (December).
impl Month
number
prop number(self) -> int
The month number, 1 (January) … 12 (December).
January.number => 1
December.number => 12
from_number
def from_number(n: int) -> Option<Month>
The Month for a number 1..=12; None otherwise.
Month.from_number(7) => Some(July)
Month.from_number(13) => None
name
prop name(self) -> string
The full English name.
July.name => "July"
short_name
prop short_name(self) -> string
The three-letter English abbreviation.
September.short_name => "Sep"
days_in
def days_in(self, year: int) -> int
The number of days this month has in year (accounts for leap years in February).
February.days_in(2024) => 29
February.days_in(2026) => 28
impl Display<Month>
to_string
def to_string(self) -> string
Renders the month's full English name.
March.to_string() => "March"
impl Eq<Month>
eq?
def eq?(self, other: Self) -> bool
Two months are equal when they are the same month.
July.eq?(July) => true
July.eq?(May) => false
IsoWeek
struct IsoWeek
week_year: int
week: int
end
An ISO 8601 week date coordinate: the week-numbering year (which may differ from the calendar year at year boundaries) and the week number 1..=53.
Duration
opaque Duration
nanos: int
end
A signed span of elapsed time, exact to the nanosecond. This is a time-based duration, a fixed count of nanoseconds, and no calendar-based one: a "day" here is 86 400 seconds, and there are no month or year spans at all, their length being variable. Use Date.add_months / add_years for calendar shifts.
impl Duration
zero
def zero() -> Duration
The zero duration.
Duration.zero().zero? => true
of_nanos
def of_nanos(n: int) -> Duration
A duration of n nanoseconds.
Duration.of_nanos(1500).total_nanos => 1500
of_micros
def of_micros(n: int) -> Duration
A duration of n microseconds.
Duration.of_micros(2).total_nanos => 2000
of_millis
def of_millis(n: int) -> Duration
A duration of n milliseconds.
Duration.of_millis(2).total_nanos => 2000000
of_seconds
def of_seconds(n: int) -> Duration
A duration of n seconds.
Duration.of_seconds(90).total_minutes => 1
of_minutes
def of_minutes(n: int) -> Duration
A duration of n minutes.
Duration.of_minutes(60).total_hours => 1
of_hours
def of_hours(n: int) -> Duration
A duration of n hours.
Duration.of_hours(25).total_days => 1
of_days
def of_days(n: int) -> Duration
A duration of n days, at 86 400 seconds each.
Duration.of_days(1).total_hours => 24
of_weeks
def of_weeks(n: int) -> Duration
A duration of n weeks, at 7 x 86 400 seconds each.
Duration.of_weeks(1).total_days => 7
total_nanos
prop total_nanos(self) -> int
The total signed nanoseconds.
Duration.of_millis(3).total_nanos => 3000000
total_micros
prop total_micros(self) -> int
The whole microseconds, truncated toward zero.
Duration.of_nanos(2500).total_micros => 2
total_millis
prop total_millis(self) -> int
The whole milliseconds, truncated toward zero.
Duration.of_nanos(2500000).total_millis => 2
total_seconds
prop total_seconds(self) -> int
The whole seconds, truncated toward zero.
Duration.of_millis(2500).total_seconds => 2
Duration.of_millis(-2500).total_seconds => -2
total_minutes
prop total_minutes(self) -> int
The whole minutes, truncated toward zero.
Duration.of_seconds(150).total_minutes => 2
total_hours
prop total_hours(self) -> int
The whole hours, truncated toward zero.
Duration.of_minutes(150).total_hours => 2
total_days
prop total_days(self) -> int
The whole days, truncated toward zero.
Duration.of_hours(50).total_days => 2
add
def add(self, other: Duration) -> Duration
Sum of two durations.
Duration.of_hours(1).add(Duration.of_minutes(30)).total_minutes => 90
sub
def sub(self, other: Duration) -> Duration
Difference of two durations.
Duration.of_hours(1).sub(Duration.of_minutes(30)).total_minutes => 30
negate
def negate(self) -> Duration
The additive inverse.
Duration.of_seconds(5).negate().total_seconds => -5
abs
def abs(self) -> Duration
The absolute (non-negative) magnitude.
Duration.of_seconds(-5).abs().total_seconds => 5
mul
def mul(self, factor: int) -> Duration
Scale by an integer factor.
Duration.of_seconds(3).mul(4).total_seconds => 12
div
def div(self, divisor: int) -> Option<Duration>
Divide by an integer, truncating toward zero; None on a zero divisor.
Duration.of_seconds(10).div(3).map(|d| d.total_millis) => Some(3333)
Duration.of_seconds(10).div(0) => None
zero?
prop zero?(self) -> bool
Whether the span is zero.
Duration.of_seconds(0).zero? => true
negative?
prop negative?(self) -> bool
Whether the span is negative (points backward in time).
Duration.of_seconds(-1).negative? => true
to_iso
def to_iso(self) -> string
The ISO 8601 duration string, e.g. PT1H30M, P2DT3H, PT0S. A negative span gets a leading -. Sub-second amounts render as a fraction of the seconds field. Days are the largest unit (calendar months/years are never emitted).
Duration.of_minutes(90).to_iso() => "PT1H30M"
Duration.zero().to_iso() => "PT0S"
ago!
def ago!(self) -> Instant [time]
The instant this duration before now (now!().sub(self)). The Rails ago, to pair with a fluent unit: 3.hours.ago!(). @no-doctest: reads the live clock; result is host-dependent
from_now!
def from_now!(self) -> Instant [time]
The instant this duration after now (now!().add(self)), e.g. 2.days.from_now!(). @no-doctest: reads the live clock; result is host-dependent
impl FromString<Duration>
parse
def parse(s: string) -> Result<Duration, ParseError>
Parse an ISO 8601 duration ([-]PnDTnHnMnS, or the week form PnW). Sub-nanosecond precision is truncated. Calendar Y/M-in-date fields are rejected (this type has no fixed month/year length).
Duration.parse("PT1H30M").map(|d| d.total_minutes) => Ok(90)
Duration.parse("P1W").map(|d| d.total_days) => Ok(7)
Duration.parse("nope").map(|d| d.to_iso()).unwrap_or("bad") => "bad"
impl Eq<Duration>
eq?
def eq?(self, other: Self) -> bool
Equal iff the same signed nanosecond span, whatever units built it.
Duration.of_seconds(1).eq?(Duration.of_millis(1000)) => true
impl Ord<Duration>
cmp
def cmp(self, other: Self) -> Ordering
Ordered by signed nanosecond span, which sorts a negative duration below zero.
Duration.of_seconds(1).cmp(Duration.of_seconds(2)) => Less
impl Display<Duration>
to_string
def to_string(self) -> string
Renders the span in ISO-8601 duration form.
Duration.of_seconds(5).to_string() => "PT5S"
Instant
opaque Instant
_epoch_nanos: int
end
An absolute point on the UTC timeline, counted in nanoseconds since the Unix epoch (1970-01-01T00:00:00Z). An Instant has no calendar or offset by itself; view it through an Offset to read wall-clock fields.
impl Instant
fromepochseconds
def from_epoch_seconds(s: int) -> Instant
The instant s seconds after the Unix epoch.
Instant.from_epoch_seconds(1).epoch_nanos => 1000000000
fromepochmillis
def from_epoch_millis(ms: int) -> Instant
The instant ms milliseconds after the Unix epoch.
Instant.from_epoch_millis(1500).epoch_seconds => 1
fromepochnanos
def from_epoch_nanos(ns: int) -> Instant
The instant ns nanoseconds after the Unix epoch.
Instant.from_epoch_nanos(42).epoch_nanos => 42
epoch_seconds
prop epoch_seconds(self) -> int
Whole seconds since the epoch, flooring toward negative infinity (so the paired subsecond_nanos is always in [0, 1e9)).
Instant.from_epoch_millis(1500).epoch_seconds => 1
Instant.from_epoch_millis(-500).epoch_seconds => -1
epoch_millis
prop epoch_millis(self) -> int
Whole milliseconds since the epoch, flooring toward negative infinity.
Instant.from_epoch_nanos(1500000).epoch_millis => 1
epoch_nanos
prop epoch_nanos(self) -> int
Nanoseconds since the epoch.
Instant.from_epoch_seconds(2).epoch_nanos => 2000000000
subsecond_nanos
prop subsecond_nanos(self) -> int
The sub-second part, always in [0, 1_000_000_000).
Instant.from_epoch_millis(1500).subsecond_nanos => 500000000
Instant.from_epoch_millis(-500).subsecond_nanos => 500000000
add
def add(self, d: Duration) -> Instant
This instant advanced by a duration.
Instant.from_epoch_seconds(10).add(Duration.of_seconds(5)).epoch_seconds => 15
sub
def sub(self, d: Duration) -> Instant
This instant moved back by a duration.
Instant.from_epoch_seconds(10).sub(Duration.of_seconds(5)).epoch_seconds => 5
duration_until
def duration_until(self, other: Instant) -> Duration
The duration from self to other (positive when other is later).
Instant.from_epoch_seconds(3).duration_until(Instant.from_epoch_seconds(8)).total_seconds => 5
at_offset
def at_offset(self, offset: Offset) -> OffsetDateTime
View this instant through a fixed UTC offset, yielding an OffsetDateTime whose wall-clock fields read in that offset.
Instant.from_epoch_seconds(0).at_offset(Offset.utc()).to_rfc3339() => "1970-01-01T00:00:00Z"
to_rfc3339
def to_rfc3339(self) -> string
The RFC 3339 timestamp in UTC (always suffixed Z).
Instant.from_epoch_seconds(0).to_rfc3339() => "1970-01-01T00:00:00Z"
torfc3339numeric
def to_rfc3339_numeric(self) -> string
The same UTC timestamp ending +00:00 and never Z. See datetime.Offset.to_iso_numeric.
Instant.from_epoch_seconds(0).to_rfc3339_numeric() => "1970-01-01T00:00:00+00:00"
impl FromString<Instant>
parse
def parse(s: string) -> Result<Instant, ParseError>
Parse an RFC 3339 timestamp into the absolute instant it names (its offset is consumed to reach UTC and then discarded).
Instant.parse("1970-01-01T00:00:01Z").map(|i| i.epoch_seconds) => Ok(1)
Instant.parse("1970-01-01T01:00:00+01:00").map(|i| i.epoch_seconds) => Ok(0)
impl Eq<Instant>
eq?
def eq?(self, other: Self) -> bool
Equal iff the same nanosecond on the UTC timeline.
Instant.from_epoch_seconds(1).eq?(Instant.from_epoch_millis(1000)) => true
impl Ord<Instant>
cmp
def cmp(self, other: Self) -> Ordering
Ordered along the UTC timeline: earlier is less.
Instant.from_epoch_seconds(1).cmp(Instant.from_epoch_seconds(2)) => Less
impl Display<Instant>
to_string
def to_string(self) -> string
Renders the instant as an RFC 3339 timestamp in UTC.
Instant.from_epoch_seconds(0).to_string() => "1970-01-01T00:00:00Z"
Offset
opaque Offset
seconds: int
end
A fixed offset from UTC, held as whole seconds east of UTC. Bounded to strictly within ±24 hours (|seconds| < 86400), which spans every real civil offset. UTC is the zero offset.
impl Offset
utc
def utc() -> Offset
The zero offset (UTC).
Offset.utc().total_seconds => 0
of_seconds
def of_seconds(s: int) -> Result<Offset, RangeError>
An offset of s seconds east of UTC; Err if |s| >= 86400.
Offset.of_seconds(3600).map(|o| o.to_iso()) => Ok("+01:00")
Offset.of_seconds(90000).map(|o| o.to_iso()).unwrap_or("bad") => "bad"
ofhoursminutes
def of_hours_minutes(hours: int, minutes: int) -> Result<Offset, RangeError>
An offset from signed hours and (unsigned) minutes. The sign of hours sets the direction; a negative hours subtracts the minutes too (so of_hours_minutes(-5, 30) is -05:30). For an offset that is only minutes east/west of UTC, use of_seconds.
Offset.of_hours_minutes(5, 30).map(|o| o.to_iso()) => Ok("+05:30")
Offset.of_hours_minutes(-8, 0).map(|o| o.to_iso()) => Ok("-08:00")
total_seconds
prop total_seconds(self) -> int
The offset as whole seconds east of UTC.
Offset.of_hours_minutes(1, 0).map(|o| o.total_seconds) => Ok(3600)
hours
prop hours(self) -> int
The signed hours component.
Offset.of_hours_minutes(5, 30).map(|o| o.hours) => Ok(5)
minutes
prop minutes(self) -> int
The signed minutes component (the part beyond whole hours).
Offset.of_hours_minutes(5, 30).map(|o| o.minutes) => Ok(30)
utc?
prop utc?(self) -> bool
Whether this is UTC (the zero offset).
Offset.utc().utc? => true
to_iso
def to_iso(self) -> string
The ISO 8601 offset string: Z for UTC, else +HH:MM / -HH:MM (with a :SS suffix only when the offset has whole seconds).
Offset.utc().to_iso() => "Z"
Offset.of_hours_minutes(-8, 0).map(|o| o.to_iso()) => Ok("-08:00")
toisonumeric
def to_iso_numeric(self) -> string
The same offset always in the numeric form, which makes UTC +00:00 and never than Z. ISO 8601 and RFC 3339 permit both, but a great many systems write only the numeric one. SQLite, Postgres, Crystal and Rust are among them, and a program comparing its output against theirs byte for byte needs to spell it their way.
A sibling method and no setting on the value: two offsets that compare equal must not render differently.
Offset.utc().to_iso_numeric() => "+00:00"
Offset.of_hours_minutes(-8, 0).map(|o| o.to_iso_numeric()) => Ok("-08:00")
impl FromString<Offset>
parse
def parse(s: string) -> Result<Offset, ParseError>
Parse an ISO 8601 offset: Z/z, ±HH:MM, ±HH:MM:SS, or the compact ±HHMM / ±HH.
Offset.parse("+05:30").map(|o| o.total_seconds) => Ok(19800)
Offset.parse("Z").map(|o| o.total_seconds) => Ok(0)
impl Eq<Offset>
eq?
def eq?(self, other: Self) -> bool
Equal iff the same number of seconds east of UTC.
Offset.utc().eq?(Offset.utc()) => true
impl Ord<Offset>
cmp
def cmp(self, other: Self) -> Ordering
Ordered by seconds east of UTC, which sorts western offsets below eastern ones.
Offset.utc().cmp(Offset.utc()) => Equal
impl Display<Offset>
to_string
def to_string(self) -> string
Renders the offset in ISO-8601 form - Z at UTC, else +HH:MM.
Offset.utc().to_string() => "Z"
Time
opaque Time
_nanos_of_day: int
end
A wall-clock time of day with nanosecond precision, in [00:00:00, 24:00:00). No date, no offset. Leap seconds are not represented (second tops out at 59).
impl Time
new
def new(hour: int, minute: int, second: int, nanosecond: int) -> Result<Time, RangeError>
A time from hour/minute/second/nanosecond; Err on any out-of-range component (hour 0..=23, minute/second 0..=59, nanosecond 0..=999_999_999).
Time.new(14, 30, 15, 0).map(|t| t.to_iso()) => Ok("14:30:15")
Time.new(24, 0, 0, 0).map(|t| t.to_iso()).unwrap_or("bad") => "bad"
hms
def hms(hour: int, minute: int, second: int) -> Result<Time, RangeError>
A time from hour/minute/second, nanoseconds zero.
Time.hms(9, 5, 0).map(|t| t.to_iso()) => Ok("09:05:00")
midnight
def midnight() -> Time
Midnight, 00:00:00.
Time.midnight().to_iso() => "00:00:00"
fromnanosof_day
def from_nanos_of_day(n: int) -> Result<Time, RangeError>
A time from raw nanoseconds since midnight; Err outside [0, 86_400_000_000_000).
Time.from_nanos_of_day(0).map(|t| t.to_iso()) => Ok("00:00:00")
hour
prop hour(self) -> int
The hour, 0..=23.
Time.hms(14, 30, 15).map(|t| t.hour) => Ok(14)
minute
prop minute(self) -> int
The minute, 0..=59.
Time.hms(14, 30, 15).map(|t| t.minute) => Ok(30)
second
prop second(self) -> int
The second, 0..=59.
Time.hms(14, 30, 15).map(|t| t.second) => Ok(15)
nanosecond
prop nanosecond(self) -> int
The sub-second nanoseconds, 0..=999_999_999.
Time.new(0, 0, 0, 500000000).map(|t| t.nanosecond) => Ok(500000000)
millisecond
prop millisecond(self) -> int
The sub-second milliseconds, 0..=999.
Time.new(0, 0, 0, 123000000).map(|t| t.millisecond) => Ok(123)
microsecond
prop microsecond(self) -> int
The sub-second microseconds, 0..=999_999.
Time.new(0, 0, 0, 123456000).map(|t| t.microsecond) => Ok(123456)
nanosofday
prop nanos_of_day(self) -> int
Nanoseconds since midnight.
Time.hms(0, 0, 1).map(|t| t.nanos_of_day) => Ok(1000000000)
add
def add(self, d: Duration) -> Time
This time advanced by a duration, wrapping around midnight (the date a wrap would imply is discarded; use DateTime.add to carry days).
Time.hms(23, 30, 0).map(|t| t.add(Duration.of_hours(1)).to_iso()) => Ok("00:30:00")
to_iso
def to_iso(self) -> string
The ISO 8601 time string HH:MM:SS, with a .fff… fraction only when there are sub-second nanoseconds.
Time.new(1, 2, 3, 500000000).map(|t| t.to_iso()) => Ok("01:02:03.5")
impl FromString<Time>
parse
def parse(s: string) -> Result<Time, ParseError>
Parse an ISO 8601 time HH:MM[:SS[.fff…]].
Time.parse("14:30:15").map(|t| t.hour) => Ok(14)
Time.parse("14:30").map(|t| t.to_iso()) => Ok("14:30:00")
impl Eq<Time>
eq?
def eq?(self, other: Self) -> bool
Equal iff the same nanosecond of the day.
Time.midnight().eq?(Time.midnight()) => true
impl Ord<Time>
cmp
def cmp(self, other: Self) -> Ordering
Ordered by nanosecond of the day: earlier is less.
Time.midnight().cmp(Time.midnight()) => Equal
impl Display<Time>
to_string
def to_string(self) -> string
Renders the wall-clock time in ISO-8601 form.
Time.hms(9, 5, 0).map(|t| t.to_string()) => Ok("09:05:00")
Date
opaque Date
_year: int
_month: int
_day: int
end
A proleptic-Gregorian calendar day (year, month, day) with no time or offset. The year is an unbounded int (negative years run backward before 1 BCE per the proleptic convention). Comparison is chronological.
impl Date
new
def new(year: int, month: int, day: int) -> Result<Date, RangeError>
A date from year/month/day; Err if the month is not 1..=12 or the day is not valid for that month and year (leap Februaries included).
Date.new(2026, 7, 11).map(|d| d.to_iso()) => Ok("2026-07-11")
Date.new(2026, 2, 29).map(|d| d.to_iso()).unwrap_or("bad") => "bad"
Date.new(2024, 2, 29).map(|d| d.to_iso()) => Ok("2024-02-29")
fromepochday
def from_epoch_day(n: int) -> Date
The date n days after the Unix epoch (1970-01-01 is day 0).
Date.from_epoch_day(0).to_iso() => "1970-01-01"
Date.from_epoch_day(-1).to_iso() => "1969-12-31"
from_ordinal
def from_ordinal(year: int, day_of_year: int) -> Result<Date, RangeError>
A date from an ISO ordinal (year + day-of-year 1..=366); Err if the ordinal is out of range for the year.
Date.from_ordinal(2026, 192).map(|d| d.to_iso()) => Ok("2026-07-11")
year
prop year(self) -> int
The year.
Date.new(2026, 7, 11).map(|d| d.year) => Ok(2026)
month
prop month(self) -> int
The month, 1..=12.
Date.new(2026, 7, 11).map(|d| d.month) => Ok(7)
day
prop day(self) -> int
The day of the month, 1..=31.
Date.new(2026, 7, 11).map(|d| d.day) => Ok(11)
epoch_day
prop epoch_day(self) -> int
Days since the Unix epoch (1970-01-01 is 0).
Date.new(1970, 1, 2).map(|d| d.epoch_day) => Ok(1)
weekday
prop weekday(self) -> Weekday
The day of the week.
Date.new(2026, 7, 11).map(|d| d.weekday) => Ok(Saturday)
monthofyear
prop month_of_year(self) -> Month
The Month value for this date.
Date.new(2026, 7, 11).map(|d| d.month_of_year) => Ok(July)
dayofyear
prop day_of_year(self) -> int
The day of the year, 1..=366.
Date.new(2026, 1, 1).map(|d| d.day_of_year) => Ok(1)
Date.new(2026, 12, 31).map(|d| d.day_of_year) => Ok(365)
iso_week
prop iso_week(self) -> IsoWeek
The ISO 8601 week date (week-numbering year + week 1..=53).
Date.new(2026, 1, 1).map(|d| d.iso_week.week) => Ok(1)
leap_year?
prop leap_year?(self) -> bool
Whether this date's year is a leap year.
Date.new(2024, 1, 1).map(|d| d.leap_year?) => Ok(true)
daysinmonth
prop days_in_month(self) -> int
The number of days in this date's month.
Date.new(2024, 2, 1).map(|d| d.days_in_month) => Ok(29)
add_days
def add_days(self, n: int) -> Date
This date shifted by n days (negative goes back).
Date.new(2026, 12, 31).map(|d| d.add_days(1).to_iso()) => Ok("2027-01-01")
add_weeks
def add_weeks(self, n: int) -> Date
This date shifted by n weeks.
Date.new(2026, 7, 11).map(|d| d.add_weeks(1).to_iso()) => Ok("2026-07-18")
add_months
def add_months(self, n: int) -> Date
This date shifted by n calendar months, clamping the day to the end of the target month (so Jan 31 + 1 month is Feb 28/29).
Date.new(2026, 1, 31).map(|d| d.add_months(1).to_iso()) => Ok("2026-02-28")
Date.new(2026, 11, 15).map(|d| d.add_months(3).to_iso()) => Ok("2027-02-15")
add_years
def add_years(self, n: int) -> Date
This date shifted by n calendar years, clamping Feb 29 to Feb 28 in a non-leap target year.
Date.new(2024, 2, 29).map(|d| d.add_years(1).to_iso()) => Ok("2025-02-28")
add_period
def add_period(self, p: Period) -> Date
This date shifted by a calendar Period: years first, then months, clamping the day to the end of each intermediate month. The order is observable: 2024-02-29 + P1Y1M clamps to 2025-02-28, then +1M is 2025-03-28 (not 2025-03-29). Because a Period is normalized, a span past a year applies as its year/month breakdown, which can differ from raw month arithmetic by a day: add_period(13.months) is the P1Y1M case above, while add_months(13) clamps only once.
Date.new(2024, 2, 29).map(|d| d.add_period(Period.of(1, 1)).to_iso()) => Ok("2025-03-28")
Date.new(2026, 1, 15).map(|d| d.add_period(Period.of_months(2)).to_iso()) => Ok("2026-03-15")
days_until
def days_until(self, other: Date) -> int
The number of days from self to other (positive when other is later).
Date.new(2026, 1, 1).map(|d| d.days_until(Date.new(2026, 1, 11).unwrap_or(d))) => Ok(10)
to_iso
def to_iso(self) -> string
The ISO 8601 date string YYYY-MM-DD (years 0 to 9999 zero-padded to four digits; negative years carry a leading -).
Date.new(2026, 7, 11).map(|d| d.to_iso()) => Ok("2026-07-11")
leap?
def leap?(year: int) -> bool
Whether year is a leap year (static form).
Date.leap?(2000) => true
Date.leap?(1900) => false
month_length
def month_length(year: int, month: int) -> int
The number of days in month of year (static form).
Date.month_length(2024, 2) => 29
impl FromString<Date>
parse
def parse(s: string) -> Result<Date, ParseError>
Parse an ISO 8601 date YYYY-MM-DD (an optional leading - marks a negative year).
Date.parse("2026-07-11").map(|d| d.day) => Ok(11)
Date.parse("2026-13-01").map(|d| d.to_iso()).unwrap_or("bad") => "bad"
impl Eq<Date>
eq?
def eq?(self, other: Self) -> bool
Equal iff the same calendar day.
Date.from_epoch_day(0).eq?(Date.from_epoch_day(0)) => true
impl Ord<Date>
cmp
def cmp(self, other: Self) -> Ordering
Ordered chronologically: year, then month, then day.
Date.from_epoch_day(0).cmp(Date.from_epoch_day(1)) => Less
impl Display<Date>
to_string
def to_string(self) -> string
Renders the day as ISO-8601 YYYY-MM-DD.
Date.new(2026, 7, 11).map(|d| d.to_string()) => Ok("2026-07-11")
DateTime
opaque DateTime
_date: Date
_time: Time
end
A calendar date paired with a wall-clock time, with no offset: a "local" datetime whose absolute instant is unknown until an Offset is supplied.
impl DateTime
new
def new(date: Date, time: Time) -> DateTime
A datetime from a date and a time.
DateTime.new(Date.new(2026, 7, 11).unwrap_or(Date.from_epoch_day(0)), Time.hms(14, 30, 0).unwrap_or(Time.midnight())).to_iso() => "2026-07-11T14:30:00"
of
def of(year: int, month: int, day: int, hour: int, minute: int, second: int) -> Result<DateTime, RangeError>
A datetime from calendar and clock components; Err on any out-of-range field.
DateTime.of(2026, 7, 11, 14, 30, 0).map(|dt| dt.to_iso()) => Ok("2026-07-11T14:30:00")
date
prop date(self) -> Date
The date part.
DateTime.of(2026, 7, 11, 14, 30, 0).map(|dt| dt.date.to_iso()) => Ok("2026-07-11")
time
prop time(self) -> Time
The time part.
DateTime.of(2026, 7, 11, 14, 30, 0).map(|dt| dt.time.to_iso()) => Ok("14:30:00")
year
prop year(self) -> int
The year.
DateTime.of(2026, 7, 11, 14, 30, 0).map(|dt| dt.year) => Ok(2026)
month
prop month(self) -> int
The month, 1..=12.
DateTime.of(2026, 7, 11, 14, 30, 0).map(|dt| dt.month) => Ok(7)
day
prop day(self) -> int
The day of the month.
DateTime.of(2026, 7, 11, 14, 30, 0).map(|dt| dt.day) => Ok(11)
hour
prop hour(self) -> int
The hour, 0..=23.
DateTime.of(2026, 7, 11, 14, 30, 0).map(|dt| dt.hour) => Ok(14)
minute
prop minute(self) -> int
The minute, 0..=59.
DateTime.of(2026, 7, 11, 14, 30, 0).map(|dt| dt.minute) => Ok(30)
second
prop second(self) -> int
The second, 0..=59.
DateTime.of(2026, 7, 11, 14, 30, 15).map(|dt| dt.second) => Ok(15)
nanosecond
prop nanosecond(self) -> int
The sub-second nanoseconds.
DateTime.of(2026, 7, 11, 0, 0, 0).map(|dt| dt.nanosecond) => Ok(0)
weekday
prop weekday(self) -> Weekday
The day of the week.
DateTime.of(2026, 7, 11, 0, 0, 0).map(|dt| dt.weekday) => Ok(Saturday)
add
def add(self, d: Duration) -> DateTime
This datetime advanced by a duration, carrying whole days across midnight.
DateTime.of(2026, 7, 11, 23, 0, 0).map(|dt| dt.add(Duration.of_hours(2)).to_iso()) => Ok("2026-07-12T01:00:00")
add_period
def add_period(self, p: Period) -> DateTime
This datetime's date part shifted by a calendar Period (see Date.add_period); the time of day is unchanged.
DateTime.of(2026, 1, 31, 9, 0, 0).map(|dt| dt.add_period(Period.of_months(1)).to_iso()) => Ok("2026-02-28T09:00:00")
duration_until
def duration_until(self, other: DateTime) -> Duration
The duration from self to other (positive when other is later), treating both as the same fictitious offset.
DateTime.of(2026, 7, 11, 0, 0, 0).map(|dt| dt.duration_until(DateTime.of(2026, 7, 12, 0, 0, 0).unwrap_or(dt)).total_hours) => Ok(24)
at_offset
def at_offset(self, offset: Offset) -> OffsetDateTime
Interpret these wall-clock fields as being in offset, producing the absolute OffsetDateTime.
DateTime.of(2026, 7, 11, 12, 0, 0).map(|dt| dt.at_offset(Offset.utc()).to_rfc3339()) => Ok("2026-07-11T12:00:00Z")
to_iso
def to_iso(self) -> string
The ISO 8601 datetime string YYYY-MM-DDTHH:MM:SS[.fff…].
DateTime.of(2026, 7, 11, 14, 30, 0).map(|dt| dt.to_iso()) => Ok("2026-07-11T14:30:00")
impl FromString<DateTime>
parse
def parse(s: string) -> Result<DateTime, ParseError>
Parse an ISO 8601 datetime YYYY-MM-DDThh:mm:ss (a T, t, or space separates the date and time; no offset).
DateTime.parse("2026-07-11T14:30:00").map(|dt| dt.hour) => Ok(14)
impl Eq<DateTime>
eq?
def eq?(self, other: Self) -> bool
Equal iff both the date and the time-of-day match.
DateTime.new(Date.from_epoch_day(0), Time.midnight()).eq?(DateTime.new(Date.from_epoch_day(0), Time.midnight())) => true
impl Ord<DateTime>
cmp
def cmp(self, other: Self) -> Ordering
Ordered by date, then by time within the day.
DateTime.new(Date.from_epoch_day(0), Time.midnight()).cmp(DateTime.new(Date.from_epoch_day(1), Time.midnight())) => Less
impl Display<DateTime>
to_string
def to_string(self) -> string
Renders the local datetime in ISO-8601 form, with no offset.
DateTime.of(2026, 7, 11, 14, 30, 0).map(|dt| dt.to_string()) => Ok("2026-07-11T14:30:00")
OffsetDateTime
opaque OffsetDateTime
_instant: Instant
_offset: Offset
end
An absolute Instant viewed through a fixed Offset, which is the RFC 3339 timestamp. Its accessors read the wall-clock fields in that offset, while equality and ordering compare the underlying instant (two views of the same moment in different offsets are equal; compare .datetime to compare wall-clock fields instead).
impl OffsetDateTime
new
def new(instant: Instant, offset: Offset) -> OffsetDateTime
Pair an instant with an offset to view it through.
OffsetDateTime.new(Instant.from_epoch_seconds(0), Offset.utc()).to_rfc3339() => "1970-01-01T00:00:00Z"
from_fields
def from_fields(dt: DateTime, offset: Offset) -> OffsetDateTime
Interpret a wall-clock DateTime as being in offset, computing the absolute instant it names.
OffsetDateTime.from_fields(DateTime.of(2026, 7, 11, 12, 0, 0).unwrap_or(DateTime.new(Date.from_epoch_day(0), Time.midnight())), Offset.utc()).to_rfc3339() => "2026-07-11T12:00:00Z"
instant
prop instant(self) -> Instant
The underlying absolute instant.
OffsetDateTime.new(Instant.from_epoch_seconds(5), Offset.utc()).instant.epoch_seconds => 5
offset
prop offset(self) -> Offset
The offset these fields are viewed through.
OffsetDateTime.new(Instant.from_epoch_seconds(0), Offset.utc()).offset.utc? => true
datetime
prop datetime(self) -> DateTime
The wall-clock DateTime in this offset.
OffsetDateTime.new(Instant.from_epoch_seconds(0), Offset.of_hours_minutes(1, 0).unwrap_or(Offset.utc())).datetime.to_iso() => "1970-01-01T01:00:00"
date
prop date(self) -> Date
The wall-clock date in this offset.
OffsetDateTime.new(Instant.from_epoch_seconds(0), Offset.utc()).date.to_iso() => "1970-01-01"
time
prop time(self) -> Time
The wall-clock time in this offset.
OffsetDateTime.new(Instant.from_epoch_seconds(0), Offset.utc()).time.to_iso() => "00:00:00"
year
prop year(self) -> int
The wall-clock year in this offset.
OffsetDateTime.new(Instant.from_epoch_seconds(0), Offset.utc()).year => 1970
month
prop month(self) -> int
The wall-clock month in this offset, 1..=12.
OffsetDateTime.new(Instant.from_epoch_seconds(0), Offset.utc()).month => 1
day
prop day(self) -> int
The wall-clock day of month in this offset.
OffsetDateTime.new(Instant.from_epoch_seconds(0), Offset.utc()).day => 1
hour
prop hour(self) -> int
The wall-clock hour in this offset.
OffsetDateTime.new(Instant.from_epoch_seconds(3600), Offset.utc()).hour => 1
minute
prop minute(self) -> int
The wall-clock minute in this offset.
OffsetDateTime.new(Instant.from_epoch_seconds(90), Offset.utc()).minute => 1
second
prop second(self) -> int
The wall-clock second in this offset.
OffsetDateTime.new(Instant.from_epoch_seconds(5), Offset.utc()).second => 5
nanosecond
prop nanosecond(self) -> int
The sub-second nanoseconds.
OffsetDateTime.new(Instant.from_epoch_millis(500), Offset.utc()).nanosecond => 500000000
weekday
prop weekday(self) -> Weekday
The wall-clock weekday in this offset.
OffsetDateTime.new(Instant.from_epoch_seconds(0), Offset.utc()).weekday => Thursday
add
def add(self, d: Duration) -> OffsetDateTime
This timestamp advanced by a duration (the offset is unchanged, and the wall-clock shifts with the instant).
OffsetDateTime.new(Instant.from_epoch_seconds(0), Offset.utc()).add(Duration.of_hours(1)).hour => 1
add_period
def add_period(self, p: Period) -> OffsetDateTime
This timestamp's calendar date shifted by a Period, keeping the wall time of day and the offset (see Date.add_period).
OffsetDateTime.from_fields(DateTime.of(2026, 1, 31, 9, 0, 0).unwrap_or(DateTime.new(Date.from_epoch_day(0), Time.midnight())), Offset.utc()).add_period(Period.of_months(1)).datetime.to_iso() => "2026-02-28T09:00:00"
sub
def sub(self, d: Duration) -> OffsetDateTime
This timestamp moved back by a duration.
OffsetDateTime.new(Instant.from_epoch_seconds(3600), Offset.utc()).sub(Duration.of_hours(1)).hour => 0
duration_until
def duration_until(self, other: OffsetDateTime) -> Duration
The duration from self to other (compares the absolute instants).
OffsetDateTime.new(Instant.from_epoch_seconds(0), Offset.utc()).duration_until(OffsetDateTime.new(Instant.from_epoch_seconds(60), Offset.utc())).total_seconds => 60
with_offset
def with_offset(self, offset: Offset) -> OffsetDateTime
The same instant viewed through a different offset (the wall-clock fields change; the moment does not).
OffsetDateTime.new(Instant.from_epoch_seconds(0), Offset.utc()).with_offset(Offset.of_hours_minutes(1, 0).unwrap_or(Offset.utc())).hour => 1
to_rfc3339
def to_rfc3339(self) -> string
The RFC 3339 timestamp, e.g. 2026-07-11T14:30:00+05:30 (or …Z for UTC).
OffsetDateTime.new(Instant.from_epoch_seconds(0), Offset.of_hours_minutes(5, 30).unwrap_or(Offset.utc())).to_rfc3339() => "1970-01-01T05:30:00+05:30"
torfc3339numeric
def to_rfc3339_numeric(self) -> string
The same timestamp with the offset always numeric, which ends UTC +00:00 and never Z. See datetime.Offset.to_iso_numeric for why that spelling is worth having.
OffsetDateTime.new(Instant.from_epoch_seconds(0), Offset.utc()).to_rfc3339_numeric() => "1970-01-01T00:00:00+00:00"
impl FromString<OffsetDateTime>
parse
def parse(s: string) -> Result<OffsetDateTime, ParseError>
Parse an RFC 3339 timestamp, preserving its offset.
OffsetDateTime.parse("2026-07-11T14:30:00+05:30").map(|o| o.to_rfc3339()) => Ok("2026-07-11T14:30:00+05:30")
OffsetDateTime.parse("2026-07-11T14:30:00Z").map(|o| o.hour) => Ok(14)
impl Eq<OffsetDateTime>
eq?
def eq?(self, other: Self) -> bool
Equal iff the same underlying instant: two views of one moment in different offsets are equal.
OffsetDateTime.new(Instant.from_epoch_seconds(0), Offset.utc()).eq?(OffsetDateTime.new(Instant.from_epoch_seconds(0), Offset.utc())) => true
impl Ord<OffsetDateTime>
cmp
def cmp(self, other: Self) -> Ordering
Ordered by the underlying instant and never by wall-clock fields.
OffsetDateTime.new(Instant.from_epoch_seconds(0), Offset.utc()).cmp(OffsetDateTime.new(Instant.from_epoch_seconds(1), Offset.utc())) => Less
impl Display<OffsetDateTime>
to_string
def to_string(self) -> string
Renders the value as an RFC 3339 timestamp carrying its offset.
OffsetDateTime.new(Instant.from_epoch_seconds(0), Offset.utc()).to_string() => "1970-01-01T00:00:00Z"
Period
opaque Period
total: int
end
A calendar-variable span of whole months. The fixed-length Duration cannot express one: a month is 28 to 31 days and a year 365 or 366. A Period is a signed count of months where a year is 12; it is normalized by duration, so Period.of_years(1) and Period.of_months(12) are the same value, and it compares by total months (1.years > 11.months, 1.years < 13.months). Duration and Period are not comparable: 30.days is not 1.months.
impl Period
zero
def zero() -> Period
The zero period.
Period.zero().zero? => true
of_years
def of_years(n: int) -> Period
A period of n whole years (12 months each).
Period.of_years(2).to_iso() => "P2Y"
of_months
def of_months(n: int) -> Period
A period of n whole months.
Period.of_months(18).to_iso() => "P1Y6M"
of
def of(years: int, months: int) -> Period
A period of years years plus months months (summed into one span, so of(1, -2) is ten months).
Period.of(1, 2).to_iso() => "P1Y2M"
Period.of(1, -2).to_iso() => "P10M"
years
prop years(self) -> int
The whole-years part of the normalized (year, month) breakdown, truncated toward zero so a negative period's parts share its sign.
Period.of(3, 5).years => 3
months
prop months(self) -> int
The remaining months after the whole years (total_months - years*12).
Period.of(3, 5).months => 5
total_months
prop total_months(self) -> int
This period as a whole number of months (the canonical value).
Period.of(1, 6).total_months => 18
zero?
prop zero?(self) -> bool
True for the zero period.
Period.of(0, 0).zero? => true
Period.of_months(1).zero? => false
negate
def negate(self) -> Period
This period negated.
Period.of(1, 2).negate().to_iso() => "-P1Y2M"
to_iso
def to_iso(self) -> string
ISO 8601 period text over the normalized (year, month) breakdown: P<years>Y<months>M, omitting a zero part, P0D for zero, and a single leading minus for a negative period (-P1Y2M).
Period.of(1, 2).to_iso() => "P1Y2M"
Period.of_years(1).to_iso() => "P1Y"
Period.of_months(18).to_iso() => "P1Y6M"
Period.of(0, 0).to_iso() => "P0D"
Period.of(-1, -2).to_iso() => "-P1Y2M"
after
def after(self, d: Date) -> Date
This period applied to d: years first, then months, clamping the day at each step (see Date.add_period).
Period.of_months(1).after(Date.new(2026, 1, 31).unwrap_or(Date.from_epoch_day(0))).to_iso() => "2026-02-28"
before
def before(self, d: Date) -> Date
This period subtracted from d (d.add_period(self.negate())). Not a true inverse of after: month-end clamping is lossy, so p.before(p.after(jan31)) lands on the 28th and never back on the 31st.
Period.of_years(1).before(Date.new(2025, 6, 15).unwrap_or(Date.from_epoch_day(0))).to_iso() => "2024-06-15"
impl Eq<Period>
eq?
def eq?(self, other: Self) -> bool
Equal iff the same total number of months, which makes P1Y equal P12M.
Period.of_years(1).eq?(Period.of_months(12)) => true
Period.of(1, 1).eq?(Period.of_months(12)) => false
impl Ord<Period>
cmp
def cmp(self, other: Self) -> Ordering
Ordered by total months, which compares a period by its duration: 1.years > 11.months and 1.years < 13.months.
Period.of_years(1).cmp(Period.of_months(11)) => Greater
Period.of_years(1).cmp(Period.of_months(13)) => Less
impl Display<Period>
to_string
def to_string(self) -> string
Renders the calendar span in ISO-8601 period form.
Period.of(1, 2).to_string() => "P1Y2M"
DurationUnits
trait DurationUnits
Fixed-length units (Duration) as properties on int: open datetime, then 5.minutes, 2.days. Read bare: 2.days is a property, and 2.days() is a property error.
nanos
prop nanos(self) -> Duration
That many nanoseconds, as a Duration.
500.nanos.total_nanos => 500
micros
prop micros(self) -> Duration
That many microseconds, as a Duration.
2.micros.total_nanos => 2000
millis
prop millis(self) -> Duration
That many milliseconds, as a Duration.
3.millis.total_nanos => 3000000
seconds
prop seconds(self) -> Duration
That many seconds, as a Duration.
90.seconds.total_minutes => 1
minutes
prop minutes(self) -> Duration
That many minutes, as a Duration.
60.minutes.total_hours => 1
hours
prop hours(self) -> Duration
That many hours, as a Duration.
24.hours.total_days => 1
days
prop days(self) -> Duration
That many days, as a Duration of 24 hours each.
2.days.total_hours => 48
weeks
prop weeks(self) -> Duration
That many weeks, as a Duration of seven days each.
2.weeks.total_days => 14
impl DurationUnits<int>
nanos
prop nanos(self) -> Duration
That many nanoseconds, as a Duration.
500.nanos.total_nanos => 500
micros
prop micros(self) -> Duration
That many microseconds, as a Duration.
2.micros.total_nanos => 2000
millis
prop millis(self) -> Duration
That many milliseconds, as a Duration.
3.millis.total_nanos => 3000000
seconds
prop seconds(self) -> Duration
That many seconds, as a Duration.
90.seconds.total_minutes => 1
minutes
prop minutes(self) -> Duration
That many minutes, as a Duration.
60.minutes.total_hours => 1
hours
prop hours(self) -> Duration
That many hours, as a Duration.
24.hours.total_days => 1
days
prop days(self) -> Duration
That many days, as a Duration of 24 hours each.
2.days.total_hours => 48
weeks
prop weeks(self) -> Duration
That many weeks, as a Duration of seven days each.
2.weeks.total_days => 14
PeriodUnits
trait PeriodUnits
Calendar units (Period) as properties on int: 2.months, 1.years. Distinct from DurationUnits because months and years are calendar-variable and no fixed number of nanoseconds.
months
prop months(self) -> Period
That many calendar months, as a Period.
2.months.to_iso() => "P2M"
years
prop years(self) -> Period
That many calendar years, as a Period of twelve months each.
3.years.to_iso() => "P3Y"
impl PeriodUnits<int>
months
prop months(self) -> Period
That many calendar months, as a Period.
2.months.to_iso() => "P2M"
years
prop years(self) -> Period
That many calendar years, as a Period of twelve months each.
3.years.to_iso() => "P3Y"
impl Add<Instant, Duration>
Output
type Output = Instant
add
def add(self, rhs: Duration) -> Instant
Shifts the instant forward along the UTC timeline.
(Instant.from_epoch_seconds(0) + Duration.of_days(2)).epoch_seconds => 172800
impl Sub<Instant, Duration>
Output
type Output = Instant
sub
def sub(self, rhs: Duration) -> Instant
Shifts the instant backward along the UTC timeline.
(Instant.from_epoch_seconds(120) - Duration.of_minutes(1)).epoch_seconds => 60
impl Sub<Instant, Instant>
Output
type Output = Duration
sub
def sub(self, rhs: Instant) -> Duration
a - b is the duration from b to a: positive when a is later.
(Instant.from_epoch_seconds(90) - Instant.from_epoch_seconds(30)).total_seconds => 60
(Instant.from_epoch_seconds(30) - Instant.from_epoch_seconds(90)).total_seconds => -60
impl Add<Duration>
Output
type Output = Duration
add
def add(self, rhs: Duration) -> Duration
Adds two spans.
(1.hours + 30.minutes).total_minutes => 90
impl Sub<Duration>
Output
type Output = Duration
sub
def sub(self, rhs: Duration) -> Duration
Subtracts one span from another; the result may be negative.
(1.hours - 30.minutes).total_minutes => 30
impl Add<DateTime, Duration>
Output
type Output = DateTime
add
def add(self, rhs: Duration) -> DateTime
Shifts the local datetime forward by a fixed-length span.
DateTime.of(2026, 7, 12, 9, 0, 0).map(|dt| (dt + Duration.of_hours(3)).to_iso()) => Ok("2026-07-12T12:00:00")
impl Sub<DateTime, Duration>
Output
type Output = DateTime
sub
def sub(self, rhs: Duration) -> DateTime
Shifts the local datetime backward by a fixed-length span.
DateTime.of(2026, 7, 12, 9, 0, 0).map(|dt| (dt - Duration.of_hours(10)).to_iso()) => Ok("2026-07-11T23:00:00")
impl Sub<DateTime, DateTime>
Output
type Output = Duration
sub
def sub(self, rhs: DateTime) -> Duration
The span from rhs to self, positive when self is later.
DateTime.of(2026, 7, 12, 0, 0, 0).map(|a| (a - DateTime.of(2026, 7, 11, 0, 0, 0).unwrap_or(a)).total_hours) => Ok(24)
impl Add<OffsetDateTime, Duration>
Output
type Output = OffsetDateTime
add
def add(self, rhs: Duration) -> OffsetDateTime
Shifts the instant forward, keeping the offset it is viewed through.
(OffsetDateTime.new(Instant.from_epoch_seconds(0), Offset.utc()) + Duration.of_minutes(1)).instant.epoch_seconds => 60
impl Sub<OffsetDateTime, Duration>
Output
type Output = OffsetDateTime
sub
def sub(self, rhs: Duration) -> OffsetDateTime
Shifts the instant backward, keeping the offset it is viewed through.
(OffsetDateTime.new(Instant.from_epoch_seconds(120), Offset.utc()) - Duration.of_minutes(1)).instant.epoch_seconds => 60
impl Sub<OffsetDateTime, OffsetDateTime>
Output
type Output = Duration
sub
def sub(self, rhs: OffsetDateTime) -> Duration
Positive when self is later, matching Instant - Instant.
(OffsetDateTime.new(Instant.from_epoch_seconds(90), Offset.utc()) - OffsetDateTime.new(Instant.from_epoch_seconds(30), Offset.utc())).total_seconds => 60
impl Add<Date, Period>
Output
type Output = Date
add
def add(self, rhs: Period) -> Date
Shifts the day by whole calendar months, clamping to the target month's end.
Date.new(2026, 1, 15).map(|d| (d + 2.months).to_iso()) => Ok("2026-03-15")
impl Sub<Date, Period>
Output
type Output = Date
sub
def sub(self, rhs: Period) -> Date
Shifting back by a period is adding its negation; month-end clamping applies the same way (sub is not add's inverse near month ends).
Date.new(2026, 3, 15).map(|d| (d - 2.months).to_iso()) => Ok("2026-01-15")
impl Add<Period>
Output
type Output = Period
add
def add(self, rhs: Period) -> Period
Total-months arithmetic: 1.years + 2.months is P1Y2M.
(1.years + 2.months).to_iso() => "P1Y2M"
impl Sub<Period>
Output
type Output = Period
sub
def sub(self, rhs: Period) -> Period
Subtracts one calendar span from another; the result may be negative.
(1.years - 2.months).to_iso() => "P10M"
_FormatPart
type _FormatPart
FmtYear
FmtMonth
FmtDay
FmtHour
FmtMinute
FmtSecond
FmtWeekdayShortName
FmtOffsetPart
FmtOffsetNumericPart
FmtLiteral(string)
end
The pieces a Format renders, one variant per part. Private: the public surface is the part constants, literal, and Format.of.
Format
opaque Format
_parts: List<_FormatPart>
end
A timestamp display format: a sequence of parts rendered in order against an OffsetDateTime. Built from the part constants below (year, month, hour, ...), literal(text), and the iso_date / iso_time fragments, composed with Format.of([...]) or +. In operator position a plain string on the right is a literal (iso_date + "T" means literal("T") there); a format's leading token still needs literal(...) (impl Add<string, Format> is barred, a builtin-primitive Self H0573), and so does an element of a Format.of list (List is homogeneous, and no implicit coercion). A top-level Format binding folds at compile time like any other constant.
year
year: Format = Format(_parts=[FmtYear])
The zero-padded four-digit year (wider years keep all their digits).
year.render(OffsetDateTime.new(Instant.from_epoch_seconds(0), Offset.utc())) => "1970"
month
month: Format = Format(_parts=[FmtMonth])
The zero-padded two-digit month, 01..=12.
month.render(OffsetDateTime.new(Instant.from_epoch_seconds(0), Offset.utc())) => "01"
day
day: Format = Format(_parts=[FmtDay])
The zero-padded two-digit day of month.
day.render(OffsetDateTime.new(Instant.from_epoch_seconds(0), Offset.utc())) => "01"
hour
hour: Format = Format(_parts=[FmtHour])
The zero-padded two-digit hour, 00..=23.
hour.render(OffsetDateTime.new(Instant.from_epoch_seconds(3600), Offset.utc())) => "01"
minute
minute: Format = Format(_parts=[FmtMinute])
The zero-padded two-digit minute.
minute.render(OffsetDateTime.new(Instant.from_epoch_seconds(90), Offset.utc())) => "01"
second
second: Format = Format(_parts=[FmtSecond])
The zero-padded two-digit whole second (fractions never render; a format needing them arrives additively, the way to_iso and to_iso_numeric are siblings).
second.render(OffsetDateTime.new(Instant.from_epoch_seconds(5), Offset.utc())) => "05"
weekdayshortname
weekday_short_name: Format = Format(_parts=[FmtWeekdayShortName])
The three-letter English weekday abbreviation (Weekday.short_name).
weekday_short_name.render(OffsetDateTime.new(Instant.from_epoch_seconds(0), Offset.utc())) => "Thu"
offset
offset: Format = Format(_parts=[FmtOffsetPart])
The offset in Offset.to_iso form: Z at zero, +02:00 otherwise.
offset.render(OffsetDateTime.new(Instant.from_epoch_seconds(0), Offset.utc())) => "Z"
offset_numeric
offset_numeric: Format = Format(_parts=[FmtOffsetNumericPart])
The offset always in numeric form (Offset.to_iso_numeric), which makes UTC renders +00:00 - the spelling SQLite, Postgres, Crystal and Rust write.
offset_numeric.render(OffsetDateTime.new(Instant.from_epoch_seconds(0), Offset.utc())) => "+00:00"
literal
def literal(text: string) -> Format
A format rendering text verbatim. Required for a format's leading token and inside Format.of lists; after a leading Format, + "text" says the same thing.
(literal("[") + hour + literal("]")).render(OffsetDateTime.new(Instant.from_epoch_seconds(0), Offset.utc())) => "[00]"
iso_date
iso_date: Format = Format.of([year, literal("-"), month, literal("-"), day])
year-month-day, the date half of ISO 8601 / RFC 3339.
iso_date.render(OffsetDateTime.new(Instant.from_epoch_seconds(0), Offset.utc())) => "1970-01-01"
iso_time
iso_time: Format = Format.of([hour, literal(":"), minute, literal(":"), second])
hour:minute:second, the whole-second time half of ISO 8601. Unlike Time.to_iso, which appends a fraction when the time has one, this fragment never renders fractions.
iso_time.render(OffsetDateTime.new(Instant.from_epoch_seconds(3661), Offset.utc())) => "01:01:01"
impl Format
of
def of(parts: List<Format>) -> Format
The concatenation of parts, in order: the canonical base the + composition is sugar over.
Format.of([hour, literal("h")]).render(OffsetDateTime.new(Instant.from_epoch_seconds(7200), Offset.utc())) => "02h"
render
def render(self, dt: OffsetDateTime) -> string
Renders dt through this format, part by part. OffsetDateTime is the render target because it alone has every part's accessor; a Date/DateTime render would force a Result on offset-bearing formats.
(iso_date + "T" + iso_time + offset_numeric).render(OffsetDateTime.new(Instant.from_epoch_seconds(0), Offset.utc())) => "1970-01-01T00:00:00+00:00"
renderpart
def _render_part(p: _FormatPart, dt: OffsetDateTime) -> string
One part's text against dt, the padding matching the to_iso family.
impl Add<Format>
Output
type Output = Format
add
def add(self, rhs: Format) -> Format
Concatenates two formats.
(iso_date + iso_time).render(OffsetDateTime.new(Instant.from_epoch_seconds(0), Offset.utc())) => "1970-01-0100:00:00"
impl Add<Format, string>
Output
type Output = Format
add
def add(self, rhs: string) -> Format
Appends a literal: in operator position a plain string IS a literal, so iso_date + "T" needs no literal(...).
(hour + "h").render(OffsetDateTime.new(Instant.from_epoch_seconds(7200), Offset.utc())) => "02h"
now!
def now!() -> Instant [time]
The current instant, read from the wall clock. Under --deterministic this reads the gate's virtual clock and replays. @no-doctest: reads the live clock; result is host-dependent
now_at!
def now_at!(offset: Offset) -> OffsetDateTime [time]
The current moment as an OffsetDateTime in offset. @no-doctest: reads the live clock; result is host-dependent
today!
def today!(offset: Offset) -> Date [time]
Today's date in offset. @no-doctest: reads the live clock; result is host-dependent
localoffsetat!
def local_offset_at!(at: Instant) -> Option<Offset> [time]
The host machine's own UTC offset at at, which is what a user of this machine would call local time. Asked per instant and never once, a zone observes daylight saving answers differently either side of a transition: the same machine is +02:00 in January and +03:00 in July.
None when the host cannot place at on its local calendar: an instant far enough out that the platform has no answer for it. A machine with no timezone configured answers UTC, which is an answer and no failure, which leaves None meaning the instant and never the setup.
This reads one offset from the host; it does not open a zone database. Converting between arbitrary named zones remains out of scope (see the module header). @no-doctest: reads the host's timezone; result is host-dependent
local_now!
def local_now!() -> Option<OffsetDateTime> [time]
The current moment as an OffsetDateTime in the host's own offset: now_at! without having to know the offset first. None on the same terms as local_offset_at!. @no-doctest: reads the live clock; result is host-dependent
offsetof_seconds
def _offset_of_seconds(seconds: int) -> Option<Offset>
Offset.of_seconds as an Option: on the seam path an out-of-band value is no answer, with no second failure to tell apart.
daysfrom_civil
def _days_from_civil(year: int, month: int, day: int) -> int
Days since 1970-01-01 for a proleptic-Gregorian y/m/d. Exact for any int year: floor // folds Hinnant's era conditional away (era = y // 400, yoe = y % 400).
civilfrom_days
def _civil_from_days(epoch_day: int) -> Date
The inverse: the Date for a day count since 1970-01-01.
_weekday
def _weekday(epoch_day: int) -> Weekday
Weekday from an epoch-day count. (epoch_day + 4) % 7 is 0 = Sunday … 6 = Saturday (1970-01-01 was a Thursday); floor % leaves it total.
isleap?
def _is_leap?(year: int) -> bool
daysin_month
def _days_in_month(year: int, month: int) -> int
isop
def _iso_p(y: int) -> int
ISO week-numbering helpers.
weeksinisoyear
def _weeks_in_iso_year(y: int) -> int
divtrunc
def _div_trunc(a: int, b: int) -> int
Integer division truncated toward zero (the bare // floors).
_pad2
def _pad2(n: int) -> string
_pad4
def _pad4(n: int) -> string
Left-pad a non-negative integer to at least four digits (for years).
formatyear
def _format_year(y: int) -> string
formatfrac
def _format_frac(nanos: int) -> string
The .fff… fractional-seconds suffix for a sub-second nanosecond count (empty when zero; trailing zeros trimmed).
_pad9
def _pad9(n: int) -> string
trimtrailing_zeros
def _trim_trailing_zeros(s: string) -> string
alldigits?
def _all_digits?(s: string) -> bool
parsedigits
def _parse_digits(s: string) -> Option<int>
Parse a run of ASCII digits into a non-negative int (None if empty or containing a non-digit).
fracto_nanos
def _frac_to_nanos(frac: string) -> Option<int>
A fractional-second digit string to nanoseconds (padded/truncated to 9 digits). None on a non-digit.
padright9
def _pad_right9(s: string) -> string
rangeto_parse
def _range_to_parse<T>(r: Result<T, RangeError>, input: string) -> Result<T, ParseError>
Rewrap a Result's range error as a parse error carrying input.
parsedate_str
def _parse_date_str(s: string) -> Result<Date, ParseError>
parsetime_str
def _parse_time_str(s: string) -> Result<Time, ParseError>
parseoffset_str
def _parse_offset_str(s: string) -> Result<Offset, ParseError>
parsedatetime_str
def _parse_datetime_str(s: string) -> Result<DateTime, ParseError>
parseoffset_datetime
def _parse_offset_datetime(s: string) -> Result<OffsetDateTime, ParseError>
finddatetimesep
def _find_date_time_sep(s: string) -> Option<int>
findoffset_start
def _find_offset_start(s: string) -> Option<int>
durationto_iso
def _duration_to_iso(nanos: int) -> string
parseduration
def _parse_duration(s: string) -> Result<Duration, ParseError>
scanduration
def _scan_duration(body: string) -> Option<int>
Scan the post-P body, accumulating unsigned nanoseconds. Tracks whether the cursor has passed T (past it, M means minutes and no months).
durunit_nanos
def _dur_unit_nanos(c: string, in_time: bool) -> int
fixednanos
def _fixed_nanos(num: string, mult: int) -> Option<int>
A component number (possibly fractional) times a nanosecond multiplier. Sub-nanosecond remainders truncate.
FOLDPROBE_OF
_FOLD_PROBE_OF: Format = Format.of([weekday_short_name, literal(" "), hour, literal(":"), minute])
Top-level bindings are compile-time-evaluated, and this module does not compile at all unless both composition forms fold (HANKI.md section 16) - these two bindings are the probes, one for the Format.of list form and one for the trait-dispatched + form.
FOLDPROBE_ADD
_FOLD_PROBE_ADD: Format = iso_date + "T" + iso_time + offset_numeric