moondate
API · Autumn

The reference.

Every public type, function and constant, grouped by package. Generated from pkg.generated.mbti and the source's own documentation comments.

@moondate

import { "moonbitstack/moondate" }

Types

Date

struct
pub(all) struct Date

A day on the proleptic Gregorian calendar.

Proleptic means the Gregorian rules are applied before 1582 as well, which is what every interchange format in reach assumes and what makes arithmetic over the whole range uniform.

  • year : Int32
  • month : Int32
  • day : Int32
Methods
pub fn Date::new(Int32, Int32, Int32) -> Self raise Refused

A date, checked against the calendar.

year Int32month Int32day Int32
pub fn Date::days(Self) -> Int32

Days since 1970-01-01, negative before it.

Howard Hinnant's days_from_civil: shift the year to start in March so the leap day falls at the end, then the month lengths follow a pattern with no table.

pub fn Date::diff(Self, Self) -> Span

One date less another, as a span of whole days.

other Date
pub fn Date::isocalendar(Self) -> (Int32, Int32, Int32)

The ISO 8601 week date: the year the week belongs to, the week number, and the day of the week with Monday as 1.

The year is not always the calendar year. A week belongs to the year that holds its Thursday, so the first of January can fall in the last week of the year before — which is exactly the case a hand-rolled week number gets wrong.

pub fn Date::isoweekday(Self) -> Int32

The day of the week with Monday as 1 and Sunday as 7.

[Date::weekday] counts from Sunday as 0, which is what cron and C's tm_wday use; ISO counts from Monday as 1. Both are here because both are asked for, and picking one silently is how an off-by-one gets into a schedule.

pub fn Date::of_days(Int32) -> Self

The date n days after 1970-01-01, the inverse of [Date::days].

n Int32
pub fn Date::of_isocalendar(Int32, Int32, Int32) -> Self raise Refused

The date named by an ISO week date.

year Int32week Int32weekday Int32
pub fn Date::of_ordinal(Int32) -> Self

The date with that ordinal.

n Int32
pub fn Date::ordinal(Self) -> Int32

Days since 0001-01-01, that day being 1.

The proleptic Gregorian ordinal, which is what makes two dates subtractable without going through an epoch.

pub fn Date::plus_days(Self, Int32) -> Self

The date n days later, or earlier when n is negative.

n Int32
pub fn Date::plus_months(Self, Int32) -> Self

The date n months later, clamped to the end of the month it lands in.

The thirty-first of January plus one month is the twenty-eighth or twenty-ninth of February, because there is no thirty-first. Every calendar library makes this choice and they all make the same one.

n Int32
pub fn Date::replace(Self, year? : Int32, month? : Int32, day? : Int32) -> Self raise Refused

The same date with some parts replaced.

year? Int32month? Int32day? Int32
pub fn Date::weekday(Self) -> Int32

The day of the week, Sunday being 0.

1970-01-01 was a Thursday, which is where the 4 comes from.

pub fn Date::yearday(Self) -> Int32

The day of the year, the first of January being 1.

Trait methods
pub fn Date::compare(Self, Self) -> Int32

Three-way ordering from Compare: less, equal or greater.

pub fn Date::equal(Self, Self) -> Bool

Structural equality from Eq.

pub fn Date::not_equal(Self, Self) -> Bool

The negation of equal, from Eq.

pub fn Date::op_ge(Self, Self) -> Bool

self >= other, from Compare.

pub fn Date::op_gt(Self, Self) -> Bool

self > other, from Compare.

pub fn Date::op_le(Self, Self) -> Bool

self <= other, from Compare.

pub fn Date::op_lt(Self, Self) -> Bool

self < other, from Compare.

pub fn Date::output(Self, &Logger) -> Unit

Writes the text form to a logger, from Show.

pub fn Date::to_repr(Self) -> @debug.Repr

The debug representation, from Debug.

pub fn Date::to_string(Self) -> String

The text form, as a String, from Show.

Moment

enum
pub(all) enum Moment

A point in time, in one of the four shapes a format names.

TOML 1.0.0 has exactly these four and gives them these meanings; RFC 3339 defines only Zoned. The distinction is not decoration: Plain is a wall clock, which is what an alarm or an opening time is, and turning it into an instant needs a zone nobody has supplied.

  • Zoned(date~ : Date, time~ : Time, zone~ : Zone)
  • Plain(date~ : Date, time~ : Time)
  • Day(date~ : Date)
  • Clock(time~ : Time)
Methods
pub fn Moment::asn1_text(Self) -> String raise Refused

A moment as an ASN.1 GeneralizedTime, which is the form DER uses from 2050 on and the one that needs no windowing rule to read back.

pub fn Moment::astimezone(Self, Zone) -> Self?

The same instant read in another zone.

None for the three shapes that are not instants: moving a wall clock to another zone would be inventing the zone it was written in.

zone Zone
pub fn Moment::combine(Date, Time, zone? : Zone) -> Self

A date and a time as one wall-clock moment.

zone makes it an instant instead; leaving it off keeps it a wall clock, which is what an opening time or an alarm is.

date Datetime Timezone? Zone
pub fn Moment::ctime(Self) -> String

The moment in C's ctime form: Sun Nov 6 08:49:37 1994.

The day is space-padded rather than zero-padded, which is the one thing that distinguishes this form from every other.

pub fn Moment::date(Self) -> Date?

The date out of a moment.

None for a bare time, which has no date to give.

pub fn Moment::diff(Self, Self) -> Span?

One moment less another, for the two shapes that can be subtracted.

None when they are not comparable: an instant and a wall clock differ by an amount that depends on a zone nobody supplied, and a bare time and a bare date are not the same kind of thing.

other Moment
pub fn Moment::epoch(Self) -> Int64?

Seconds since 1970-01-01T00:00:00Z, for a moment that names one.

None for the three shapes that do not: a wall clock, a bare date and a bare time are not instants until someone supplies the zone they are read in.

pub fn Moment::http_text(Self) -> String raise Refused

A moment as an HTTP date (RFC 9110 §5.6.7): Sun, 06 Nov 1994 08:49:37 GMT.

This is the one form a Date header may take. The two obsolete forms are read by [http] and never written, which is what §5.6.7 asks for.

pub fn Moment::instant(Self) -> (Int64, Int32)?

The instant, as whole seconds past 1970 and the nanoseconds after them.

The two are apart because a count of nanoseconds runs out in 2262, and because a zone whose offset is not a whole second puts the nanoseconds of the instant at odds with the nanoseconds on the clock: 15:23:30.123456789-00:00:00.000000100 is 100 nanoseconds earlier than its clock says.

pub fn Moment::isocalendar(Self) -> (Int32, Int32, Int32)?

The ISO week date of the moment's date.

pub fn Moment::isoformat(Self, sep? : Char, fraction? : Int32, offset? : Bool, zone_name? : Bool) -> String

The moment as ISO 8601 text, which is what [Show] writes.

sep is the character between the date and the time, T by default; any character works, and a space is what a log line wants.

fraction is the number of fractional-second digits, 0 to 9, truncated. Left out, it writes as many as the value needs and none for a whole second, which is Temporal's auto.

offset writes the zone the moment carries, Z for UTC and ±HH:MM for an offset; false leaves the zone off, which is Temporal's offset: "never".

zone_name follows the offset with the name the zone is filed under, in brackets: 2026-11-01T01:30:00-04:00[America/New_York], which is Temporal's timeZoneName: "critical" and the only way the text keeps the name as well as the instant.

sep~ Char = …fraction? Int32offset~ Bool = …zone_name~ Bool = …
pub fn Moment::of_epoch(Int64, nano? : Int32, zone? : Zone) -> Self

The moment seconds after 1970-01-01T00:00:00Z, the inverse of [Moment::timestamp].

Reading the clock is not this package's job — it has no I/O — so a caller that wants "now" passes what its own clock said.

seconds Int64nano~ Int32 = …zone~ Zone = …
pub fn Moment::ordinal(Self) -> Int32?

Days since 0001-01-01 of the moment's date.

pub fn Moment::plus(Self, Span) -> Self

The moment span later.

A bare date moves by whole days and a bare time wraps within the day, because that is all each of them can mean.

span Span
pub fn Moment::replace(Self, year? : Int32, month? : Int32, day? : Int32, hour? : Int32, minute? : Int32, second? : Int32, nano? : Int32, zone? : Zone) -> Self raise Refused

The same moment with some parts replaced.

year? Int32month? Int32day? Int32hour? Int32minute? Int32second? Int32nano? Int32zone? Zone
pub fn Moment::time(Self) -> Time

The time out of a moment.

Midnight for a bare date, which is the time it names.

pub fn Moment::timestamp(Self) -> Double?

Seconds since the epoch as a Double, fraction included.

pub fn Moment::timetuple(Self) -> (Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32)

The nine fields of a broken-down time, in the usual order: year, month, day, hour, minute, second, weekday counted from Monday as 0, day of the year, and whether daylight saving applies — always -1 here, because that needs a zone database this package does not carry.

pub fn Moment::to_iso_string(Self) -> String

The Date.prototype.toISOString shape: always the UTC instant, always three fractional digits, always a trailing Z.

A moment with no zone has no instant of its own, so its wall clock is taken as UTC — the same convention [Moment::strftime] uses for a bare time.

pub fn Moment::to_utc_string(Self) -> String raise Refused

The Date.prototype.toUTCString shape: Tue, 22 Sep 2026 05:30:05 GMT.

This is the HTTP date form [Moment::http_text] already writes, on the UTC instant, which is exactly what ECMAScript's toUTCString produces.

pub fn Moment::utcoffset(Self) -> Span?

How far the moment's clock is from UTC.

pub fn Moment::weekday(Self) -> Int32?

The day of the week of the moment's date, Sunday being 0.

pub fn Moment::zone(Self) -> Zone?

The zone a moment carries, or None when it carries none.

Trait methods
pub fn Moment::equal(Self, Self) -> Bool

Structural equality from Eq.

pub fn Moment::layout(Self, StringView) -> String
pub fn Moment::not_equal(Self, Self) -> Bool

The negation of equal, from Eq.

pub fn Moment::output(Self, &Logger) -> Unit

Writes the text form to a logger, from Show.

pub fn Moment::pattern(Self, StringView) -> String
pub fn Moment::strftime(Self, StringView) -> String
pub fn Moment::to_repr(Self) -> @debug.Repr

The debug representation, from Debug.

pub fn Moment::to_string(Self) -> String

The text form, as a String, from Show.

Span

struct
pub(all) struct Span

A length of time, to the nanosecond, with no calendar attached.

A span is not a number of months: a month is not a fixed length, so adding one is a calendar operation (Date::plus_months) rather than arithmetic.

  • nanos : Int64
Methods
pub fn Span::new(days? : Int64, hours? : Int64, minutes? : Int64, seconds? : Int64, millis? : Int64, micros? : Int64, nanos? : Int64) -> Self

A span made of the usual parts: days, hours, minutes, seconds and fractions.

days~ Int64 = …hours~ Int64 = …minutes~ Int64 = …seconds~ Int64 = …millis~ Int64 = …micros~ Int64 = …nanos~ Int64 = …
pub fn Span::abs(Self) -> Self

The span with its sign dropped.

pub fn Span::add(Self, Self) -> Self

Two spans added.

other Span
pub fn Span::neg(Self) -> Self

The span negated.

pub fn Span::of(Self, Int64) -> Self

A span of n of these.

unit Spann Int64
pub fn Span::parts(Self) -> (Int32, Int32, Int32)

The whole days in a span, and the seconds and nanoseconds left over — the three fields a duration is usually stored in.

pub fn Span::seconds(Self) -> Int64

The span as whole seconds, rounded towards zero.

pub fn Span::sub(Self, Self) -> Self

One span less another.

other Span
pub fn Span::total_seconds(Self) -> Double

The span as seconds, fraction included.

Trait methods
pub fn Span::compare(Self, Self) -> Int32

Three-way ordering from Compare: less, equal or greater.

pub fn Span::equal(Self, Self) -> Bool

Structural equality from Eq.

pub fn Span::not_equal(Self, Self) -> Bool

The negation of equal, from Eq.

pub fn Span::op_ge(Self, Self) -> Bool

self >= other, from Compare.

pub fn Span::op_gt(Self, Self) -> Bool

self > other, from Compare.

pub fn Span::op_le(Self, Self) -> Bool

self <= other, from Compare.

pub fn Span::op_lt(Self, Self) -> Bool

self < other, from Compare.

pub fn Span::to_repr(Self) -> @debug.Repr

The debug representation, from Debug.

Time

struct
pub(all) struct Time

A time on a twenty-four hour clock, to the nanosecond.

A second of 60 is allowed: RFC 3339 §5.6 permits it for a leap second, and a timestamp that records one is a real timestamp that a reader should not throw away.

  • hour : Int32
  • minute : Int32
  • second : Int32
  • nano : Int32
Methods
pub fn Time::new(Int32, Int32, second? : Int32, nano? : Int32) -> Self raise Refused

A time, checked against the clock.

hour Int32minute Int32second~ Int32 = …nano~ Int32 = …
pub fn Time::nanos(Self) -> Int64

Nanoseconds since midnight.

pub fn Time::replace(Self, hour? : Int32, minute? : Int32, second? : Int32, nano? : Int32) -> Self raise Refused

The same time with some parts replaced.

hour? Int32minute? Int32second? Int32nano? Int32
Trait methods
pub fn Time::compare(Self, Self) -> Int32

Three-way ordering from Compare: less, equal or greater.

pub fn Time::equal(Self, Self) -> Bool

Structural equality from Eq.

pub fn Time::not_equal(Self, Self) -> Bool

The negation of equal, from Eq.

pub fn Time::op_ge(Self, Self) -> Bool

self >= other, from Compare.

pub fn Time::op_gt(Self, Self) -> Bool

self > other, from Compare.

pub fn Time::op_le(Self, Self) -> Bool

self <= other, from Compare.

pub fn Time::op_lt(Self, Self) -> Bool

self < other, from Compare.

pub fn Time::output(Self, &Logger) -> Unit

Writes the text form to a logger, from Show.

pub fn Time::to_repr(Self) -> @debug.Repr

The debug representation, from Debug.

pub fn Time::to_string(Self) -> String

The text form, as a String, from Show.

Zone

enum
pub(all) enum Zone

How far the clock is from UTC.

Utc and Offset(0) are both midnight-at-Greenwich, but they are not the same thing to write down: RFC 3339 §4.3 gives -00:00 to a timestamp whose offset is unknown, and Z to one that is genuinely UTC.

Offset counts minutes; Named counts seconds and carries the name the zone is filed under, so VV and a Go layout's MST have something to write.

  • Utc
  • Offset(Int32)
  • Named(ZoneName)
  • Precise(Int64)
Methods
pub fn Zone::abbrev(Self) -> String

The abbreviation written for the zone: EST, GMT, +0530.

A named zone read from text that carried no abbreviation falls back to the numeric spelling, which is what a fixed offset writes anyway.

pub fn Zone::minutes(Self) -> Int32

How far east of UTC the zone is, in minutes, rounded towards zero.

A named zone whose local mean time is not a whole number of minutes loses the remainder here, which is what the %z forms write and what every other implementation writes for those years.

pub fn Zone::name(Self) -> String

The name the zone is filed under: EST5EDT for a POSIX rule, the path in the database for a named zone.

A fixed offset has no name of its own, so it is spelled the way a POSIX rule spells one — UTC, UTC-05:00 — which is what VV writes for it.

pub fn Zone::nanos(Self) -> Int64

How far east of UTC the zone is, in nanoseconds.

The finest of the three readers, and the one the others are built on: an instant is a count of nanoseconds, so this is what epoch subtracts.

pub fn Zone::seconds(Self) -> Int32

How far east of UTC the zone is, in seconds, rounded towards zero.

Trait methods
pub fn Zone::equal(Self, Self) -> Bool

Structural equality from Eq.

pub fn Zone::not_equal(Self, Self) -> Bool

The negation of equal, from Eq.

pub fn Zone::to_repr(Self) -> @debug.Repr

The debug representation, from Debug.

ZoneName

struct
pub(all) struct ZoneName

A zone named by a database, read at one instant.

The offset and the abbreviation are the ones in force at the instant the moment carries, which is what makes a Zoned a value: it can be compared, written and moved to another zone with no database in reach. Crossing a transition is a question for the rules, so a moment that has to survive one goes back through @zone.Tz::moment.

The offset is in seconds east of UTC, because a zone's local mean time before standard time was adopted is not a whole number of minutes: New York ran 4:56:02 behind Greenwich. Offsets in whole minutes are what [Zone::Offset] carries, and [Zone::seconds] puts both on the same footing.

  • name : String
  • offset : Int32
  • abbrev : String
Trait methods
pub fn ZoneName::equal(Self, Self) -> Bool

Structural equality from Eq.

pub fn ZoneName::not_equal(Self, Self) -> Bool

The negation of equal, from Eq.

pub fn ZoneName::to_repr(Self) -> @debug.Repr

The debug representation, from Debug.

Errors

Refused

suberror
pub(all) suberror Refused

Why a zone, a rule or a wall clock was refused.

A zone database is a binary format with a version byte, a set of rules whose text is a small language of its own, and a question — which instant does this clock name — that has no answer for one hour a year in most of the world. Each of those fails in its own way, and the message says which.

  • Range(field~ : String, got~ : Int32, low~ : Int32, high~ : Int32)
  • Malformed(String)
  • Truncated(String)
Trait methods
pub fn Refused::equal(Self, Self) -> Bool

Structural equality from Eq.

pub fn Refused::not_equal(Self, Self) -> Bool

The negation of equal, from Eq.

pub fn Refused::output(Self, &Logger) -> Unit

Writes the text form to a logger, from Show.

pub fn Refused::to_repr(Self) -> @debug.Repr

The debug representation, from Debug.

pub fn Refused::to_string(Self) -> String

The text form, as a String, from Show.

Functions

pub fn asn1(StringView) -> Moment raise Refused

An ASN.1 UTCTime or GeneralizedTime (ITU-T X.680 §47), as DER writes them.

This is how a certificate spells its validity. DER requires the Z and whole seconds; UTCTime's two-digit year is read as RFC 5280 §4.1.2.5.1 says — 49 and under are 2000s, 50 and over are 1900s, which is not the same rule HTTP uses.

src StringView
pub fn http(StringView) -> Moment raise Refused

An HTTP date read (RFC 9110 §5.6.7).

The preferred IMF-fixdate, and the two obsolete forms a recipient must still accept: RFC 850's Sunday, 06-Nov-94 08:49:37 GMT and asctime's Sun Nov 6 08:49:37 1994. A two-digit year is read as RFC 6265 §5.1.1 says: 69 and under are 2000s, 70 and over are 1900s.

src StringView
pub fn leap(Int32) -> Bool

Whether year has a twenty-ninth of February.

Every fourth year, except every hundredth, except every four-hundredth — the rule that makes the calendar year 365.2425 days long.

year Int32
pub fn length(Int32, Int32) -> Int32

How many days month has in year.

year Int32month Int32
pub fn of_isoformat(StringView) -> Moment raise Refused

A moment read from ISO 8601 text, which is what [parse] does.

src StringView
pub fn parse(StringView) -> Moment raise Refused

A moment read from text whose zone is named by a place.

[@moondate.parse] keeps a place name when the offset is written beside it and refuses it when it is not: the place alone does not say which side of a transition the clock was on. This is the entry point that knows — it reads the place from the database and answers with fold for the hour that has none.

let moment = @tzdata.parse("2026-11-01T01:30:00[America/New_York]") moment.strftime("%H:%M %Z") // 01:30 EDT

A text that names an offset rather than a place is answered by [@moondate.parse] alone, so this costs a database lookup and nothing else.

text StringViewfold~ @zone.Fold = …
pub fn parse_layout(StringView, StringView) -> Moment raise Refused
pub fn parse_pattern(StringView, StringView) -> Moment raise Refused
pub fn strptime(StringView, StringView) -> Moment raise Refused

Constants

pub const ANSIC : String = "Mon Jan _2 15:04:05 2006"
pub const DateOnly : String = "2006-01-02"
pub const DateTime : String = "2006-01-02 15:04:05"
pub const Kitchen : String = "3:04PM"
pub const RFC1123 : String = "Mon, 02 Jan 2006 15:04:05 MST"
pub const RFC1123Z : String = "Mon, 02 Jan 2006 15:04:05 -0700"
pub const RFC3339 : String = "2006-01-02T15:04:05Z07:00"
pub const RFC3339Nano : String = "2006-01-02T15:04:05.999999999Z07:00"
pub const RFC822 : String = "02 Jan 06 15:04 MST"
pub const RFC822Z : String = "02 Jan 06 15:04 -0700"
pub const RFC850 : String = "Monday, 02-Jan-06 15:04:05 MST"
pub const RubyDate : String = "Mon Jan 02 15:04:05 -0700 2006"
pub const Stamp : String = "Jan _2 15:04:05"
pub const StampMicro : String = "Jan _2 15:04:05.000000"
pub const StampMilli : String = "Jan _2 15:04:05.000"
pub const StampNano : String = "Jan _2 15:04:05.000000000"
pub const TimeOnly : String = "15:04:05"
pub const UnixDate : String = "Mon Jan _2 15:04:05 MST 2006"
pub let day : Span
pub let end_of_day : Time
pub let hour : Span
pub let max_date : Date
pub let max_year : Int32
pub let midnight : Time

Midnight, and the last representable moment of a day.

pub let min_date : Date

The earliest and latest dates this calendar writes with a four-digit year.

They are 1-01-01 and 9999-12-31, which is as far as a four-digit year writes; the arithmetic here works outside that range, so these are a convention for writing rather than a limit on computing.

pub let min_year : Int32

The years this calendar spans.

pub let minute : Span
pub let nanosecond : Span

Nanoseconds in a second, a minute, an hour and a day.

pub let resolution : Span

The smallest difference two moments can have.

A nanosecond rather than a microsecond: the clock here is finer, which is what a protocol timestamp and a benchmark both want.

pub let second : Span

@cron

import { "moonbitstack/moondate/cron" }

Types

Cron

struct
pub(all) struct Cron

A parsed expression: which values of each field it fires on.

Each field is a bit set, so asking whether a moment matches is six bit tests rather than a search. star_day and star_week are kept because the two fields interact — see [Cron::matches].

  • seconds : Int64
  • minutes : Int64
  • hours : Int32
  • days : Int32
  • months : Int32
  • weekdays : Int32
  • star_day : Bool
  • star_week : Bool
Methods
pub fn Cron::matches(Self, @moondate.Moment) -> Bool

Whether m is a moment this expression fires on.

The day-of-month and day-of-week fields are or-ed when both are restricted, which is Vixie cron's rule and surprises everyone once: 0 0 13 * 5 is the thirteenth *or* any Friday, not Friday the thirteenth. When one of them is * they are and-ed, which is why 0 0 13 * * is only the thirteenth.

m @moondate.Moment
pub fn Cron::next(Self, @moondate.Moment) -> @moondate.Moment?

The first moment strictly after m that this expression fires on.

None when it never does. An expression can name a day that does not exist — 0 0 30 2 * is the thirtieth of February — so the search is bounded at four years, which is past any leap-year cycle a real schedule turns on.

m @moondate.Moment
Trait methods
pub fn Cron::equal(Self, Self) -> Bool

Structural equality from Eq.

pub fn Cron::not_equal(Self, Self) -> Bool

The negation of equal, from Eq.

pub fn Cron::to_repr(Self) -> @debug.Repr

The debug representation, from Debug.

Errors

Refused

suberror
pub(all) suberror Refused

Why a zone, a rule or a wall clock was refused.

A zone database is a binary format with a version byte, a set of rules whose text is a small language of its own, and a question — which instant does this clock name — that has no answer for one hour a year in most of the world. Each of those fails in its own way, and the message says which.

  • Fields(got~ : Int32)
  • Range(field~ : String, got~ : Int32, low~ : Int32, high~ : Int32)
  • Malformed(String)
Trait methods
pub fn Refused::equal(Self, Self) -> Bool

Structural equality from Eq.

pub fn Refused::not_equal(Self, Self) -> Bool

The negation of equal, from Eq.

pub fn Refused::output(Self, &Logger) -> Unit

Writes the text form to a logger, from Show.

pub fn Refused::to_repr(Self) -> @debug.Repr

The debug representation, from Debug.

pub fn Refused::to_string(Self) -> String

The text form, as a String, from Show.

Functions

pub fn parse(StringView) -> Cron raise Refused

A moment read from text whose zone is named by a place.

[@moondate.parse] keeps a place name when the offset is written beside it and refuses it when it is not: the place alone does not say which side of a transition the clock was on. This is the entry point that knows — it reads the place from the database and answers with fold for the hour that has none.

let moment = @tzdata.parse("2026-11-01T01:30:00[America/New_York]") moment.strftime("%H:%M %Z") // 01:30 EDT

A text that names an offset rather than a place is answered by [@moondate.parse] alone, so this costs a database lookup and nothing else.

text StringViewfold~ @zone.Fold = …

@zone

import { "moonbitstack/moondate/zone" }

Types

ClockRule

struct
pub(all) struct ClockRule

When a change happens: which day, and how long after local midnight.

The seconds can pass a day — Ireland's rules once changed the clocks at twenty-five hundred hours — because a rule is read as "so many hours after the start of that day", not as a time of day.

  • onset : Onset
  • seconds : Int32
Trait methods
pub fn ClockRule::equal(Self, Self) -> Bool

Structural equality from Eq.

pub fn ClockRule::not_equal(Self, Self) -> Bool

The negation of equal, from Eq.

pub fn ClockRule::to_repr(Self) -> @debug.Repr

The debug representation, from Debug.

Fold

enum
pub(all) enum Fold

Which side of a fold a wall clock lands on.

Most zones move their clocks twice a year, so one hour of the year happens twice and another never happens at all. Asking a zone for an instant from a wall clock therefore has three defensible answers and one that refuses.

Compatible is what Python's fold=0 and Temporal's disambiguation: "compatible" both do: the first of two repeated hours, the second of two skipped ones. Earlier and Later pick by instant. Python's fold=1 is Later for a repeated clock and Earlier for a skipped one.

  • Compatible
  • Earlier
  • Later
  • Reject
Trait methods
pub fn Fold::equal(Self, Self) -> Bool

Structural equality from Eq.

pub fn Fold::not_equal(Self, Self) -> Bool

The negation of equal, from Eq.

pub fn Fold::to_repr(Self) -> @debug.Repr

The debug representation, from Debug.

Local

enum
pub(all) enum Local

The instants a wall clock names in a zone.

Zero, one or two, and the difference is not an edge case: a timestamp written by a program in a zone that observes daylight saving is ambiguous for one hour every autumn and impossible for one hour every spring, and both have to be handled rather than papered over.

  • Exact(instant~ : Int64)
  • Ambiguous(earlier~ : Int64, later~ : Int64)
  • Skipped(earlier~ : Int64, later~ : Int64)
Methods
pub fn Local::epoch(Self, fold? : Fold) -> Int64 raise Refused

The instant a wall clock names, under a policy for the two hours a year that have no single answer.

fold~ Fold = …
pub fn Local::is_exact(Self) -> Bool

Whether the wall clock names exactly one instant.

Trait methods
pub fn Local::equal(Self, Self) -> Bool

Structural equality from Eq.

pub fn Local::not_equal(Self, Self) -> Bool

The negation of equal, from Eq.

pub fn Local::output(Self, &Logger) -> Unit

Writes the text form to a logger, from Show.

pub fn Local::to_repr(Self) -> @debug.Repr

The debug representation, from Debug.

pub fn Local::to_string(Self) -> String

The text form, as a String, from Show.

OffsetPolicy

enum
pub(all) enum OffsetPolicy

How a field edit keeps the offset that was in force before the edit.

Prefer is the default: the instant the old offset named is kept when that offset is still the one the zone has at the new reading, and when it is not, [Fold] decides. Ignore goes straight to the zone's rules. Use keeps the old offset whatever the zone says, so a change that fell in between moves the reading — the instant does not move, but the date and time and the offset in force do. Reject is the offset that refuses: a reading the old offset no longer covers is an error, even when the zone would have answered it, which is the one case the disambiguation is not the one to decide.

  • Prefer
  • Ignore
  • Use
  • Reject
Trait methods
pub fn OffsetPolicy::equal(Self, Self) -> Bool

Structural equality from Eq.

pub fn OffsetPolicy::not_equal(Self, Self) -> Bool

The negation of equal, from Eq.

pub fn OffsetPolicy::to_repr(Self) -> @debug.Repr

The debug representation, from Debug.

Onset

enum
pub(all) enum Onset

The day a daylight-saving rule changes the clocks on, as POSIX TZ strings write it.

  • MonthWeekDay(month~ : Int32, week~ : Int32, weekday~ : Int32)
  • Julian(n~ : Int32)
  • Days(n~ : Int32)
Trait methods
pub fn Onset::equal(Self, Self) -> Bool

Structural equality from Eq.

pub fn Onset::not_equal(Self, Self) -> Bool

The negation of equal, from Eq.

pub fn Onset::to_repr(Self) -> @debug.Repr

The debug representation, from Debug.

Reading

struct
pub(all) struct Reading

What a zone reads at one instant: the offset in force, whether daylight saving was on, and the abbreviation written for it.

epoch is when the reading began, not the instant it was asked about, so a table of readings doubles as the list of changes.

  • epoch : Int64
  • offset : Int32
  • is_dst : Bool
  • abbrev : String
Trait methods
pub fn Reading::equal(Self, Self) -> Bool

Structural equality from Eq.

pub fn Reading::not_equal(Self, Self) -> Bool

The negation of equal, from Eq.

pub fn Reading::to_repr(Self) -> @debug.Repr

The debug representation, from Debug.

Rule

struct
pub(all) struct Rule

A POSIX TZ string: the standard offset, and either no daylight saving or the two changes that bracket it.

RFC 8536 §3.3 puts one of these in every TZif footer, which is how a file that stops in 2037 still answers for 2100. The grammar is POSIX's TZ, extended there to allow a quoted name like <+0545> and seconds in the offsets.

  • std_offset : Int32
  • std_abbrev : String
  • dst_offset : Int32?
  • dst_abbrev : String
  • dst_start : ClockRule?
  • dst_end : ClockRule?
Methods
pub fn Rule::at(Self, Int64) -> Reading

What the rule reads at an instant.

The two changes are placed year by year rather than tabulated: a rule is a pattern, and asking it about a date a century out costs a handful of date computations, which is how a TZif footer answers past the end of its table.

epoch Int64
pub fn Rule::parse(StringView) -> Self raise Refused

A POSIX TZ string read.

EST5EDT,M3.2.0,M11.1.0 is New York; UTC0 is UTC; <+0545>-5:45 is a fixed offset with a name that needs the angle brackets because of the sign. The offset in the string is what to *add* to local time to get UTC, so its sign is the opposite of the one a timestamp writes, which is the one trap in the whole format.

text StringView
pub fn Rule::text(Self) -> String

The string a rule is written as, in the form [Rule::parse] reads.

Trait methods
pub fn Rule::equal(Self, Self) -> Bool

Structural equality from Eq.

pub fn Rule::not_equal(Self, Self) -> Bool

The negation of equal, from Eq.

pub fn Rule::to_repr(Self) -> @debug.Repr

The debug representation, from Debug.

Tz

struct
pub struct Tz
  • name : String
  • readings : Array[Reading]
  • rule : Rule?
Methods
pub fn Tz::abbrev(Self, Int64) -> String

The abbreviation the zone writes at an instant.

epoch Int64
pub fn Tz::at(Self, Int64) -> Reading

What the zone reads at an instant.

epoch Int64
pub fn Tz::changes(Self, from~ : Int64, to~ : Int64) -> Array[Reading]

The readings that begin between two instants, from included and to not.

from~ Int64to~ Int64
pub fn Tz::epoch(Self, @moondate.Date, @moondate.Time, fold? : Fold) -> Int64 raise Refused

The instant a wall clock names, under a policy for the two hours a year that have no single answer.

date @moondate.Datetime @moondate.Timefold~ Fold = …
pub fn Tz::is_dst(Self, Int64) -> Bool

Whether daylight saving was on at an instant.

epoch Int64
pub fn Tz::moment(Self, Int64, nano? : Int32) -> @moondate.Moment

The instant epoch read in this zone.

This is the direction that needs no policy: an instant is one instant, and the zone only says what the clock read at it.

epoch Int64nano~ Int32 = …
pub fn Tz::name(Self) -> String

The name the zone is filed under.

pub fn Tz::of(name~ : String, readings~ : Array[Reading], rule? : Rule) -> Self

A zone built from readings that were computed elsewhere.

The first reading is the one in force from the beginning of time; the rest each begin at their own epoch. A zone database that is not a TZif file — a table of its own, a test fixture — comes in through here.

name~ Stringreadings~ Array[Reading]rule? Rule
pub fn Tz::offset(Self, Int64) -> Int32

How far east of UTC the zone is at an instant, in seconds.

epoch Int64
pub fn Tz::parse(Bytes, name? : String) -> Self raise Refused

A TZif file read (RFC 9636).

Versions 1 to 4 are read. A version 2 file carries the same zone twice — once in 32-bit transition times, once in 64-bit — and the second copy is the one used, because it is the one that keeps the years past 2038.

Leap-second records are checked for shape and then left alone: a zone answers for civil time, and this library already lets a Time hold the sixtieth second of a minute, so a leap second needs no table here.

bytes Bytesname~ String = …
pub fn Tz::read(Self, @moondate.Date, @moondate.Time) -> Local

The instants a wall clock names in this zone.

date @moondate.Datetime @moondate.Time
pub fn Tz::readings(Self) -> Array[Reading]

The readings the zone has been through, oldest first.

pub fn Tz::rule(Self) -> Rule?

The rule that carries on past the last reading, when the zone has one.

pub fn Tz::zoned(Self, @moondate.Moment, fold? : Fold) -> @moondate.Moment raise Refused

A wall clock pinned to this zone.

A moment that already names an instant is moved to this zone instead, which is what @moondate.Moment::astimezone does with a fixed offset and this does with a whole database.

moment @moondate.Momentfold~ Fold = …

ZonedDateTime

struct
pub(all) struct ZonedDateTime

A date and a time that already names an instant, carried in a zone.

The instant is the point on the line — epoch seconds and nano nanoseconds past 1970 — and the zone is what the clocks read at it. The two together are the value Temporal calls a ZonedDateTime: move it to another zone and the instant does not move, only the reading does; add a duration and the instant moves by it, whatever a day happens to be here.

What [@moondate.Moment] carries is a Zone — a name with the offset in force at that instant beside it. That is a value that can be written and compared with no database in reach, but it cannot cross a change, because a change is a question for the rules. This type is for that: it keeps a Tz, the readings and the rule that carries on past them, so a step that lands on the other side of a change reads the new offset back from the rules rather than carrying a stale one forward.

let zone = @tzdata.load("America/New_York") let zoned = ZonedDateTime::of(2026, 3, 8, hour=2, minute=30, zone~) // 02:30 never happens, so the compatible answer is the later instant zoned.isoformat() // 2026-03-08T03:30:00-04:00[America/New_York]

The zone it is built in is what it is filed under. A wall clock that is Ambiguous or Skipped is answered by [Fold]; under Reject it raises rather than guessing. A field that is out of range is refused as [Refused::BadDate], the calendar and the clock's own refusal carried here.

  • epoch : Int64
  • nano : Int32
  • tz : Tz
Methods
pub fn ZonedDateTime::abbrev(Self) -> String

The abbreviation the zone writes at the instant: EST, EDT, UTC.

pub fn ZonedDateTime::date(Self) -> @moondate.Date

The date the clocks read at the instant, in the zone.

pub fn ZonedDateTime::day(Self) -> Int32

The day the clocks read at the instant, in the zone.

pub fn ZonedDateTime::epoch(Self) -> Int64

Seconds since 1970-01-01T00:00:00Z, the point on the line.

pub fn ZonedDateTime::from_unix(Int64, nano? : Int32, zone~ : Tz) -> Self

An instant in a zone, the direction that needs no policy.

An instant is one instant, and the zone only says what the clocks read at it. This is what an arithmetic step lands on, and what a value is moved to with [into].

epoch Int64nano~ Int32 = …zone~ Tz
pub fn ZonedDateTime::hour(Self) -> Int32

The hour the clocks read at the instant, in the zone, 0 to 23.

pub fn ZonedDateTime::into(Self, Tz) -> Self

The same instant carried in another zone.

The instant does not move; only the reading does. This is what @moondate.Moment::astimezone is, with a whole database in place of a fixed offset.

zone Tz
pub fn ZonedDateTime::is_dst(Self) -> Bool

Whether daylight saving was on at the instant.

pub fn ZonedDateTime::isoformat(Self, fraction? : Int32, zone_name? : Bool) -> String

The instant as the ISO 8601 text it reads in the zone: the date and time the clocks show, the offset in force beside it, and the name in brackets.

2026-09-22T14:30:00+08:00[Asia/Shanghai]

fraction is the number of fractional-second digits, 0 to 9; left out, as many as the value needs. zone_name leaves the name off when false.

fraction? Int32zone_name~ Bool = …
pub fn ZonedDateTime::minute(Self) -> Int32

The minute the clocks read at the instant, in the zone, 0 to 59.

pub fn ZonedDateTime::moment(Self) -> @moondate.Moment

The Moment the zone writes for the instant: a Zoned that carries the name and the offset in force, so it can be written back with no database in reach.

This is the round trip that makes [of] and the writers agree: read the value back as a moment, and the offset beside it is the one in force.

pub fn ZonedDateTime::month(Self) -> Int32

The month the clocks read at the instant, in the zone, 1 to 12.

pub fn ZonedDateTime::nanos(Self) -> Int32

The nanoseconds of the instant, 0 to 999_999_999.

pub fn ZonedDateTime::of(Int32, Int32, Int32, hour? : Int32, minute? : Int32, second? : Int32, nano? : Int32, zone~ : Tz, fold? : Fold) -> Self raise Refused

A date and a time in a zone.

The two hours a year that have no single answer are decided by [Fold]: for a wall clock read twice, Compatible (the default) takes the earlier instant and Later the later; for one that never reads, Compatible takes the later instant and Earlier the earlier. Reject raises for either. A field that is not a date and time the calendar has is refused as [Refused::BadDate].

year Int32month Int32day Int32hour~ Int32 = …minute~ Int32 = …second~ Int32 = …nano~ Int32 = …zone~ Tzfold~ Fold = …
pub fn ZonedDateTime::of_moment(@moondate.Moment, zone~ : Tz, fold? : Fold) -> Self raise Refused

A moment the caller already has, placed in a zone.

A moment that names an instant is put in the zone; one that does not — a Plain wall clock — is answered by [Fold], and Reject raises for the hours that have none.

moment @moondate.Momentzone~ Tzfold~ Fold = …
pub fn ZonedDateTime::offset(Self) -> Int32

How far east of UTC the zone is at the instant, in seconds.

pub fn ZonedDateTime::offset_minutes(Self) -> Int32

How far east of UTC the zone is at the instant, in whole minutes.

A local mean time that is not a whole number of minutes loses the remainder, which is what an offset written ±HH:MM says.

pub fn ZonedDateTime::plus(Self, @moondate.Span) -> Self

The instant span later, in the same zone.

The span is added to the point on the line, not to the reading: a day added here is the instant a day later, and if a change falls in between, the reading moves across it. That is what a duration means for an instant, and the one thing that keeps a span added the same however it is split.

span @moondate.Span
pub fn ZonedDateTime::plus_days(Self, Int64) -> Self

The instant days later, in the same zone.

days Int64
pub fn ZonedDateTime::plus_months(Self, Int64, offset? : OffsetPolicy, fold? : Fold) -> Self raise Refused

The instant months later, in the same zone.

A month is a calendar step, so the reading is moved rather than the instant: the same day of the month, clamped to one that exists, is read in the month months later, and the offset that was in force is kept if it still is. Across a change the result is not the old instant plus a fixed number of days, because that is not what a month is.

months Int64offset~ OffsetPolicy = …fold~ Fold = …
pub fn ZonedDateTime::reading(Self) -> Reading

What the zone read at the instant: the offset, the abbreviation, and whether daylight saving was on.

pub fn ZonedDateTime::second(Self) -> Int32

The second the clocks read at the instant, in the zone, 0 to 59.

A leap second is kept as the sixtieth second of its minute, the way the clock here does; it is not projected to the next minute.

pub fn ZonedDateTime::time(Self) -> @moondate.Time

The time the clocks read at the instant, in the zone.

pub fn ZonedDateTime::to_string(Self) -> String

The instant as a sentence: 2026-09-22T14:30:00+08:00 Asia/Shanghai.

pub fn ZonedDateTime::with_day(Self, Int32, offset? : OffsetPolicy, fold? : Fold) -> Self raise Refused

The instant with the day changed, the rest of the reading kept. A day the month does not have is refused as [Refused::BadDate].

day Int32offset~ OffsetPolicy = …fold~ Fold = …
pub fn ZonedDateTime::with_hour(Self, Int32, offset? : OffsetPolicy, fold? : Fold) -> Self raise Refused

The instant with the hour changed, the rest of the reading kept.

hour Int32offset~ OffsetPolicy = …fold~ Fold = …
pub fn ZonedDateTime::with_minute(Self, Int32, offset? : OffsetPolicy, fold? : Fold) -> Self raise Refused

The instant with the minute changed, the rest of the reading kept.

minute Int32offset~ OffsetPolicy = …fold~ Fold = …
pub fn ZonedDateTime::with_month(Self, Int32, offset? : OffsetPolicy, fold? : Fold) -> Self raise Refused

The instant with the month changed, the rest of the reading kept. A month that leaves no such day — the thirty-first into February — is refused as [Refused::BadDate].

month Int32offset~ OffsetPolicy = …fold~ Fold = …
pub fn ZonedDateTime::with_nanos(Self, Int32) -> Self

The instant with the nanoseconds changed, the rest of the reading kept.

The offset is in seconds, so the seconds the instant names are not moved: only the fraction is replaced, and the reading is read back from the zone.

nanos Int32
pub fn ZonedDateTime::with_second(Self, Int32, offset? : OffsetPolicy, fold? : Fold) -> Self raise Refused

The instant with the second changed, the rest of the reading kept.

second Int32offset~ OffsetPolicy = …fold~ Fold = …
pub fn ZonedDateTime::with_year(Self, Int32, offset? : OffsetPolicy, fold? : Fold) -> Self raise Refused

The instant with the year changed, the rest of the reading kept.

year Int32offset~ OffsetPolicy = …fold~ Fold = …
pub fn ZonedDateTime::year(Self) -> Int32

The year the clocks read at the instant, in the zone.

pub fn ZonedDateTime::zone(Self) -> Tz

The zone the instant is carried in.

pub fn ZonedDateTime::zone_name(Self) -> String

The name the zone is filed under.

Trait methods
pub fn ZonedDateTime::output(Self, &Logger) -> Unit

Writes the text form to a logger, from Show.

Errors

Refused

suberror
pub(all) suberror Refused

Why a zone, a rule or a wall clock was refused.

A zone database is a binary format with a version byte, a set of rules whose text is a small language of its own, and a question — which instant does this clock name — that has no answer for one hour a year in most of the world. Each of those fails in its own way, and the message says which.

  • Malformed(kind~ : String, at~ : Int32)
  • Truncated(want~ : Int32, got~ : Int32)
  • Unsupported(version~ : Char)
  • Unknown(name~ : String)
  • Missing(text~ : String)
  • BadDate(@moondate.Refused)
Trait methods
pub fn Refused::equal(Self, Self) -> Bool

Structural equality from Eq.

pub fn Refused::not_equal(Self, Self) -> Bool

The negation of equal, from Eq.

pub fn Refused::output(Self, &Logger) -> Unit

Writes the text form to a logger, from Show.

pub fn Refused::to_repr(Self) -> @debug.Repr

The debug representation, from Debug.

pub fn Refused::to_string(Self) -> String

The text form, as a String, from Show.

@tzdata

import { "moonbitstack/moondate/tzdata" }

Errors

Refused

suberror
pub suberror Refused

Why a zone, a rule or a wall clock was refused.

A zone database is a binary format with a version byte, a set of rules whose text is a small language of its own, and a question — which instant does this clock name — that has no answer for one hour a year in most of the world. Each of those fails in its own way, and the message says which.

  • BadText(@moondate.Refused)
  • BadZone(@zone.Refused)
Trait methods
pub fn Refused::output(Self, &Logger) -> Unit

Writes the text form to a logger, from Show.

pub fn Refused::to_string(Self) -> String

The text form, as a String, from Show.

Functions

pub fn find(StringView) -> @zone.Tz?

The zone filed under name, or None when the database has no such name.

name StringView
pub fn load(StringView) -> @zone.Tz raise @zone.Refused

The zone filed under name, which may be an alias.

The zone is compiled from the release's source the first time it is asked for, which is a table of a few hundred readings per zone; a program that needs a dozen zones pays for a dozen.

name StringView
pub fn parse(StringView, fold? : @zone.Fold) -> @moondate.Moment raise Refused

A moment read from text whose zone is named by a place.

[@moondate.parse] keeps a place name when the offset is written beside it and refuses it when it is not: the place alone does not say which side of a transition the clock was on. This is the entry point that knows — it reads the place from the database and answers with fold for the hour that has none.

let moment = @tzdata.parse("2026-11-01T01:30:00[America/New_York]") moment.strftime("%H:%M %Z") // 01:30 EDT

A text that names an offset rather than a place is answered by [@moondate.parse] alone, so this costs a database lookup and nothing else.

text StringViewfold~ @zone.Fold = …
pub fn zones() -> Array[String]

Every name the database knows, the aliases included, in order.

Constants

pub let release : String

The IANA release this package carries.

pub let source_digest : String

The SHA-256 of the source text below, so a release can be recognised without reading the whole of it.

@zoneinfo

import { "moonbitstack/moondate/zoneinfo" }

Errors

Refused

suberror
pub suberror Refused

Why a zone, a rule or a wall clock was refused.

A zone database is a binary format with a version byte, a set of rules whose text is a small language of its own, and a question — which instant does this clock name — that has no answer for one hour a year in most of the world. Each of those fails in its own way, and the message says which.

  • Missing(path~ : String)
  • Unreadable(path~ : String, said~ : String)
  • BadZone(@zone.Refused)
  • Unset
Trait methods
pub fn Refused::equal(Self, Self) -> Bool

Structural equality from Eq.

pub fn Refused::not_equal(Self, Self) -> Bool

The negation of equal, from Eq.

pub fn Refused::output(Self, &Logger) -> Unit

Writes the text form to a logger, from Show.

pub fn Refused::to_repr(Self) -> @debug.Repr

The debug representation, from Debug.

pub fn Refused::to_string(Self) -> String

The text form, as a String, from Show.

Functions

pub fn directory() -> String

The directory the zones are filed in.

TZDIR when the environment names one, and /usr/share/zoneinfo otherwise, which is where the IANA release is installed on every system that has one. A machine that keeps them elsewhere — a container with a copy in the image, a program that carries its own — says so with TZDIR.

pub fn named(StringView) -> @zone.Tz raise Refused

The zone filed under name in [directory], which is what a zone name means to the system: Asia/Shanghai, America/New_York, Etc/UTC.

name StringView
pub fn open(StringView, name? : String) -> @zone.Tz raise Refused

A zone read from the TZif file at path.

let tz = @zoneinfo.open("/etc/localtime") tz.abbrev(1790058605) // EDT, on a machine set to New York

name is what the zone is filed under, and the path is what it is filed under by default: a TZif file carries no name of its own, so the only name there is is the one it was found by. [named] passes the zone's name instead.

path StringViewname~ String = …
pub fn system() -> @zone.Tz raise Refused

The zone this machine is set to.

What TZ says, and /etc/localtime when it says nothing — the two answers every other library's localtime gives, in that order:

  • TZ unset: the file at /etc/localtime, which is how a system states its

zone to everything that asks.

  • TZ empty: UTC, which POSIX says outright rather than by omission.
  • TZ a name, with or without the leading : glibc allows: the file of that

name in [directory].

  • TZ a path, starting at /: that file, wherever it is.
  • TZ anything else: a POSIX TZ rule, which is how CST-8 and

EST5EDT,M3.2.0,M11.1.0 are written and how a container states a zone without carrying a database.

A machine that names nothing is Unset rather than UTC. Guessing UTC is how a program silently writes timestamps eight hours out; a caller that wants UTC can say so, and a machine whose TZ is empty has said it.