date
bun add @stopcock/dateDates are branded numbers (Unix ms timestamps). No Date objects get allocated
anywhere, it’s all integer arithmetic. Data-oriented operations expose paired
data-first and data-last overloads where noted below.
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.- Dual operations work both ways:
add(7, 'day')returns(ts) => Timestampforpipe, oradd(ts, 7, 'day')runs 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 } from '@stopcock/fp'import * as A from '@stopcock/fp/array'import { daysIn, getWeekday, format, isToday, isWeekend } from '@stopcock/date'
const calendarMonth = (year: number, month: number) => pipe( daysIn(year, month), A.map((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'), Tz.add(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”The timestamp-taking predicates and clamp support both forms. compare,
min, max, isWeekend, isWeekday, isToday, isPast, isFuture, and
isValid are direct functions.
compare(a, b): numbermin(dates: Timestamp[]): Timestamp max(dates: Timestamp[]): Timestampclamp(ts, lo, hi): Timestamp clamp(lo, hi): (ts) => TimestampisBefore(ts, other): boolean isBefore(other): (ts) => booleanisAfter(ts, other): boolean isAfter(other): (ts) => booleanisEqual(ts, other): boolean isEqual(other): (ts) => booleanisSameDay(ts, other): boolean isSameDay(other): (ts) => booleanisSameMonth(ts, other): boolean isSameMonth(other): (ts) => booleanisSameYear(ts, other): boolean isSameYear(other): (ts) => booleanisBetween(ts, start, end): boolean isBetween(start, end): (ts) => booleanisWeekend(ts): boolean isWeekday(ts): booleanisToday(ts): boolean isPast(ts): booleanisFuture(ts): boolean isValid(ts): booleanArithmetic
Section titled “Arithmetic”add(ts, amount, unit): Timestamp add(amount, unit): (ts) => Timestampsubtract(ts, amount, unit): Timestamp subtract(amount, unit): (ts) => TimestampstartOf(ts, unit): Timestamp startOf(unit): (ts) => TimestampendOf(ts, unit): Timestamp endOf(unit): (ts) => TimestampsetYear(ts, year): Timestamp setYear(year): (ts) => TimestampsetMonth(ts, month): Timestamp setMonth(month): (ts) => TimestampsetDay(ts, day): Timestamp setDay(day): (ts) => TimestampsetHours(ts, hours): Timestamp setHours(hours): (ts) => TimestampsetMinutes(ts, minutes): Timestamp setMinutes(minutes): (ts) => TimestampsetSeconds(ts, seconds): Timestamp setSeconds(seconds): (ts) => Timestampdiff(a, b, unit): number diff(b, unit): (a) => numberdiffInDays(a, b): number diffInDays(b): (a) => numberdiffInHours(a, b): number diffInHours(b): (a) => numberdiffInMinutes(a, b): number diffInMinutes(b): (a) => numberdiffInSeconds(a, b): number diffInSeconds(b): (a) => numberdiffInMonths(a, b): number diffInMonths(b): (a) => numberdiffInYears(a, b): number diffInYears(b): (a) => numberRounding
Section titled “Rounding”roundTo(ts, unit): Timestamp roundTo(unit): (ts) => TimestampceilTo(ts, unit): Timestamp ceilTo(unit): (ts) => TimestampfloorTo(ts, unit): Timestamp floorTo(unit): (ts) => TimestampsnapTo(ts, interval, unit): Timestamp snapTo(interval, unit): (ts) => TimestampDuration
Section titled “Duration”duration(amount, unit): DurationaddDuration(ts, d): Timestamp addDuration(d): (ts) => TimestampsubtractDuration(ts, d): Timestamp subtractDuration(d): (ts) => TimestampdurationToUnit(d, unit): number durationToUnit(unit): (d) => numbertoDuration(a, b): Duration scaleDuration(d, factor): DurationnegateDuration(d): DurationtoDuration, scaleDuration, and negateDuration aren’t curried; they’re
plain functions.
Ranges & intervals
Section titled “Ranges & intervals”range(start, end, step, unit): Timestamp[] range(end, step, unit): (start) => Timestamp[]rangeBy(start, end, stepFn): Timestamp[] rangeBy(end, stepFn): (start) => Timestamp[]daysIn(year, month): Timestamp[] weekdaysIn(year, month): Timestamp[]sequence(start, count, step, unit): Timestamp[]sequence(count, step, unit): (start) => Timestamp[]overlaps(a1, a2, b1, b2): boolean contains(start, end, point): booleanintersection(a1, a2, b1, b2): [Timestamp, Timestamp] | nullunion(a1, a2, b1, b2): [Timestamp, Timestamp] | nullgap(a1, a2, b1, b2): [Timestamp, Timestamp] | nullmergeIntervals(intervals): [Timestamp, Timestamp][]daysIn and weekdaysIn take a year and a month, not a range. overlaps,
contains, intersection, union, gap, and mergeIntervals aren’t
curried.
Business days
Section titled “Business days”isBusinessDay(ts): booleanaddBusinessDays(ts, amount): Timestamp addBusinessDays(amount): (ts) => TimestampsubtractBusinessDays(ts, amount): Timestamp subtractBusinessDays(amount): (ts) => TimestampbusinessDaysBetween(a, b): number businessDaysBetween(b): (a) => numbernextBusinessDay(ts): Timestamp prevBusinessDay(ts): TimestampaddBusinessDaysWithHolidays(ts, amount, holidays): TimestampaddBusinessDaysWithHolidays(amount, holidays): (ts) => TimestampFormatting & parsing
Section titled “Formatting & parsing”format(ts, pattern): string format(pattern): (ts) => stringformatter(pattern): (ts) => stringparse(input, pattern): Timestamp parse(pattern): (input) => Timestampparser(pattern): (input) => TimestamptryParse(input, pattern): Timestamp | null tryParse(pattern): (input) => Timestamp | nulltryParser(pattern): (input) => Timestamp | nullparseISO(input): TimestampTimezone (Tz namespace)
Section titled “Timezone (Tz namespace)”Tz.getYear(ts, zone): number Tz.getYear(zone): (ts) => numberTz.getMonth(ts, zone): number Tz.getMonth(zone): (ts) => numberTz.getDay(ts, zone): number Tz.getDay(zone): (ts) => numberTz.getHours(ts, zone): number Tz.getHours(zone): (ts) => numberTz.getMinutes(ts, zone): number Tz.getMinutes(zone): (ts) => numberTz.getSeconds(ts, zone): number Tz.getSeconds(zone): (ts) => numberTz.isSameDay(a, b, zone): boolean Tz.isSameDay(b, zone): (a) => booleanTz.startOf(ts, unit, zone, disambiguation?): TimestampTz.startOf(unit, zone): (ts) => TimestampTz.endOf(ts, unit, zone, disambiguation?): TimestampTz.endOf(unit, zone): (ts) => TimestampTz.add(ts, amount, unit, zone, disambiguation?): TimestampTz.add(amount, unit, zone): (ts) => TimestampTz.subtract(ts, amount, unit, zone, disambiguation?): TimestampTz.subtract(amount, unit, zone): (ts) => TimestampTz.format(ts, pattern, zone): string Tz.format(pattern, zone): (ts) => stringTz.diff(a, b, unit, zone): number Tz.diff(b, unit, zone): (a) => number
Tz.utcToLocal(ts, zone): TimestampTz.localToUTC(localMs, zone): TimestampTz.isWeekend(ts, zone): booleanTz.isToday(ts, zone): booleanTz.getOffsetMinutes(ts, zone): numberTz.getOffsetString(ts, zone): string