Skip to content

Date and Time Only

"No time zone" is a real concept. A calendar date (2024-01-15) and a wall-clock time (08:00) are values you read off a calendar or clock — a daily alarm is 8:00 AM regardless of DST or travel. ZDateOnly and ZTimeOnly model these directly instead of forcing a fake midnight or a fake date into a zoned instant.

Both types carry the same verb set as ZDatecompare, by, to, diff — scoped to their fields.

Why These Types Exist

A carefully configured ZDate can simulate date-only or time-only behavior, but correctness would depend on remembering the same rules at every call site: use the neutral zone, anchor the ignored fields, prevent projection changes from shifting the value, avoid epoch and full-date operations that do not belong. These types exist for two reasons:

Runtime safety. The wrappers encapsulate those invariants once. Neither exposes its internal instant or epoch, ZDateOnly exposes no time fields, ZTimeOnly exposes no date fields, and both block adjustments of the wrong kind at the type level. What the wrapper encapsulates, the caller cannot break.

TypeScript contracts. These signatures communicate different intent, and the compiler enforces the difference:

ts
function schedule(date: ZDate): void
function schedule(date: ZDateOnly): void

ZDateOnly

ts
import {ZDateOnly} from '@sajajs/zdate'

const d = ZDateOnly.from('2024-01-15')

d.year          // 2024
d.month         // 1
d.day           // 15
d.era
d.calendar      // iso8601 - ZCalendar

d.by('2 weeks')
d.to('start of month')
d.diff(other, 'day')
d.compare(other)
MemberDescription
ZDateOnly.from(source?)From a string, fields, or nothing (today)
ZDateOnly.min(...) / .max(...)Extremes
toFields(){year, month, day, era}
toJSON() / toString()ISO date string
valueOf()Days — a domain-scaled number, never the underlying instant

Accepted date string forms:

ts
ZDateOnly.from('2025-03-15')              // full date
ZDateOnly.from('2025-03')                 // year-month; day anchored to 1
ZDateOnly.from('03-15')                   // month-day; year anchored to 0
ZDateOnly.from('20250315')                // compact form of any of the above
ZDateOnly.from('+012025-03')              // expanded signed year
ZDateOnly.from('2025-03-15[u-ca=hebrew]') // ISO fields; the instance's calendar is hebrew

String numerals are always ISO 8601 calendar dates — an annotation sets the instance's calendar, never the interpretation of the numerals. An RFC 9557 timestamp is an RFC 3339 date-time in the ISO 8601 calendar, regardless of the suffix calendar. ZDateOnly follows that for its date-only fields. To supply fields in another calendar, use a fields object with its calendar property:

ts
// the same day as '2025-03-15[u-ca=hebrew]', supplied as Hebrew fields
ZDateOnly.from({year: 5785, month: 6, day: 15, calendar: 'hebrew'})

ZTimeOnly

ts
import {ZTimeOnly} from '@sajajs/zdate'

const t = ZTimeOnly.from('08:00')

t.hour          // 8
t.minute        // 0
t.second
t.nanosecond

t.by({minutes: 90})
t.to({nearest: 'minute', interval: 15})
t.diff(other, 'minute')
MemberDescription
ZTimeOnly.from(source?)From a string, fields, or nothing (current time)
ZTimeOnly.min(...) / .max(...)Extremes
toFields(){hour, minute, second, nanosecond}
toJSON() / toString()ISO time string
valueOf()Nanoseconds since midnight — a domain-scaled number, never the underlying instant

Accepted time string forms:

ts
ZTimeOnly.from('14:30')           // HH:MM
ZTimeOnly.from('14')              // hour only
ZTimeOnly.from('14:30:45.25')     // fractional seconds, up to nanoseconds
ZTimeOnly.from('T1430')           // compact form, optional T prefix on any form

Arithmetic wraps at midnight: 23:30 moved by two hours reads 01:30. Neither type exposes its internal instant or epoch, so the wrap happens behind the sealed boundary — nothing observable rolls over but the time.

Combining

A date-only plus a time-only plus a zone identifies an instant; construct a ZDate from their fields when the moment becomes real:

ts
zdate({...d.toFields(), ...t.toFields()}, {zone: 'America/Chicago'})