luxon-hijri/README.md
Aric Camarata 1ab6463184 feat: v2.0.0 — FCNA calendar, dual ESM/CJS build, weekday bug fix, full test suite
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.
2026-02-25 13:25:11 -05:00

7.4 KiB
Raw Blame History

luxon-hijri

npm version CI License: MIT

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

npm install luxon-hijri

Quick Start

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.

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.

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.

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.

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.

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.

function isValidHijriDate(hy: number, hm: number, hd: number, options?: ConversionOptions): boolean

For 'uaq' (default): year must be 13181500, month 112, day must not exceed the actual month length from the UAQ table.

For 'fcna': year must be ≥ 1, month 112, day must not exceed the computed FCNA month length.

Types

interface HijriDate {
  hy: number; // Hijri year
  hm: number; // Hijri month (112)
  hd: number; // Hijri day (130)
}

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

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 (17, 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 13181500 (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 10003000 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.

Compatibility

  • Node.js 20+ (ESM and CJS)
  • Bundlers: webpack, Rollup, Vite, esbuild (tree-shakeable, sideEffects: false)
  • TypeScript: full type definitions included

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

  • nrel-spa — NREL Solar Position Algorithm (pure JS)
  • pray-calc — Islamic prayer times, depends on nrel-spa
  • solar-spa — NREL SPA compiled to WebAssembly

License

MIT. Copyright (c) 2024-2026 Aric Camarata.

See LICENSE for the full text.