-
Notifications
You must be signed in to change notification settings - Fork 0
Architecture
The core goal was a function library that feels native to the JavaScript ecosystem. Anyone who has used date-fns will find the API familiar: one function per operation, plain Date inputs and outputs, no side effects, tree-shakeable.
Two constraints shaped the design:
- The Hijri calendar engine should be a separate, zero-dependency package (
hijri-core). This keepsdate-fns-hijrithin and lets other libraries use the engine directly. -
date-fns-hijrishould not couple users to any specific calendar algorithm. Theoptions.calendarparameter threads through tohijri-core's registry, so custom calendar engines registered there are usable without changes to this package.
Every exported function is a standalone pure function. There are no classes, no global state, no configuration objects that persist between calls.
This mirrors how date-fns works:
// date-fns style
import { addMonths, format } from 'date-fns';
// date-fns-hijri style
import { addHijriMonths, formatHijriDate } from 'date-fns-hijri';Tree-shaking works as expected. sideEffects: false in package.json tells bundlers they can eliminate any function that isn't imported.
hijri-core is a peer dependency. The consumer installs it explicitly:
pnpm add date-fns-hijri hijri-coreThis pattern has two benefits:
- The consumer controls which version of
hijri-corethey use. If a critical fix ships inhijri-core, they can upgrade without waiting for a newdate-fns-hijrirelease. - Applications that use multiple Hijri-aware packages (e.g., both
date-fns-hijriand a separate formatter) share a singlehijri-coreinstance. The engine's calendar registry is global within that instance, so a calendar registered once is available everywhere.
The devDependencies includes hijri-core: file:../hijri-core for local development and CI. Published packages pick up the peer from the consumer's node_modules.
formatHijriDate uses a single regular expression to locate all tokens in one pass:
/iYYYY|iYY|iMMMM|iMMM|iMM|iM|iDD|iD|iEEEE|iEEE|iE|ioooo|iooo/g
Token ordering within the pattern matters. The engine uses JavaScript's String.replace with a global regex, which matches the first alternative at each position. Longer tokens appear before their prefixes:
-
iYYYYbeforeiYY(preventsiYYYYmatching asiYY+YY) -
iMMMMbeforeiMMMbeforeiMMbeforeiM -
iEEEEbeforeiEEEbeforeiE -
ioooobeforeiooo
The iE token uses hwNumeric[date.getDay()] where hwNumeric = [1, 2, 3, 4, 5, 6, 7]. Sunday maps to 1, Saturday to 7. This matches the ISO weekday convention in the Hijri context (Sunday = first day of the Islamic week).
Non-token text passes through unchanged because String.replace with a regex only replaces matched substrings. This means literal separators (-, /, , .) and arbitrary text work without escaping.
The two types of functions handle out-of-range differently:
Query functions (getters, comparisons, toHijriDate, formatHijriDate): return null or an empty string. These functions are often called in display contexts where a silent fallback is appropriate. Throwing would require try/catch around every date display call.
Mutation functions (arithmetic, boundary, fromHijriDate): throw. When you call addHijriMonths or startOfHijriMonth, you expect a Date back. Returning null would require null checks on every arithmetic result, which is error-prone. An exception is the better signal.
hijri-core implements a registry pattern:
// hijri-core/src/registry.ts
const _engines = new Map<string, CalendarEngine>();
export function registerCalendar(name: string, engine: CalendarEngine): void {
_engines.set(name, engine);
}Two engines ship by default: 'uaq' (Umm al-Qura) and 'fcna' (FCNA/ISNA). Third parties can register custom engines and use them with date-fns-hijri by passing { calendar: 'mycalendar' } to any function.
This package never touches the registry directly. It passes the options argument through to hijri-core's convenience wrappers, which resolve the calendar name internally.
Month arithmetic works by converting to a total month offset from the Hijri epoch, adding the delta, then decomposing back to year/month:
const totalMonths = (h.hy - 1) * 12 + (h.hm - 1) + months;
const newYear = Math.floor(totalMonths / 12) + 1;
const newMonth = (((totalMonths % 12) + 12) % 12) + 1;The double-modulo pattern (((n % 12) + 12) % 12) handles negative values correctly. If months is -1 and the current month is 1 (Muharram), totalMonths % 12 gives -1, the pattern corrects it to 11, and the year decrements by 1 via Math.floor.
Day clamping runs after the new month is determined:
const maxDay = coreDaysInHijriMonth(newYear, newMonth, options);
const newDay = Math.min(h.hd, maxDay);This handles the case where month lengths differ (29 vs 30 days) without requiring the caller to know the calendar structure.
tsup produces four outputs from a single source:
| File | Format | Purpose |
|---|---|---|
dist/index.cjs |
CommonJS | Node.js require()
|
dist/index.mjs |
ESM |
import / bundlers |
dist/index.d.ts |
TypeScript declarations | CJS consumers |
dist/index.d.mts |
TypeScript declarations | ESM consumers |
hijri-core is marked as external so it resolves from the consumer's node_modules rather than being bundled. This is required for the peer dependency pattern to work correctly.
Source maps ship in the package for debugging. The target: 'es2020' setting is conservative enough to run on all supported Node versions (20+) without polyfills.
date-fns-hijri · MIT License · npm · Issues
Guides
Examples
Reference
API — Per Function
- toHijriDate
- fromHijriDate
- isValidHijriDate
- getHijriYear
- getHijriMonth
- getHijriDay
- getDaysInHijriMonth
- getHijriQuarter
- getHijriMonthName
- getHijriWeekdayName
- formatHijriDate
- addHijriMonths
- addHijriYears
- startOfHijriMonth
- endOfHijriMonth
- isSameHijriMonth
- isSameHijriYear
Community