@zxkit/chrono
Calendar dates and instants that never throw.
Your server runs in UTC; your business does not. chrono keeps calendar days and timezone-aware instants apart with a branded PlainDate and a total API — invalid input returns null, never an exception.
Boundary
parse once
DB rows, form input, URL params — one parse at the edge returns null instead of throwing. Past it, failure is unrepresentable.
import { parsePlainDate, addMonths, toUtcMidnight } from '@zxkit/chrono'
const due = parsePlainDate(row.dueDate) // PlainDate | null — never throws
if (!due) return badRequest()
addMonths(due, 1) // 'Jan 31' + 1 → 'Feb 28' — clamps, no overflow
due < tz.today() // native string comparison
toUtcMidnight(due) // Date at UTC midnight, ready for a DATE columnZone
app-date.ts
Bind the IANA zone and locale once. The server can run in UTC anywhere; results never change.
import { zone } from '@zxkit/chrono'
export const tz = zone(process.env.APP_TZ ?? 'America/New_York', {
locale: 'en-US',
})
tz.today() // PlainDate of the current local day
tz.dayOf(order.paidAt) // day containing that instant
tz.startOfDay() // instant of today's local midnight
tz.toInstant(day, '14:30') // local wall clock → instantQueries
ranges
Half-open { gte, lt } instant ranges for timestamp filters, UTC-midnight bounds for DATE columns. No hand-built boundaries.
db.order.aggregate({ where: { paidAt: tz.monthRange() } })
db.order.findMany({ where: { paidAt: tz.dayRange(day) } })
db.order.findMany({ where: { paidAt: tz.rangeBetween(from, to) } })
// DATE columns compare against UTC midnights instead
db.invoice.findMany({ where: { issuedOn: toUtcRange(from, to) } })Display
formatting
Intl-based formatting with the zone locale preset. Bad locales and options degrade to ISO strings instead of crashing.
tz.format(day) // 'July 1, 2026'
tz.formatRange(from, to) // 'July 1 – 15, 2026'
tz.formatTime(order.paidAt) // '14:05'
tz.formatRelative(order.paidAt) // '5 minutes ago'- Total API
- Invalid input returns null at the boundary. Arithmetic saturates. Formatters fall back to ISO. Nothing throws.
- Branded PlainDate
- A 'YYYY-MM-DD' string type: serializes across RSC and JSON for free, compares with < > ===, and never mixes with instants.
- DATE column semantics
- parsePlainDate reads UTC midnights, toUtcMidnight writes them back. One boundary, no shifted days.
- DST safe
- Midnights skipped or repeated by DST resolve to the first existing instant of the day, in any IANA zone.
- Zero dependencies
- Native Intl only, with cached formatters. Identical results on server, edge, and client — whatever the process timezone.