date
bun add @stopcock/dateDates 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')Timestampis a brandednumberso you can’t accidentally mix raw numbers with timestamps. UsefromTimestamp()for explicit conversion.- Every function works both ways:
add(7, 'day')returns(ts) => Timestampforpipe, oradd(ts, 7, 'day')if you want data-first. - Business days, holidays, timezones, and interval merging are all built in.
Benchmarks
Section titled “Benchmarks”Measured on Bun 1.3, batch operations over 10,000 timestamps.
| Operation | vs date-fns | vs moment | vs luxon |
|---|---|---|---|
| add days | 1.6x faster | 4.5x faster | 8.6x faster |
| add months | 2.3x faster | 6.1x faster | 11.5x faster |
| startOf day | 1.5x faster | 3.6x faster | 6.4x faster |
| startOf month | 2.6x faster | 5.4x faster | 10x faster |
| endOf year | 3.2x faster | 5.5x faster | 48x faster |
| Operation | vs date-fns | vs moment | vs luxon |
|---|---|---|---|
| YYYY-MM-DD HH:mm:ss | 7.3x faster | 3.2x faster | 5.9x faster |
| ddd, DD MMM YYYY hh:mm A | 8.4x faster | 3.4x faster | 6.7x faster |
| Operation | vs date-fns | vs moment | vs luxon |
|---|---|---|---|
| diffInDays | 73x faster | 45x faster | 308x faster |
| diffInMonths | 13x faster | 25x faster | 45x faster |
| diffInYears | 10x faster | 25x faster | 44x faster |
Comparisons
Section titled “Comparisons”// date-fnsimport { addDays, format } from 'date-fns'const result = format(addDays(new Date(), 7), 'yyyy-MM-dd')
// dayjsimport dayjs from 'dayjs'const result = dayjs().add(7, 'day').format('YYYY-MM-DD')
// stopcock: no Date objects, pipes naturallyimport { pipe } from '@stopcock/fp'import { now, add, format } from '@stopcock/date'const result = pipe(now(), add(7, 'day'), format('YYYY-MM-DD'))// date-fns: needs date-fns-tz, separate adapterimport { toZonedTime } from 'date-fns-tz'import { isWeekend } from 'date-fns'isWeekend(toZonedTime(new Date(), 'Asia/Tokyo'))
// dayjs: needs timezone + utc pluginsimport dayjs from 'dayjs'import utc from 'dayjs/plugin/utc'import tz from 'dayjs/plugin/timezone'dayjs.extend(utc); dayjs.extend(tz)dayjs().tz('Asia/Tokyo').day() === 0 || dayjs().tz('Asia/Tokyo').day() === 6
// stopcock: built-in, no pluginsimport { Tz, now } from '@stopcock/date'Tz.isWeekend(now(), 'Asia/Tokyo')// date-fns: no built-in business day support// You need date-fns-business-days or manual loops
// stopcock: built-inimport { now, addBusinessDaysWithHolidays, format, fromISO } from '@stopcock/date'
const holidays = [fromISO('2026-12-25'), fromISO('2026-01-01')]const delivery = addBusinessDaysWithHolidays(now(), 10, holidays)format(delivery, 'YYYY-MM-DD')Real-world patterns
Section titled “Real-world patterns”Invoice due date
Section titled “Invoice due date”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"Calendar grid
Section titled “Calendar grid”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), }))}Time-ago formatting
Section titled “Time-ago formatting”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`}Cross-timezone scheduling
Section titled “Cross-timezone scheduling”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"Merging overlapping bookings
Section titled “Merging overlapping bookings”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]]API reference
Section titled “API reference”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 | 6type DateParts = { year: number; month: number; day?: number; hour?: number; minute?: number; second?: number; millisecond?: number }Creation
Section titled “Creation”now(): TimestampfromDate(date: Date): TimestamptoDate(ts: Timestamp): DatefromParts(parts: DateParts): TimestampfromTimestamp(ms: number): TimestampfromISO(iso: string): TimestamptoTimestamp(ts: Timestamp): numbertoISO(ts: Timestamp): stringExtraction
Section titled “Extraction”getYear(ts): number getMonth(ts): number // 1-12getDay(ts): number getWeekday(ts): Weekday // 0=SungetHours(ts): number getMinutes(ts): numbergetSeconds(ts): number getMilliseconds(ts): numbergetDayOfYear(ts): number getWeekOfYear(ts): numbergetQuarter(ts): number getDaysInMonth(ts): numbergetDaysInYear(ts): number isLeapYear(ts): booleanComparison & predicates
Section titled “Comparison & predicates”compare(a, b): number min(...ts): Timestampmax(...ts): Timestamp clamp(ts, lo, hi): TimestampisBefore(a, b): boolean isAfter(a, b): booleanisEqual(a, b): boolean isSameDay(a, b): booleanisSameMonth(a, b): boolean isSameYear(a, b): booleanisBetween(ts, start, end): booleanisWeekend(ts): boolean isWeekday(ts): booleanisToday(ts): boolean isPast(ts): booleanisFuture(ts): boolean isValid(ts): booleanArithmetic
Section titled “Arithmetic”add(ts, amount, unit): Timestampsubtract(ts, amount, unit): TimestampstartOf(ts, unit): TimestampendOf(ts, unit): TimestampsetYear(ts, year): Timestamp setMonth(ts, month): TimestampsetDay(ts, day): Timestamp setHours(ts, hours): TimestampsetMinutes(ts, minutes): Timestamp setSeconds(ts, seconds): Timestampdiff(a, b, unit): number diffInDays(a, b): numberdiffInHours(a, b): number diffInMinutes(a, b): numberdiffInSeconds(a, b): number diffInMonths(a, b): numberdiffInYears(a, b): numberRounding
Section titled “Rounding”roundTo(ts, unit): Timestamp ceilTo(ts, unit): TimestampfloorTo(ts, unit): Timestamp snapTo(ts, interval, unit): TimestampDuration
Section titled “Duration”duration(amount, unit): DurationaddDuration(ts, d): Timestamp subtractDuration(ts, d): TimestamptoDuration(ms): Duration durationToUnit(d, unit): numberscaleDuration(d, factor): Duration negateDuration(d): DurationRanges & intervals
Section titled “Ranges & intervals”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): booleanintersection(a, b): [Timestamp, Timestamp] | nullunion(a, b): [Timestamp, Timestamp] | nullgap(a, b): [Timestamp, Timestamp] | nullmergeIntervals(intervals): [Timestamp, Timestamp][]Business days
Section titled “Business days”isBusinessDay(ts): booleanaddBusinessDays(ts, days): TimestampsubtractBusinessDays(ts, days): TimestampbusinessDaysBetween(a, b): numbernextBusinessDay(ts): Timestamp prevBusinessDay(ts): TimestampaddBusinessDaysWithHolidays(ts, days, holidays): TimestampFormatting & parsing
Section titled “Formatting & parsing”format(ts, pattern): string formatter(pattern): (ts) => stringparse(input, pattern): Timestamp parser(pattern): (input) => TimestamptryParse(input, pattern): Timestamp | nulltryParser(pattern): (input) => Timestamp | nullparseISO(input): TimestampTimezone (Tz namespace)
Section titled “Timezone (Tz namespace)”Tz.utcToLocal(ts, zone): TimestampTz.localToUTC(localMs, zone): TimestampTz.startOf(ts, unit, zone): TimestampTz.endOf(ts, unit, zone): TimestampTz.add(ts, amount, unit, zone): TimestampTz.format(ts, pattern, zone): stringTz.diff(a, b, unit, zone): numberTz.isSameDay(a, b, zone): booleanTz.isWeekend(ts, zone): booleanTz.isToday(ts, zone): booleanTz.getOffsetMinutes(ts, zone): numberTz.getOffsetString(ts, zone): string