zxkit

@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.

bun add @zxkit/chronoDocumentationnpm

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 column

Zone

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 → instant

Queries

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.