mirror of
https://github.com/acamarata/luxon-hijri.git
synced 2026-07-01 19:24:28 +00:00
Core fixes:
- Fix critical weekday bug: iE/iEEE/iEEEE tokens used Hijri year as Gregorian,
returning weekdays ~580 years wrong. Now converts via toGregorian() first.
- Fix era tokens iooo/ioooo: were returning Gregorian era, now always return "AH".
- Fix toGregorian timezone sensitivity: was using DateTime.local(), now DateTime.utc().
- Fix format token regex: word-boundary approach caused partial matches.
New: FCNA/ISNA calendar support:
- toHijri, toGregorian, isValidHijriDate now accept { calendar: 'fcna' } option.
- FCNA criterion: conjunction before 12:00 UTC → month starts D+1, else D+2.
- New moon times from Meeus Ch.49 full formula (accurate to within minutes, 1000–3000 CE).
- Works for all Hijri years, not just the 1318–1500 UAQ table range.
- Anchor: UAQ table for in-range years, Islamic epoch estimate for out-of-range.
- Exports: CalendarSystem, ConversionOptions types.
Build and infrastructure:
- pnpm replaces npm; tsup replaces tsc for dual CJS/ESM output.
- Exports map with types-first conditional exports for import/require.
- Binary search O(log 183) replaces linear O(n) scan in all three functions.
- Luxon upgraded from ^2.5.2 to ^3.5.0; TypeScript from ^4 to ^5.5.
- CI: Node 20/22/24 matrix, typecheck, and pack-check jobs.
- GitHub Wiki: four pages synced via Actions on push.
- Test suite: 81 ESM tests + 24 CJS tests, verified against ISNA 2023–2025 calendars.
- Exports hwLong, hwShort, hwNumeric weekday arrays.
Breaking changes:
- Dual ESM/CJS exports map (CJS consumers: no change via main field).
- HijriYearRecord replaces hDates interface name.
- Luxon peer dep bumped to ^3.5.0.
- Node >=20 required.
199 lines
7.4 KiB
Markdown
199 lines
7.4 KiB
Markdown
# luxon-hijri
|
||
|
||
[](https://www.npmjs.com/package/luxon-hijri)
|
||
[](https://github.com/acamarata/luxon-hijri/actions/workflows/ci.yml)
|
||
[](./LICENSE)
|
||
|
||
Hijri/Gregorian date conversion and formatting. Supports two calendar systems: Umm al-Qura (default, table-based) and FCNA/ISNA (astronomical, all Hijri years). Built on Luxon.
|
||
|
||
## Installation
|
||
|
||
```bash
|
||
npm install luxon-hijri
|
||
```
|
||
|
||
## Quick Start
|
||
|
||
```javascript
|
||
import { toHijri, toGregorian, formatHijriDate } from 'luxon-hijri';
|
||
|
||
// Gregorian to Hijri (Umm al-Qura, default)
|
||
const h = toHijri(new Date(2023, 2, 23, 12)); // March 23, 2023
|
||
// { hy: 1444, hm: 9, hd: 1 }
|
||
|
||
// Hijri to Gregorian
|
||
const g = toGregorian(1444, 9, 1); // 1 Ramadan 1444
|
||
// Date: 2023-03-23T00:00:00.000Z
|
||
|
||
// Format a Hijri date
|
||
formatHijriDate({ hy: 1444, hm: 9, hd: 1 }, 'iEEEE, iD iMMMM iYYYY ioooo');
|
||
// "Yawm al-Khamis, 1 Ramadan 1444 AH"
|
||
|
||
// FCNA/ISNA calendar (astronomical, works for all Hijri years)
|
||
toHijri(new Date(2025, 2, 1, 12), { calendar: 'fcna' }); // { hy: 1446, hm: 9, hd: 1 }
|
||
toGregorian(1446, 9, 1, { calendar: 'fcna' }); // Date: 2025-03-01T00:00:00.000Z
|
||
```
|
||
|
||
## API
|
||
|
||
### `toHijri(date, options?)`
|
||
|
||
Converts a Gregorian `Date` to a Hijri date object.
|
||
|
||
```typescript
|
||
function toHijri(date: Date, options?: ConversionOptions): HijriDate | null
|
||
```
|
||
|
||
For `'uaq'` (default): returns `null` if the date falls outside the table range (before 1 Muharram 1318 H / 1900-04-30, or at/after 1 Muharram 1501 H / 2077-11-17). Uses local date components.
|
||
|
||
For `'fcna'`: returns `null` only for dates before 1 AH. Uses UTC date components (FCNA boundaries are defined in UTC).
|
||
|
||
Throws `Error("Invalid Gregorian date")` if `date` is not a valid `Date`.
|
||
|
||
```javascript
|
||
toHijri(new Date(2024, 6, 7, 12)) // { hy: 1446, hm: 1, hd: 1 } (UAQ)
|
||
toHijri(new Date(2025, 2, 1, 12), { calendar: 'fcna' }) // { hy: 1446, hm: 9, hd: 1 } (FCNA)
|
||
toHijri(new Date(1800, 0, 1)) // null — before UAQ table range
|
||
```
|
||
|
||
### `toGregorian(hy, hm, hd, options?)`
|
||
|
||
Converts a Hijri date to a Gregorian `Date` at UTC midnight.
|
||
|
||
```typescript
|
||
function toGregorian(hy: number, hm: number, hd: number, options?: ConversionOptions): Date | null
|
||
```
|
||
|
||
Throws `Error("Invalid Hijri date")` if the date is invalid for the selected calendar.
|
||
|
||
```javascript
|
||
toGregorian(1446, 1, 1) // Date: 2024-07-07T00:00:00.000Z (UAQ)
|
||
toGregorian(1446, 9, 1, { calendar: 'fcna' }) // Date: 2025-03-01T00:00:00.000Z (FCNA)
|
||
toGregorian(1, 1, 1, { calendar: 'fcna' }) // Date: 0622-07-18T00:00:00.000Z (Islamic epoch)
|
||
```
|
||
|
||
### `formatHijriDate(date, format)`
|
||
|
||
Formats a Hijri date using the token patterns below. Tokens not listed pass through unchanged.
|
||
|
||
```typescript
|
||
function formatHijriDate(date: HijriDate, format: string): string
|
||
```
|
||
|
||
| Token | Output | Example |
|
||
| --- | --- | --- |
|
||
| `iYYYY` | Year, 4 digits | `1444` |
|
||
| `iYY` | Year, last 2 digits | `44` |
|
||
| `iMMMM` | Month, full name | `Ramadan` |
|
||
| `iMMM` | Month, medium name | `Ramadan` |
|
||
| `iMM` | Month, zero-padded | `09` |
|
||
| `iM` | Month, no padding | `9` |
|
||
| `iDD` | Day, zero-padded | `01` |
|
||
| `iD` | Day, no padding | `1` |
|
||
| `iEEEE` | Weekday, full name | `Yawm al-Khamis` |
|
||
| `iEEE` | Weekday, abbreviated | `Kham` |
|
||
| `iE` | Weekday, numeric (Sun=1) | `5` |
|
||
| `ioooo` | Era, full | `AH` |
|
||
| `iooo` | Era, abbreviated | `AH` |
|
||
| `HH`, `H`, `hh`, `h` | Hour (via Luxon) | `14`, `14`, `02`, `2` |
|
||
| `mm`, `m` | Minute (via Luxon) | `05`, `5` |
|
||
| `ss`, `s` | Second (via Luxon) | `30`, `30` |
|
||
| `a` | AM/PM | `AM` |
|
||
| `z`, `zz`, `zzz` | Timezone | `UTC` |
|
||
| `Z`, `ZZ` | Timezone offset | `+00:00` |
|
||
|
||
### `isValidHijriDate(hy, hm, hd, options?)`
|
||
|
||
Returns `true` if the Hijri date is valid for the selected calendar.
|
||
|
||
```typescript
|
||
function isValidHijriDate(hy: number, hm: number, hd: number, options?: ConversionOptions): boolean
|
||
```
|
||
|
||
For `'uaq'` (default): year must be 1318–1500, month 1–12, day must not exceed the actual month length from the UAQ table.
|
||
|
||
For `'fcna'`: year must be ≥ 1, month 1–12, day must not exceed the computed FCNA month length.
|
||
|
||
### Types
|
||
|
||
```typescript
|
||
interface HijriDate {
|
||
hy: number; // Hijri year
|
||
hm: number; // Hijri month (1–12)
|
||
hd: number; // Hijri day (1–30)
|
||
}
|
||
|
||
type CalendarSystem = 'uaq' | 'fcna';
|
||
|
||
interface ConversionOptions {
|
||
calendar?: CalendarSystem; // default: 'uaq'
|
||
}
|
||
|
||
interface HijriYearRecord {
|
||
hy: number; // Hijri year
|
||
dpm: number; // days-per-month bitmask (bit 0 = month 1, 1 = 30 days, 0 = 29 days)
|
||
gy: number; // Gregorian year of 1 Muharram
|
||
gm: number; // Gregorian month of 1 Muharram
|
||
gd: number; // Gregorian day of 1 Muharram
|
||
}
|
||
```
|
||
|
||
### Additional exports
|
||
|
||
```javascript
|
||
import {
|
||
hDatesTable, // HijriYearRecord[] — the full Umm al-Qura table (184 entries)
|
||
hmLong, // string[12] — full month names
|
||
hmMedium, // string[12] — medium month names
|
||
hmShort, // string[12] — abbreviated month names
|
||
hwLong, // string[7] — full weekday names (Sunday first)
|
||
hwShort, // string[7] — abbreviated weekday names
|
||
hwNumeric, // number[7] — weekday numbers (1–7, Sunday=1)
|
||
formatPatterns, // Record<string, string> — token reference
|
||
} from 'luxon-hijri';
|
||
```
|
||
|
||
## Calendar Systems
|
||
|
||
**Umm al-Qura (`'uaq'`, default):** Official Saudi calendar, table-based, covers Hijri years 1318–1500 (April 1900 to November 2076). Authoritative for Saudi Arabia and widely used across the Arab world.
|
||
|
||
**FCNA/ISNA (`'fcna'`):** Used by the Fiqh Council of North America and ISNA. Astronomical criterion: if the new moon conjunction occurs before 12:00 UTC on day D, the month begins at midnight of D+1; otherwise D+2. Works for all Hijri years (no range limit). New moon times use the full Meeus Chapter 49 algorithm, accurate to within a few minutes for 1000–3000 CE.
|
||
|
||
## Architecture
|
||
|
||
The UAQ engine is a pure table lookup with binary search (O(log 183)). The FCNA engine computes new moon times astronomically using the Meeus Ch.49 formula — 3 to 5 trigonometric evaluations per call, sub-millisecond on any modern JS engine.
|
||
|
||
For more detail see the [Architecture wiki page](https://github.com/acamarata/luxon-hijri/wiki/Architecture).
|
||
|
||
## Compatibility
|
||
|
||
- Node.js 20+ (ESM and CJS)
|
||
- Bundlers: webpack, Rollup, Vite, esbuild (tree-shakeable, `sideEffects: false`)
|
||
- TypeScript: full type definitions included
|
||
|
||
## TypeScript
|
||
|
||
```typescript
|
||
import { toHijri, toGregorian, formatHijriDate, isValidHijriDate } from 'luxon-hijri';
|
||
import type { HijriDate, HijriYearRecord, CalendarSystem, ConversionOptions } from 'luxon-hijri';
|
||
|
||
const h: HijriDate | null = toHijri(new Date());
|
||
const g: Date | null = toGregorian(1444, 9, 1, { calendar: 'fcna' });
|
||
```
|
||
|
||
## Documentation
|
||
|
||
Full API reference, architecture notes, calendar background, and format token guide:
|
||
[https://github.com/acamarata/luxon-hijri/wiki](https://github.com/acamarata/luxon-hijri/wiki)
|
||
|
||
## Related
|
||
|
||
- [nrel-spa](https://www.npmjs.com/package/nrel-spa) — NREL Solar Position Algorithm (pure JS)
|
||
- [pray-calc](https://www.npmjs.com/package/pray-calc) — Islamic prayer times, depends on nrel-spa
|
||
- [solar-spa](https://www.npmjs.com/package/solar-spa) — NREL SPA compiled to WebAssembly
|
||
|
||
## License
|
||
|
||
MIT. Copyright (c) 2024-2026 Aric Camarata.
|
||
|
||
See [LICENSE](./LICENSE) for the full text.
|