Skip to content

date

Terminal window
bun add @stopcock/date

Dates are branded numbers (Unix ms timestamps). No Date objects get allocated anywhere, it’s all integer arithmetic. All multi-arg functions are dual-form.

import { pipe } from '@stopcock/fp'
import { now, add, startOf, format, isWeekend } from '@stopcock/date'
const nextMonday = pipe(
now(),
startOf('week'),
add(7, 'day'),
)
format(nextMonday, 'YYYY-MM-DD')
  • Timestamp is a branded number so you can’t accidentally mix raw numbers with timestamps. Use fromTimestamp() for explicit conversion.
  • Every function works both ways: add(7, 'day') returns (ts) => Timestamp for pipe, or add(ts, 7, 'day') if you want data-first.
  • Business days, holidays, timezones, and interval merging are all built in.

Measured on Bun 1.3, batch operations over 10,000 timestamps.

Operationvs date-fnsvs momentvs luxon
add days1.6x faster4.5x faster8.6x faster
add months2.3x faster6.1x faster11.5x faster
startOf day1.5x faster3.6x faster6.4x faster
startOf month2.6x faster5.4x faster10x faster
endOf year3.2x faster5.5x faster48x faster
// date-fns
import { addDays, format } from 'date-fns'
const result = format(addDays(new Date(), 7), 'yyyy-MM-dd')
// dayjs
import dayjs from 'dayjs'
const result = dayjs().add(7, 'day').format('YYYY-MM-DD')
// stopcock: no Date objects, pipes naturally
import { pipe } from '@stopcock/fp'
import { now, add, format } from '@stopcock/date'
const result = pipe(now(), add(7, 'day'), format('YYYY-MM-DD'))
import { pipe } from '@stopcock/fp'
import { fromISO, add, isWeekend, nextBusinessDay, format, isBefore, now } from '@stopcock/date'
const invoiceDueDate = (issuedAt: string, netDays: number) => {
const due = pipe(fromISO(issuedAt), add(netDays, 'day'))
return isWeekend(due) ? nextBusinessDay(due) : due
}
const due = invoiceDueDate('2026-03-15', 30)
const overdue = isBefore(due, now())
format(due, 'DD MMM YYYY') // "14 Apr 2026"
import { pipe, A } from '@stopcock/fp'
import { fromParts, startOf, endOf, daysIn, getWeekday, format, isToday, isWeekend } from '@stopcock/date'
const calendarMonth = (year: number, month: number) => {
const start = fromParts({ year, month })
const days = daysIn(startOf(start, 'month'), endOf(start, 'month'))
return A.map(days, day => ({
label: format(day, 'D'),
weekday: getWeekday(day),
today: isToday(day),
weekend: isWeekend(day),
}))
}
import { now, diffInMinutes, diffInHours, diffInDays } from '@stopcock/date'
const timeAgo = (ts: Timestamp) => {
const mins = diffInMinutes(now(), ts)
if (mins < 1) return 'just now'
if (mins < 60) return `${mins}m ago`
const hrs = diffInHours(now(), ts)
if (hrs < 24) return `${hrs}h ago`
return `${diffInDays(now(), ts)}d ago`
}
import { pipe } from '@stopcock/fp'
import { fromISO, Tz } from '@stopcock/date'
const standup = pipe(
fromISO('2026-04-03'),
ts => Tz.add(ts, 9, 'hour', 'Europe/London'),
)
Tz.format(standup, 'HH:mm z', 'America/New_York') // "04:00 EDT"
Tz.format(standup, 'HH:mm z', 'Asia/Tokyo') // "17:00 JST"
import { fromISO, mergeIntervals } from '@stopcock/date'
const bookings = [
[fromISO('2026-06-01'), fromISO('2026-06-05')],
[fromISO('2026-06-03'), fromISO('2026-06-08')],
[fromISO('2026-06-10'), fromISO('2026-06-12')],
] as [Timestamp, Timestamp][]
mergeIntervals(bookings)
// [[Jun 1 → Jun 8], [Jun 10 → Jun 12]]

type Timestamp = number & { readonly [TimestampBrand]: true }
type Duration = number & { readonly [DurationBrand]: true }
type DateUnit = 'year' | 'month' | 'week' | 'day' | 'hour' | 'minute' | 'second' | 'millisecond'
type Weekday = 0 | 1 | 2 | 3 | 4 | 5 | 6
type DateParts = { year: number; month: number; day?: number; hour?: number; minute?: number; second?: number; millisecond?: number }
now(): Timestamp
fromDate(date: Date): Timestamp
toDate(ts: Timestamp): Date
fromParts(parts: DateParts): Timestamp
fromTimestamp(ms: number): Timestamp
fromISO(iso: string): Timestamp
toTimestamp(ts: Timestamp): number
toISO(ts: Timestamp): string
getYear(ts): number getMonth(ts): number // 1-12
getDay(ts): number getWeekday(ts): Weekday // 0=Sun
getHours(ts): number getMinutes(ts): number
getSeconds(ts): number getMilliseconds(ts): number
getDayOfYear(ts): number getWeekOfYear(ts): number
getQuarter(ts): number getDaysInMonth(ts): number
getDaysInYear(ts): number isLeapYear(ts): boolean
compare(a, b): number min(...ts): Timestamp
max(...ts): Timestamp clamp(ts, lo, hi): Timestamp
isBefore(a, b): boolean isAfter(a, b): boolean
isEqual(a, b): boolean isSameDay(a, b): boolean
isSameMonth(a, b): boolean isSameYear(a, b): boolean
isBetween(ts, start, end): boolean
isWeekend(ts): boolean isWeekday(ts): boolean
isToday(ts): boolean isPast(ts): boolean
isFuture(ts): boolean isValid(ts): boolean
add(ts, amount, unit): Timestamp
subtract(ts, amount, unit): Timestamp
startOf(ts, unit): Timestamp
endOf(ts, unit): Timestamp
setYear(ts, year): Timestamp setMonth(ts, month): Timestamp
setDay(ts, day): Timestamp setHours(ts, hours): Timestamp
setMinutes(ts, minutes): Timestamp setSeconds(ts, seconds): Timestamp
diff(a, b, unit): number diffInDays(a, b): number
diffInHours(a, b): number diffInMinutes(a, b): number
diffInSeconds(a, b): number diffInMonths(a, b): number
diffInYears(a, b): number
roundTo(ts, unit): Timestamp ceilTo(ts, unit): Timestamp
floorTo(ts, unit): Timestamp snapTo(ts, interval, unit): Timestamp
duration(amount, unit): Duration
addDuration(ts, d): Timestamp subtractDuration(ts, d): Timestamp
toDuration(ms): Duration durationToUnit(d, unit): number
scaleDuration(d, factor): Duration negateDuration(d): Duration
range(start, end, step, unit): Timestamp[]
rangeBy(start, end, stepFn): Timestamp[]
daysIn(start, end): Timestamp[] weekdaysIn(start, end): Timestamp[]
sequence(start, count, step, unit): Timestamp[]
overlaps(a, b): boolean contains(interval, ts): boolean
intersection(a, b): [Timestamp, Timestamp] | null
union(a, b): [Timestamp, Timestamp] | null
gap(a, b): [Timestamp, Timestamp] | null
mergeIntervals(intervals): [Timestamp, Timestamp][]
isBusinessDay(ts): boolean
addBusinessDays(ts, days): Timestamp
subtractBusinessDays(ts, days): Timestamp
businessDaysBetween(a, b): number
nextBusinessDay(ts): Timestamp prevBusinessDay(ts): Timestamp
addBusinessDaysWithHolidays(ts, days, holidays): Timestamp
format(ts, pattern): string formatter(pattern): (ts) => string
parse(input, pattern): Timestamp parser(pattern): (input) => Timestamp
tryParse(input, pattern): Timestamp | null
tryParser(pattern): (input) => Timestamp | null
parseISO(input): Timestamp
Tz.utcToLocal(ts, zone): Timestamp
Tz.localToUTC(localMs, zone): Timestamp
Tz.startOf(ts, unit, zone): Timestamp
Tz.endOf(ts, unit, zone): Timestamp
Tz.add(ts, amount, unit, zone): Timestamp
Tz.format(ts, pattern, zone): string
Tz.diff(a, b, unit, zone): number
Tz.isSameDay(a, b, zone): boolean
Tz.isWeekend(ts, zone): boolean
Tz.isToday(ts, zone): boolean
Tz.getOffsetMinutes(ts, zone): number
Tz.getOffsetString(ts, zone): string