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.
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.
| Adapter | Import | Description | Peer dependency |
|---|---|---|---|
NativeAdapter | @gentleduck/calendar | Zero-dep adapter for the built-in Date object | None |
DateFnsAdapter | @gentleduck/calendar/adapter | Adapter for date-fns | date-fns |
DayjsAdapter | @gentleduck/calendar/adapter | Adapter for dayjs | dayjs |
LuxonAdapter | @gentleduck/calendar/adapter | Adapter for Luxon | luxon |
PersianAdapter | @gentleduck/calendar/adapter | Persian / Jalali calendar | None |
IslamicAdapter | @gentleduck/calendar/adapter | Islamic / Hijri calendar | None |
HebrewAdapter | @gentleduck/calendar/adapter | Hebrew calendar | None |
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:
| Method | Signature | Description |
|---|---|---|
today | () => TDate | Return today at midnight |
create | (y, m, d) => TDate | Create a date from year, month (0-indexed), day |
isValid | (d) => boolean | Check if a date is valid |
isSameDay | (a, b) => boolean | Compare two dates ignoring time |
isSameMonth | (a, b) => boolean | Compare month and year |
isBefore | (a, b) => boolean | a is before b |
isAfter | (a, b) => boolean | a is after b |
startOfMonth | (d) => TDate | First day of the month |
endOfMonth | (d) => TDate | Last day of the month |
startOfWeek | (d, weekStart) => TDate | First day of the week |
addDays | (d, n) => TDate | Add n days |
addMonths | (d, n) => TDate | Add n months |
addYears | (d, n) => TDate | Add n years |
getYear | (d) => number | Extract year |
getMonth | (d) => number | Extract month (0-indexed) |
getDate | (d) => number | Extract day of month |
getDayOfWeek | (d) => Adapter.WeekStartDay | Day of week (0=Sun) |
toDate | (d) => Date | Convert to native Date |
fromDate | (d: Date) => TDate | Convert from native Date |
format | (d, opts, locale?) => string | Format for display |
getHours | (d) => number | Extract hours |
getMinutes | (d) => number | Extract minutes |
getSeconds | (d) => number | Extract seconds |
setTime | (d, h, m, s?) => TDate | Set time on a date |
getMonthsInYear? | (d) => number | Optional. Month count for the year (defaults to 12). Hebrew leap years return 13. |
All date operations must be immutable - never mutate the input date. Return a new instance instead.