Skip to main content

Date Adapters

Understand the DateAdapter pattern and how to plug in any date library.

The adapter pattern

The Adapter.IDateAdapter<TDate> interface abstracts all date operations. The engine never calls new Date() or date.getMonth() directly - everything goes through the adapter.

This means you can swap Date for dayjs, date-fns, Luxon, or a non-Gregorian calendar without changing any calendar logic.

Loading diagram...


Built-in: NativeAdapter

Zero dependencies. Uses Date + Intl.DateTimeFormat.

import { NativeAdapter } from '@gentleduck/calendar'
 
const adapter = new NativeAdapter()
import { NativeAdapter } from '@gentleduck/calendar'
 
const adapter = new NativeAdapter()

The NativeAdapter implements every adapter method with Date plus Intl.DateTimeFormat for locale-aware formatting. No polyfills required.


Shipped adapters

The package includes 7 ready-to-use adapters. Adapters marked with a peer dependency require you to install the underlying date library yourself.

AdapterImportDescriptionPeer dependency
NativeAdapter@gentleduck/calendarZero-dep adapter for the built-in Date objectNone
DateFnsAdapter@gentleduck/calendar/adapterAdapter for date-fnsdate-fns
DayjsAdapter@gentleduck/calendar/adapterAdapter for dayjsdayjs
LuxonAdapter@gentleduck/calendar/adapterAdapter for Luxonluxon
PersianAdapter@gentleduck/calendar/adapterPersian / Jalali calendarNone
IslamicAdapter@gentleduck/calendar/adapterIslamic / Hijri calendarNone
HebrewAdapter@gentleduck/calendar/adapterHebrew calendarNone

Writing a custom adapter

Implement the Adapter.IDateAdapter<TDate> interface for your preferred date library.

dayjs example

import type { Adapter } from '@gentleduck/calendar'
import dayjs, { type Dayjs } from 'dayjs'
 
const dayjsAdapter: Adapter.IDateAdapter<Dayjs> = {
  today: () => dayjs().startOf('day'),
  create: (y, m, d) => dayjs().year(y).month(m).date(d).startOf('day'),
  isValid: (d) => d.isValid(),
  isSameDay: (a, b) => a.isSame(b, 'day'),
  isSameMonth: (a, b) => a.isSame(b, 'month'),
  isBefore: (a, b) => a.isBefore(b),
  isAfter: (a, b) => a.isAfter(b),
  startOfMonth: (d) => d.startOf('month'),
  endOfMonth: (d) => d.endOf('month'),
  startOfWeek: (d, weekStart) => {
    const diff = (d.day() - weekStart + 7) % 7
    return d.subtract(diff, 'day')
  },
  addDays: (d, n) => d.add(n, 'day'),
  addMonths: (d, n) => d.add(n, 'month'),
  addYears: (d, n) => d.add(n, 'year'),
  getYear: (d) => d.year(),
  getMonth: (d) => d.month(),
  getDate: (d) => d.date(),
  getDayOfWeek: (d) => d.day() as any,
  toDate: (d) => d.toDate(),
  fromDate: (d) => dayjs(d).startOf('day'),
  format: (d, opts, locale) => new Intl.DateTimeFormat(locale, opts).format(d.toDate()),
  getHours: (d) => d.hour(),
  getMinutes: (d) => d.minute(),
  getSeconds: (d) => d.second(),
  setTime: (d, h, m, s) => d.hour(h).minute(m).second(s ?? 0),
}
import type { Adapter } from '@gentleduck/calendar'
import dayjs, { type Dayjs } from 'dayjs'
 
const dayjsAdapter: Adapter.IDateAdapter<Dayjs> = {
  today: () => dayjs().startOf('day'),
  create: (y, m, d) => dayjs().year(y).month(m).date(d).startOf('day'),
  isValid: (d) => d.isValid(),
  isSameDay: (a, b) => a.isSame(b, 'day'),
  isSameMonth: (a, b) => a.isSame(b, 'month'),
  isBefore: (a, b) => a.isBefore(b),
  isAfter: (a, b) => a.isAfter(b),
  startOfMonth: (d) => d.startOf('month'),
  endOfMonth: (d) => d.endOf('month'),
  startOfWeek: (d, weekStart) => {
    const diff = (d.day() - weekStart + 7) % 7
    return d.subtract(diff, 'day')
  },
  addDays: (d, n) => d.add(n, 'day'),
  addMonths: (d, n) => d.add(n, 'month'),
  addYears: (d, n) => d.add(n, 'year'),
  getYear: (d) => d.year(),
  getMonth: (d) => d.month(),
  getDate: (d) => d.date(),
  getDayOfWeek: (d) => d.day() as any,
  toDate: (d) => d.toDate(),
  fromDate: (d) => dayjs(d).startOf('day'),
  format: (d, opts, locale) => new Intl.DateTimeFormat(locale, opts).format(d.toDate()),
  getHours: (d) => d.hour(),
  getMinutes: (d) => d.minute(),
  getSeconds: (d) => d.second(),
  setTime: (d, h, m, s) => d.hour(h).minute(m).second(s ?? 0),
}

Adapter interface

Every adapter must implement these methods:

MethodSignatureDescription
today() => TDateReturn today at midnight
create(y, m, d) => TDateCreate a date from year, month (0-indexed), day
isValid(d) => booleanCheck if a date is valid
isSameDay(a, b) => booleanCompare two dates ignoring time
isSameMonth(a, b) => booleanCompare month and year
isBefore(a, b) => booleana is before b
isAfter(a, b) => booleana is after b
startOfMonth(d) => TDateFirst day of the month
endOfMonth(d) => TDateLast day of the month
startOfWeek(d, weekStart) => TDateFirst day of the week
addDays(d, n) => TDateAdd n days
addMonths(d, n) => TDateAdd n months
addYears(d, n) => TDateAdd n years
getYear(d) => numberExtract year
getMonth(d) => numberExtract month (0-indexed)
getDate(d) => numberExtract day of month
getDayOfWeek(d) => Adapter.WeekStartDayDay of week (0=Sun)
toDate(d) => DateConvert to native Date
fromDate(d: Date) => TDateConvert from native Date
format(d, opts, locale?) => stringFormat for display
getHours(d) => numberExtract hours
getMinutes(d) => numberExtract minutes
getSeconds(d) => numberExtract seconds
setTime(d, h, m, s?) => TDateSet time on a date
getMonthsInYear?(d) => numberOptional. Month count for the year (defaults to 12). Hebrew leap years return 13.