Skip to content
You are reading v0.3, which is not the current release. Go to v0.4.

std::time

Dates and times as people write them, rather than as a machine counts them.

Clock gives unix_millis and monotonic_millis and that is the whole of time in std until this module. An instant is enough to measure something and not enough to say when it happened: reconciliation is bucketed by value date and settlement date, a report covers a month, and a log line is read by somebody who wants to know what day it was. None of that is expressible in milliseconds since 1970.

The types are here — a civil date, a time of day, the two together, and a fixed offset from UTC — because they are vocabulary. Two libraries that disagree about what a date is cannot exchange one, and The ecosystem design note reserves exactly that case for std.

The time zone database is not, and cannot be. It is a dataset: Egypt changes its rules, IANA cuts a release, and every binary already shipped is wrong about the future. Nothing behind this document’s compatibility promise can be updated four times a year. So std owns Offset — the answer — and a package or the host owns the rules that produce one.

That is not a limitation dressed up. A program that stores UTC and renders with an offset it was handed is correct forever; a program that bakes in zone rules is wrong the next time a government moves a clock.

The proleptic Gregorian calendar, everywhere

Section titled “The proleptic Gregorian calendar, everywhere”

One calendar, extended backwards past its adoption, which is what ISO 8601 and every database mean by a date. Years before 1 are negative and year zero exists — again ISO’s convention, and the one that makes arithmetic work without a special case nobody remembers.

Types: Date · Time · DateTime · Offset

Methods: Date · Time · Offset · DateTime

Trait implementations: Eq for Date · Ord for Date · Eq for Time · Ord for Time · Show for Date · Show for Time · Show for DateTime · Show for Offset

Functions: days_in_month · is_leap

pub type Date = {
year: Int,
month: Int,
day: Int,
};

A date on the proleptic Gregorian calendar.

month is 1 to 12 and day is 1 to the length of that month. Construct through Date::of, which checks; the fields are public because a date is its three numbers and hiding them would only mean writing three accessors.

pub type Time = {
hour: Int,
minute: Int,
second: Int,
milli: Int,
};

A time of day, to the millisecond.

No leap seconds. second is 0 to 59, and a minute is sixty seconds here as it is in every database and every wire format worth naming. A calendar that admitted 23:59:60 would push that into every comparison and every subtraction to be correct about something almost nothing observes.

pub type DateTime = {
date: Date,
time: Time,
};

A date and a time, with no zone attached.

Not an instant. “The fourteenth at nine in the morning” is a different kind of thing from “one and a half billion seconds after 1970”: one names a position on a calendar and the other names a moment, and turning either into the other needs an offset. Keeping them apart is what stops a program silently treating local wall time as UTC — the mistake that puts a midnight-boundary report a day out twice a year.

pub type Offset = {
minutes: Int,
};

Minutes east of UTC.

Offset::utc() is zero, India is +330, and -300 is New York in winter. A fixed number, not a zone: a zone is a rule that says which offset applies on a given date, and that rule is a dataset this module refuses to carry. A caller gets one of these from a package or from the host and hands it here.

impl Date
pub fn of(year: Int, month: Int, day: Int) -> Option<Date>

A date, or None if there is no such day.

The thirty-first of February is refused rather than rolled forward into March. Rolling is a policy — some systems clamp, some wrap, some raise — and a library that picked silently would be picking for a caller who nearly always has bad input rather than an interesting question.

pub fn of_string(text: String) -> Option<Date>

A date written 2026-08-25, or None.

Exactly four digits, two, two, hyphen-separated, and the day has to exist – 2026-02-30 is None for the same reason Date::of refuses it.

What Show prints for the years 0000 to 9999, read back, and only those. Date holds an Int year, so Show will print -0044-03-15 or a five-digit year quite happily; ten bytes of digits and hyphens cannot say either, and this answers None for both. The round trip holds over the range a date is normally written in and stops at its edges.

pub fn to_days(self) -> Int

Days since the first of January 1970, which may be negative.

Howard Hinnant’s days_from_civil, which is the algorithm every modern date library uses: it is exact for the whole range of an Int, it needs no table, and its shifts of the year and month are what remove the leap day from the middle of the calculation by moving March to the start of the year.

pub fn of_days(days: Int) -> Date

The date days after the first of January 1970.

The inverse of to_days, and the other half of Hinnant’s pair.

pub fn add_days(self, days: Int) -> Date

The date days later, which may be earlier if days is negative.

pub fn days_until(self, other: Date) -> Int

How many days from self to other, negative if other is earlier.

pub fn weekday(self) -> Int

Monday is 1 and Sunday is 7, which is ISO 8601’s numbering.

The first of January 1970 was a Thursday, which is where the 3 comes from: it is day zero and Thursday is 4, so the offset that lands Monday on 1 is three.

impl Time
pub fn of(hour: Int, minute: Int, second: Int, milli: Int) -> Option<Time>

A time of day, or None if there is no such moment.

pub fn of_string(text: String) -> Option<Time>

A time written 09:05:00 or 09:05:00.123, or None.

The fraction is optional and everything else is not, which is ISO 8601’s own rule rather than a leniency: HH:MM:SS is a complete time and the fraction is an extension of it. Show always prints three digits, so what it writes reads back; what arrives from somewhere else usually has no fraction at all.

More than three fractional digits are refused rather than rounded. This type holds milliseconds, and a library that silently dropped the microseconds off a timestamp would be losing data in a way nothing later could notice.

pub fn midnight() -> Time

Midnight.

pub fn to_millis(self) -> Int

Milliseconds since midnight.

pub fn of_millis(millis: Int) -> Option<Time>

The time millis after midnight, or None if that is not inside one day.

The range is checked here, because the arithmetic cannot refuse on its own. / and % truncate toward zero, so without the check of_millis(-1) would hold milli: -1 and print 00:00:00.-001 — not ISO 8601, and not something Time::of_string reads back — while of_millis(86400000) would print 24:00:00.000. Either would then travel into a DateTime with nothing there to notice it.

impl Offset
pub fn utc() -> Offset

No offset at all.

pub fn of_minutes(minutes: Int) -> Option<Offset>

minutes east of UTC, or None if that is not less than a day. Positive is ahead, negative is behind.

The bound is the one Offset::of_string already enforces – two digits of hours and two of minutes, so -1439 to 1439 – rather than the fourteen hours governments have actually reached for, because a wire format may carry a historical offset this module has no list of. What it will not carry is 100000, which Show would write as +1666:40 and of_string refuses to read back.

pub fn of_string(text: String) -> Option<Offset>

An offset written +05:30, -05:00 or Z, or None.

Z is accepted although Show prints +00:00. It is not a second format – it is ISO 8601’s own spelling of this exact value, and it is what every wire format in the world writes. Refusing it would mean a parser that cannot read the timestamps it is for.

impl DateTime
pub fn of_unix_millis(millis: Int) -> DateTime

The moment millis after 1970, as UTC says it.

Flooring, not truncating, which is the whole of what makes dates before 1970 come out right: -1 millisecond is the last millisecond of 1969, not the first of 1970 with a minus sign. Truncating division puts it a whole day out, and it is the bug this function exists to not have.

pub fn to_unix_millis(self) -> Int

Milliseconds since 1970, reading self as UTC.

pub fn of_string(text: String) -> Option<DateTime>

A timestamp written 2026-08-25T09:05:00 or ...T09:05:00.123, or None.

No zone, because this type has none. A trailing Z or +05:30 is refused here rather than ignored – silently dropping it would turn a moment into a different moment, which is the one mistake a date library must not make quietly. DateTime::instant_of_string reads the zoned form and gives back the instant it names.

pub fn instant_of_string(text: String) -> Option<Int>

The instant a zoned timestamp names, as milliseconds since 1970.

2026-08-25T09:05:00Z, 2026-08-25T09:05:00.123+05:30. The zone is required: this is the function for reading what somebody else wrote, and a timestamp with no zone does not name an instant. Use of_string for that, which gives back the wall clock it actually is.

An Int rather than a DateTime because that is what the moment is – converting it back to a wall clock needs a zone, and the caller has one in mind or they would not be asking.

pub fn of_unix_seconds(seconds: Int) -> DateTime

The moment seconds after 1970, as UTC says it.

Beside the millisecond pair because a Unix timestamp is usually seconds: that is what extract(epoch) gives, what a JSON API sends, and what a log line carries. Multiplying by a thousand at every call site is the kind of thing that is right until once it is not.

pub fn to_unix_seconds(self) -> Int

Seconds since 1970, reading self as UTC.

Floored, so a moment before 1970 rounds towards the past and not towards zero. -1 millisecond is second -1, which is the second that contains it, and truncating division would answer 0 – a whole second on the wrong side of the epoch.

pub fn at_offset(millis: Int, offset: Offset) -> DateTime

The same moment, seen from offset.

The one place a zone enters, and it enters as a number somebody else worked out.

pub fn from_offset(self, offset: Offset) -> Int

Milliseconds since 1970, reading self as a wall clock at offset.

impl Eq for Date
fn eq(self, other: Date) -> Bool
impl Ord for Date
fn cmp(self, other: Date) -> Ordering
impl Eq for Time
fn eq(self, other: Time) -> Bool
impl Ord for Time
fn cmp(self, other: Time) -> Ordering
impl Show for Date
fn show(self) -> String

ISO 8601: 2026-08-25. Zero-padded, because a date that is sometimes nine characters and sometimes ten does not sort as text and half the world sorts dates as text.

impl Show for Time
fn show(self) -> String

ISO 8601: 09:05:00.000. The milliseconds are always there, for the same reason the zeros are.

impl Show for DateTime
fn show(self) -> String

ISO 8601 with a T, and no zone — because there is none. A Z here would be a claim this type does not make.

impl Show for Offset
fn show(self) -> String

+00:00, +05:30, -05:00.

pub fn days_in_month(year: Int, month: Int) -> Option<Int>

Days in month of year, or None if month is not 1 to 12.

Only interesting for February, and only an Option because of the range check. 31 is the worst possible answer for a month that does not exist: the main use of this function is clamping a day to the end of a month, and a clamp that answers 31 does not clamp. Date::of refuses a month outside 1 to 12, and so does this, so the two agree about whether a month index is checked.

pub fn is_leap(year: Int) -> Bool

Whether year has a twenty-ninth of February.

Divisible by four, except centuries, except every fourth century. The rule is written out rather than looked up because it is three comparisons and every reader can check it.

Khora 0.3.0built frombd964738afb6on 2026-10-17 20:41ZVersioningSecurityContributingReleasesSource