date-fns-hijri/.wiki/Architecture.md
Aric Camarata d8c5398fdf feat: initial release of date-fns-hijri v1.0.0
Functional Hijri date utility library in date-fns style. 17 exported
functions: toHijriDate, fromHijriDate, isValidHijriDate, getHijriYear,
getHijriMonth, getHijriDay, getDaysInHijriMonth, getHijriMonthName,
getHijriWeekdayName, formatHijriDate, addHijriMonths, addHijriYears,
startOfHijriMonth, endOfHijriMonth, isSameHijriMonth, isSameHijriYear,
getHijriQuarter. All delegate to hijri-core. UTC-midnight-to-local-noon
correction applied in arithmetic functions to avoid timezone boundary
drift. 55 ESM + 10 CJS tests passing. Dual CJS/ESM build.
2026-02-25 14:15:24 -05:00

125 lines
5.9 KiB
Markdown

# Architecture
## Design Goals
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:
1. The Hijri calendar engine should be a separate, zero-dependency package (`hijri-core`). This keeps `date-fns-hijri` thin and lets other libraries use the engine directly.
2. `date-fns-hijri` should not couple users to any specific calendar algorithm. The `options.calendar` parameter threads through to `hijri-core`'s registry, so custom calendar engines registered there are usable without changes to this package.
## Functional API Pattern
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:
```typescript
// 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.
## Peer Dependency Pattern
`hijri-core` is a peer dependency. The consumer installs it explicitly:
```bash
pnpm add date-fns-hijri hijri-core
```
This pattern has two benefits:
- The consumer controls which version of `hijri-core` they use. If a critical fix ships in `hijri-core`, they can upgrade without waiting for a new `date-fns-hijri` release.
- Applications that use multiple Hijri-aware packages (e.g., both `date-fns-hijri` and a separate formatter) share a single `hijri-core` instance. 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`.
## Format Token Resolution
`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:
- `iYYYY` before `iYY` (prevents `iYYYY` matching as `iYY` + `YY`)
- `iMMMM` before `iMMM` before `iMM` before `iM`
- `iEEEE` before `iEEE` before `iE`
- `ioooo` before `iooo`
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.
## Out-of-Range Handling
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.
## Calendar System Architecture
`hijri-core` implements a registry pattern:
```typescript
// 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.
## Arithmetic Implementation
Month arithmetic works by converting to a total month offset from the Hijri epoch, adding the delta, then decomposing back to year/month:
```typescript
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:
```typescript
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.
## Build Configuration
`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.
---
[Home](Home) · [API Reference](API-Reference) · [Architecture](Architecture)