mirror of
https://github.com/acamarata/date-fns-hijri.git
synced 2026-06-30 18:54:25 +00:00
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.
125 lines
5.9 KiB
Markdown
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)
|