Civil time is time as a document states it: a date on an invoice, a closing time on a notice, a birth date on an identity record. It carries no zone, and world keeps it that way - a zone is a separate concern, applied deliberately, not smuggled in by a default.

import world.*

Dates

A date is validated once, at construction. A constant is checked while the build runs, so a fixture pays no Either ceremony for a value the compiler can already see is a date:

Date(2026, 7, 23).value

Date("2023-02-29") fails the build rather than the test run. Runtime input goes through the validating constructors instead, and comes back as a value you can branch on:

Date.of(2023, 2, 29)

Date.parse("2026-07-23").map(_.weekday)

Arithmetic states its policy

Adding a month to the 31st has two defensible answers, and world will not pick one for you: the policy is a parameter, so the reader of the call site can see which was chosen.

Date(2026, 1, 31).plus(Months(1), Overflow.Constrain).map(_.value)

Date(2026, 1, 31).plus(Months(1), Overflow.Reject).isLeft

Ages are their own operation rather than a composition, because composing them is subtly wrong: a person born on 29 February attains their anniversary on 28 February in common years, which month arithmetic alone does not give you.

Date(2008, 2, 29).years(Date(2026, 2, 28))

Where a statute reads that anniversary as 1 March instead, say so explicitly:

Date(2008, 2, 29).plus(Days(1)).map(_.years(Date(2026, 2, 28)))

Months and end-of-month

YearMonth is the bridge that statements, ageing buckets, and payment terms all walk:

Date(2026, 7, 26).yearMonth.last.value

YearMonth.of(2028, 2).map(_.length)

Week numbering follows the territory

Week one is not the same week everywhere. The rule is the territory's, and it is data:

Territory.GB.week.number(Date(2026, 1, 1))

Territory.GB.week.number(Date(2028, 1, 1))

Territory.US.week.number(Date(2026, 1, 1))

The third answer differs from the second because the United States needs only one day of the new year in a week for it to count as the first, where the United Kingdom needs four.

Times and date-times

Time runs to second precision and admits 24:00:00 as the end of a day, which is how notices and contracts state a closing time. Constructing a DateTime from it normalises onto the following midnight, so the two spellings of one instant cannot both exist:

DateTime(Date(2026, 7, 26), Time.of(24, 0, 0).toOption.get).value

DateTime.parse("2026-07-26T14:30:05").map(_.time.value)

Interest over a period

A day-count convention turns a date range into an exact fraction of a year, which Money.scaled then applies at a named rounding boundary:

Basis.Actual365F.fraction(Date(2026, 1, 1), Date(2026, 7, 1))

Basis.Thirty360.fraction(Date(2026, 1, 1), Date(2026, 7, 1))

Calendars label the same day

A Date is an epoch day, not a Gregorian date that other calendars convert from. Every calendar reads its own labels off that one value, so conversion is composition through the day and arithmetic never enters a calendar at all:

import world.Calendar.{Buddhist, Coptic, Ethiopic}

val day = Date(2025, 9, 11)

Ethiopic.at(day)

Coptic.at(day)

Buddhist.at(day)

Entry runs through the named calendar, which is what stops a Buddhist-era year reaching a Gregorian constructor - on their own, the components are just three numbers:

Buddhist.of(2569, 1, 1).map(_.value)

Ethiopic.of(2015, 13, 6).map(_.value)

The thirteenth month is short - five days, six in a leap year - and says so:

Ethiopic.of(2016, 13, 6).isLeft

Your own calendar is one more instance. A pure year offset is a single line:

object Anka extends Calendar.Offset("ANKA", -1000)
Anka.at(Date(2026, 7, 23))

Age and majority

A majority check is a composition, not an operation: add the years to the birth date and compare. The leap-day convention is the one Overflow names, so 29 February plus eighteen years is 28 February under Constrain - stated rather than assumed:

val born = Date(2008, 2, 29)
val today = Date(2026, 7, 23)

born.plus(Years(18), Overflow.Constrain).map(Ordering[Date].lteq(_, today))

years answers the same question as a count where a form needs the number itself:

born.years(today)