Timestamp treats Gregorian dates as the default public contract. Calendar-aware helpers accept a CalendarSystem argument and default to gregorianCalendar, so existing calls keep their Gregorian behavior while adapter-based code can use native calendar fields.
The calendar-system work starts underneath that contract. Core exposes a CalendarSystem adapter shape and a built-in gregorianCalendar adapter so future calendar packages can plug into the same date math model instead of creating separate, incompatible helper APIs. Adapter-produced timestamps can use the optional calendarId field, but the current Gregorian core helpers leave it unset so existing object output remains stable.
Recommended package shape
Keep @timestamp-js/core small and predictable:
- Core owns shared types, immutable Timestamp objects, comparison/range primitives, time helpers, and the default Gregorian adapter.
- Optional calendar packages own calendar-specific math and parsing. Timestamp currently publishes
@timestamp-js/calendar-islamic,@timestamp-js/calendar-saka,@timestamp-js/calendar-hebrew, and@timestamp-js/calendar-persian; future adapters can add other systems without changing the core package. - The Gregorian adapter remains available from core first. A separate
@timestamp-js/calendar-gregorianpackage can be added later if external adapter packages need a reusable reference implementation.
This keeps existing users on one package while making advanced calendar support opt-in.
Current adapters
Timestamp currently ships the default Gregorian adapter from @timestamp-js/core and optional adapter packages for Islamic civil, Indian National/Saka, Hebrew, and Persian dates. Optional adapters use the same CalendarSystem contract, which keeps the conversion surface small while letting each package own its calendar rules.
- Islamic Civil (Hijri) documents the deterministic tabular Hijri calendar, including Arabic labels, RTL presentation, and native week/month ranges.
- Saka documents the Indian National Calendar adapter, including locale-aware labels and native week/month ranges.
- Hebrew documents the arithmetic Hebrew calendar adapter, including civil/CLDR month numbering, RTL presentation, and native week/month ranges.
- Persian documents the Persian/Jalali calendar adapter, including civil month numbering, RTL presentation, and native week/month ranges.
Additional calendar pages should be added under this section as new adapters prove their behavior against real calendar, picker, scheduler, and data workflow usage.
If your application needs a calendar Timestamp does not publish yet, start with Create a Calendar Adapter. Custom adapters use the same CalendarSystem contract as the built-in Gregorian adapter and optional adapter packages.
Adapter contract
A calendar adapter needs to answer questions that are calendar-specific:
import type { CalendarSystem } from '@timestamp-js/core'
export const const myCalendar: CalendarSystemmyCalendar: CalendarSystem = {
CalendarSystem.id: CalendarIdPackage-facing calendar id.id: 'my-calendar',
CalendarSystem.label: stringHuman-readable calendar name.label: 'My Calendar',
CalendarSystem.defaultLocale?: string | undefinedRecommended BCP 47 locale for presentation when the caller does not provide one.defaultLocale: 'en-US',
CalendarSystem.defaultDirection?: CalendarDirection | undefinedRecommended text direction for presentation when the caller does not provide one.defaultDirection: 'ltr',
CalendarSystem.defaultWeekdays?: readonly number[] | undefinedRecommended visible weekday order when the caller does not provide one.
Weekdays use JavaScript numbering, where Sunday is `0` and Saturday is `6`.defaultWeekdays: [0, 1, 2, 3, 4, 5, 6],
CalendarSystem.monthsInYear(year: number): numberNumber of months in a calendar year.monthsInYear(year: numberyear) {
return 12
},
CalendarSystem.isLeapYear(year: number): booleanTrue when the calendar year has a leap day or leap month.isLeapYear(year: numberyear) {
return false
},
CalendarSystem.daysInMonth(year: number, month: number): numberNumber of days in the specified calendar month.daysInMonth(year: numberyear, month: numbermonth) {
return 30
},
CalendarSystem.toEpochDay(date: CalendarDateParts): numberConverts a calendar date into a stable serial day.
For Gregorian this is the number of UTC days since 1970-01-01. Other
calendar adapters should return the serial day for the equivalent civil
instant so comparisons and ranges can stay calendar-agnostic.toEpochDay(date: CalendarDatePartsdate) {
// Convert calendar fields to a stable serial day.
return 0
},
CalendarSystem.fromEpochDay(epochDay: number): CalendarDatePartsConverts a stable serial day into calendar date fields.fromEpochDay(epochDay: numberepochDay) {
// Convert the stable serial day back to calendar fields.
return { CalendarDateParts.year: numberCalendar year.year: 1, CalendarDateParts.month: numberCalendar month number. The first month is `1`.month: 1, CalendarDateParts.day: numberDay of the month. The first day is `1`.day: 1 }
},
CalendarSystem.addDays(date: CalendarDateParts, amount: number): CalendarDatePartsMoves a calendar date by a whole number of days.addDays(date: CalendarDatePartsdate, amount: numberamount) {
return this.CalendarSystem.fromEpochDay(epochDay: number): CalendarDatePartsConverts a stable serial day into calendar date fields.fromEpochDay(this.CalendarSystem.toEpochDay(date: CalendarDateParts): numberConverts a calendar date into a stable serial day.
For Gregorian this is the number of UTC days since 1970-01-01. Other
calendar adapters should return the serial day for the equivalent civil
instant so comparisons and ranges can stay calendar-agnostic.toEpochDay(date: CalendarDatePartsdate) + amount: numberamount)
},
CalendarSystem.nextDay(date: CalendarDateParts): CalendarDatePartsReturns the next calendar date.nextDay(date: CalendarDatePartsdate) {
return this.CalendarSystem.addDays(date: CalendarDateParts, amount: number): CalendarDatePartsMoves a calendar date by a whole number of days.addDays(date: CalendarDatePartsdate, 1)
},
CalendarSystem.prevDay(date: CalendarDateParts): CalendarDatePartsReturns the previous calendar date.prevDay(date: CalendarDatePartsdate) {
return this.CalendarSystem.addDays(date: CalendarDateParts, amount: number): CalendarDatePartsMoves a calendar date by a whole number of days.addDays(date: CalendarDatePartsdate, -1)
},
CalendarSystem.getDayOfYear(date: CalendarDateParts): numberReturns day-of-year for a calendar date.getDayOfYear(date: CalendarDatePartsdate) {
return 1
},
CalendarSystem.getWeekday(date: CalendarDateParts): numberReturns weekday where Sunday is `0` and Saturday is `6`.getWeekday(date: CalendarDatePartsdate) {
return 0
},
}
myCalendarThe important part is epochDay. Calendar year/month/day fields are not directly comparable across calendar systems. An adapter maps its fields to a stable serial day so ranges, min/max, disabled dates, selected dates, and list generation can stay calendar-agnostic internally while public component APIs remain adapter-native.
Presentation fields such as defaultLocale, defaultDirection, and defaultWeekdays are optional recommendations. Components can use them when the app does not pass explicit display props, but callers can still override them for another language, direction, or work-week layout.
Use getCalendarLocale(), getCalendarDirection(), isCalendarRTL(), and getCalendarWeekdays() when component or application code needs to read those defaults without reaching into adapter internals. Each helper falls back to Gregorian defaults if an adapter omits a presentation field.
Calendar-aware core helpers
Core keeps the original helper names stable and makes calendar math adapter-capable where it makes sense. Helpers such as today(), parseDate(), nextDay(), daysInMonth(), getStartOfWeek(), createDayList(), updateFormatted(), updateRelative(), and addToDate() default to Gregorian but accept a CalendarSystem when the input/output date fields should be native to another calendar.
The explicitly named calendar helpers remain useful for component and adapter work because their purpose is unambiguous and they return timestamp-shaped objects tagged with the adapter id:
import {
function createCalendarDayList(start: Timestamp, end: Timestamp, now: Timestamp, calendar?: CalendarSystem, options?: CalendarDayListOptions): Timestamp[]Creates an inclusive list of calendar adapter days between start and end.createCalendarDayList,
function createCalendarMonthView(timestamp: Timestamp, now: Timestamp, calendar?: CalendarSystem, options?: CalendarMonthViewOptions): CalendarMonthViewCreates native calendar month view state for component rendering.
The returned `days` use adapter-native `YYYY-MM-DD` values, while each
state's identity also includes Gregorian interop metadata.createCalendarMonthView,
function formatCalendarDateLabel(timestamp: Timestamp, calendar?: CalendarSystem, options?: Intl.DateTimeFormatOptions, locale?: string): stringFormats a calendar timestamp date with Intl using adapter defaults.
The timestamp's adapter date fields are converted through the calendar
before Intl receives the equivalent Gregorian Date. Use this for native
calendar labels in UI examples and application code.formatCalendarDateLabel,
function getCalendarDateIdentity(timestamp: Timestamp, calendar?: CalendarSystem): CalendarDateIdentityCreates stable native and Gregorian identities for a calendar timestamp.
Use this when component APIs need to expose adapter-native values while also
preserving a canonical Gregorian key for storage, comparisons, or interop
with existing data.getCalendarDateIdentity,
function getCalendarEndOfMonth(timestamp: Timestamp, calendar?: CalendarSystem): TimestampReturns the end of the calendar month for a timestamp.getCalendarEndOfMonth,
function getCalendarMonthName(calendar: CalendarSystem | undefined, month: number, type?: string, locale?: string, year?: number): stringFormats one month number using calendar adapter defaults.
Month numbers are one-based for calendar adapters.getCalendarMonthName,
function getCalendarWeekdayNames(calendar?: CalendarSystem, type?: string, locale?: string, weekdays?: readonly number[]): string[]Retrieves localized weekday names using calendar adapter defaults.getCalendarWeekdayNames,
function getCalendarSelectionState(timestamp: Timestamp, options?: CalendarSelectionOptions, calendar?: CalendarSystem): CalendarSelectionStateResolves selected-date and selected-range state for a calendar timestamp.
The default Gregorian calendar preserves existing Timestamp behavior. When
an alternate calendar is supplied, selected date strings are parsed as native
adapter dates.getCalendarSelectionState,
function parseCalendarTimestamp(input: string, calendar?: CalendarSystem, now?: Timestamp | null): Timestamp | nullParses a date or date-time string as calendar adapter fields.
Unlike parseTimestamp(), this helper validates the date against the supplied
calendar and derives weekday/day-of-year values through the adapter.parseCalendarTimestamp,
function updateCalendarDisabled(timestamp: Timestamp, disabledBefore?: string, disabledAfter?: string, disabledWeekdays?: number[], disabledDays?: DisabledDays, calendar?: CalendarSystem): TimestampUpdates a timestamp with disabled state using calendar-native date strings.
The default Gregorian calendar preserves existing Timestamp behavior. When
an alternate calendar is supplied, `disabledBefore`, `disabledAfter`, and
`disabledDays` are parsed as native dates for that adapter.updateCalendarDisabled,
} from '@timestamp-js/core'
import { const islamicCivilCalendar: CalendarSystemDeterministic tabular Islamic civil calendar.
This adapter intentionally models the arithmetic/civil Hijri calendar. It
does not model observational calendars or Umm al-Qura adjustments.islamicCivilCalendar } from '@timestamp-js/calendar-islamic'
const const visible: Timestampvisible = function parseCalendarTimestamp(input: string, calendar?: CalendarSystem, now?: Timestamp | null): Timestamp | nullParses a date or date-time string as calendar adapter fields.
Unlike parseTimestamp(), this helper validates the date against the supplied
calendar and derives weekday/day-of-year values through the adapter.parseCalendarTimestamp('1445-09-01', const islamicCivilCalendar: CalendarSystemDeterministic tabular Islamic civil calendar.
This adapter intentionally models the arithmetic/civil Hijri calendar. It
does not model observational calendars or Umm al-Qura adjustments.islamicCivilCalendar)!
const const start: Timestampstart = const visible: Timestampvisible
const const end: Timestampend = function getCalendarEndOfMonth(timestamp: Timestamp, calendar?: CalendarSystem): TimestampReturns the end of the calendar month for a timestamp.getCalendarEndOfMonth(const visible: Timestampvisible, const islamicCivilCalendar: CalendarSystemDeterministic tabular Islamic civil calendar.
This adapter intentionally models the arithmetic/civil Hijri calendar. It
does not model observational calendars or Umm al-Qura adjustments.islamicCivilCalendar)
const const days: Timestamp[]days = function createCalendarDayList(start: Timestamp, end: Timestamp, now: Timestamp, calendar?: CalendarSystem, options?: CalendarDayListOptions): Timestamp[]Creates an inclusive list of calendar adapter days between start and end.createCalendarDayList(const start: Timestampstart, const end: Timestampend, const visible: Timestampvisible, const islamicCivilCalendar: CalendarSystemDeterministic tabular Islamic civil calendar.
This adapter intentionally models the arithmetic/civil Hijri calendar. It
does not model observational calendars or Umm al-Qura adjustments.islamicCivilCalendar)
const const monthLabel: stringmonthLabel = function getCalendarMonthName(calendar: CalendarSystem | undefined, month: number, type?: string, locale?: string, year?: number): stringFormats one month number using calendar adapter defaults.
Month numbers are one-based for calendar adapters.getCalendarMonthName(
const islamicCivilCalendar: CalendarSystemDeterministic tabular Islamic civil calendar.
This adapter intentionally models the arithmetic/civil Hijri calendar. It
does not model observational calendars or Umm al-Qura adjustments.islamicCivilCalendar,
const visible: Timestampvisible.Timestamp.month: numberCalendar month number, where the first month is `1`. Core parser helpers
use Gregorian month numbers.month,
'long',
'en-US',
const visible: Timestampvisible.Timestamp.year: numberCalendar year. Core parser helpers use Gregorian years.year,
)
const const weekdayLabels: string[]weekdayLabels = function getCalendarWeekdayNames(calendar?: CalendarSystem, type?: string, locale?: string, weekdays?: readonly number[]): string[]Retrieves localized weekday names using calendar adapter defaults.getCalendarWeekdayNames(const islamicCivilCalendar: CalendarSystemDeterministic tabular Islamic civil calendar.
This adapter intentionally models the arithmetic/civil Hijri calendar. It
does not model observational calendars or Umm al-Qura adjustments.islamicCivilCalendar, 'short', 'en-US')
const const visibleLabel: stringvisibleLabel = function formatCalendarDateLabel(timestamp: Timestamp, calendar?: CalendarSystem, options?: Intl.DateTimeFormatOptions, locale?: string): stringFormats a calendar timestamp date with Intl using adapter defaults.
The timestamp's adapter date fields are converted through the calendar
before Intl receives the equivalent Gregorian Date. Use this for native
calendar labels in UI examples and application code.formatCalendarDateLabel(
const visible: Timestampvisible,
const islamicCivilCalendar: CalendarSystemDeterministic tabular Islamic civil calendar.
This adapter intentionally models the arithmetic/civil Hijri calendar. It
does not model observational calendars or Umm al-Qura adjustments.islamicCivilCalendar,
{
Intl.DateTimeFormatOptions.month?: "long" | "short" | "numeric" | "2-digit" | "narrow" | undefinedmonth: 'long',
Intl.DateTimeFormatOptions.day?: "numeric" | "2-digit" | undefinedday: 'numeric',
Intl.DateTimeFormatOptions.year?: "numeric" | "2-digit" | undefinedyear: 'numeric',
},
'en-US',
)
const const identity: CalendarDateIdentityidentity = function getCalendarDateIdentity(timestamp: Timestamp, calendar?: CalendarSystem): CalendarDateIdentityCreates stable native and Gregorian identities for a calendar timestamp.
Use this when component APIs need to expose adapter-native values while also
preserving a canonical Gregorian key for storage, comparisons, or interop
with existing data.getCalendarDateIdentity(const days: Timestamp[]days[0], const islamicCivilCalendar: CalendarSystemDeterministic tabular Islamic civil calendar.
This adapter intentionally models the arithmetic/civil Hijri calendar. It
does not model observational calendars or Umm al-Qura adjustments.islamicCivilCalendar)
const const disabled: Timestampdisabled = function updateCalendarDisabled(timestamp: Timestamp, disabledBefore?: string, disabledAfter?: string, disabledWeekdays?: number[], disabledDays?: DisabledDays, calendar?: CalendarSystem): TimestampUpdates a timestamp with disabled state using calendar-native date strings.
The default Gregorian calendar preserves existing Timestamp behavior. When
an alternate calendar is supplied, `disabledBefore`, `disabledAfter`, and
`disabledDays` are parsed as native dates for that adapter.updateCalendarDisabled(
const days: Timestamp[]days[0],
var undefinedundefined,
var undefinedundefined,
[],
['1445-09-01'],
const islamicCivilCalendar: CalendarSystemDeterministic tabular Islamic civil calendar.
This adapter intentionally models the arithmetic/civil Hijri calendar. It
does not model observational calendars or Umm al-Qura adjustments.islamicCivilCalendar,
)
const const selection: CalendarSelectionStateselection = function getCalendarSelectionState(timestamp: Timestamp, options?: CalendarSelectionOptions, calendar?: CalendarSystem): CalendarSelectionStateResolves selected-date and selected-range state for a calendar timestamp.
The default Gregorian calendar preserves existing Timestamp behavior. When
an alternate calendar is supplied, selected date strings are parsed as native
adapter dates.getCalendarSelectionState(
const days: Timestamp[]days[0],
{ CalendarSelectionOptions.selectedStartEndDates?: string[] | undefinedTwo adapter-native date strings marking an inclusive selected range.selectedStartEndDates: ['1445-09-01', '1445-09-05'] },
const islamicCivilCalendar: CalendarSystemDeterministic tabular Islamic civil calendar.
This adapter intentionally models the arithmetic/civil Hijri calendar. It
does not model observational calendars or Umm al-Qura adjustments.islamicCivilCalendar,
)
const const monthView: CalendarMonthViewmonthView = function createCalendarMonthView(timestamp: Timestamp, now: Timestamp, calendar?: CalendarSystem, options?: CalendarMonthViewOptions): CalendarMonthViewCreates native calendar month view state for component rendering.
The returned `days` use adapter-native `YYYY-MM-DD` values, while each
state's identity also includes Gregorian interop metadata.createCalendarMonthView(const visible: Timestampvisible, const visible: Timestampvisible, const islamicCivilCalendar: CalendarSystemDeterministic tabular Islamic civil calendar.
This adapter intentionally models the arithmetic/civil Hijri calendar. It
does not model observational calendars or Umm al-Qura adjustments.islamicCivilCalendar, {
CalendarSelectionOptions.selectedDates?: CalendarDateCollection | undefinedAdapter-native date strings to mark as selected.selectedDates: ['1445-09-03'],
CalendarDisabledOptions.disabledDays?: DisabledDays | undefinedSpecific dates or date ranges to mark disabled.disabledDays: ['1445-09-04'],
})
const days: Timestamp[]days[0].Timestamp.calendarId?: CalendarId | undefinedOptional calendar-system identifier for adapter-produced timestamps.
Core Gregorian helpers omit this field for backwards compatibility.calendarId // 'islamic-civil'
const days: Timestamp[]days[0].Timestamp.date: stringDate string in `YYYY-MM-DD` form when the timestamp has a day.date // '1445-09-01'
const monthLabel: stringmonthLabel // 'Ramadan'
const weekdayLabels: string[]weekdayLabels // ['Sat', 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri']
const visibleLabel: stringvisibleLabel // 'Ramadan 1, 1445 AH'
const identity: CalendarDateIdentityidentity.CalendarDateIdentity.nativeDate: stringNative calendar date string in `YYYY-MM-DD` form.nativeDate // '1445-09-01'
const identity: CalendarDateIdentityidentity.CalendarDateIdentity.gregorianDate: stringCanonical Gregorian date string for the same serial day.gregorianDate // '2024-03-11'
const identity: CalendarDateIdentityidentity.CalendarDateIdentity.epochDay: numberStable serial day shared across calendar systems.epochDay // stable cross-calendar sort/range key
const disabled: Timestampdisabled.Timestamp.disabled?: boolean | undefinedTrue when this timestamp represents a disabled date.disabled // true
const selection: CalendarSelectionStateselection.CalendarSelectionState.rangeFirst: booleanTrue when the date is the selected range start.rangeFirst // true
const monthView: CalendarMonthViewmonthView.CalendarMonthView.days: CalendarDateState[]Rendered adapter-native day states.days[0].CalendarDateState.timestamp: TimestampAdapter-native timestamp for the rendered date.timestamp.Timestamp.date: stringDate string in `YYYY-MM-DD` form when the timestamp has a day.date // adapter-native YYYY-MM-DD
const monthView: CalendarMonthViewmonthView.CalendarMonthView.days: CalendarDateState[]Rendered adapter-native day states.days[0].CalendarDateState.identity: CalendarDateIdentityStable native/Gregorian identity for the rendered date.identity.CalendarDateIdentity.gregorianDate: stringCanonical Gregorian date string for the same serial day.gregorianDate // Gregorian interop metadataThese helpers are the bridge for calendar-style views: adapters own calendar math, and core turns adapter dates into immutable Timestamp objects with weekday, doy, past, current, future, disabled state, selection/range state, outside-month state, and identity metadata. For component or application APIs that need to feel adapter-native, expose the native fields while keeping gregorianDate and epochDay available as deterministic interop keys.
Gregorian interop metadata
Adapter-native dates should be the main contract when a component receives a CalendarSystem. For example, a Saka calendar view should accept, emit, select, and disable Saka YYYY-MM-DD strings so application code does not have to translate every interaction.
Timestamp still keeps Gregorian interop metadata available because real applications often have to cross calendar boundaries:
- Existing apps may already store records keyed by Gregorian ISO dates.
- APIs, databases, exports, and analytics pipelines often expect Gregorian dates even when the UI is native to another calendar.
- Cross-calendar comparisons need a neutral serial key. Use
epochDayfor that job rather than a Gregorian display string. - Debugging and migration are easier when you can see which civil/Gregorian day an adapter-native date maps to.
- Integrations can adopt native calendar UI incrementally without rewriting every storage or reporting boundary at the same time.
Treat gregorianDate as interop metadata and epochDay as the durable comparison key. User-facing calendar state should stay native to the active adapter.
When a component accepts a calendar adapter, YYYY-MM-DD values at that component boundary should be treated as native to that adapter. For example, selected-dates, disabled-days, and model-value should all use Hijri strings when the Islamic civil adapter is active. Timestamp helpers keep comparisons stable by converting those native strings to epochDay internally.
First-class support vs. display-only support
Locale labels and calendar math are different problems. Intl.DateTimeFormat can format labels with calendar options in many runtimes, but display-only formatting is not enough for calendar views. A real calendar adapter must control month length, leap rules, navigation, weekday calculation, and range comparisons.
That means Islamic/Hijri, Hebrew, Indian National/Saka, Persian, and other non-Gregorian calendars should be treated as first-class adapter work, not only as translated Gregorian dates.
Temporal
Temporal is the right ecosystem direction to watch. Temporal.PlainDate carries calendar-system metadata, and Temporal is designed to replace the older JavaScript Date model. Timestamp is keeping its adapter contract small for now because Temporal is still not available in every target browser, and calendar-style views need deterministic adapter rules for range generation, list generation, and package-level tree shaking.
The current adapter shape should stay easy to bridge later: adapters already convert their year/month/day fields through a stable serial day, which is the same boundary adapter-aware consumers need when different calendar systems eventually share the same view code.
Integration status
The top-level helpers default to Gregorian for compatibility. When a CalendarSystem can be supplied, new component-facing code should pass it through consistently so parsing, navigation, range generation, disabled/selected state, relative flags, and formatted metadata all use the same calendar.
Use parseCalendarTimestamp() for adapter-native YYYY-MM-DD strings. Use helpers such as nextDay(timestamp, calendar), createDayList(..., calendar), and updateRelative(timestamp, now, time, calendar) when working with timestamp objects that already belong to a calendar. Use the *Calendar* helper names when that makes the call site clearer.
Islamic civil, Saka, Hebrew, and Persian are the current proving adapters. Future adapters with leap months or non-linear month naming will be the next design pressure point because those calendars need an explicit model representation before Timestamp can expose unambiguous native YYYY-MM-DD strings.