feat: v2.1.0 — delegate engine logic to hijri-core

All Hijri conversion logic (UAQ table, FCNA algorithm, month/weekday
names, isValidHijriDate) now lives in hijri-core and is re-exported
from this package with identical signatures. Zero breaking changes:
the public API surface, type exports, and behavior are unchanged.

src/fcna.ts removed — FCNA engine is in hijri-core.
src/hDates.ts, hMonths.ts, hWeekdays.ts, utils.ts, toHijri.ts,
toGregorian.ts all rewritten as thin re-exports or single-line wrappers.

hijri-core added as a runtime dependency.
This commit is contained in:
Aric Camarata 2026-02-25 14:14:29 -05:00
parent 8b734dd777
commit 30afd3c8a7
12 changed files with 42 additions and 731 deletions

View file

@ -1,9 +1,19 @@
# Changelog
<!-- markdownlint-disable MD024 -->
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
## [2.1.0] - 2026-02-25
### Changed
- Engine logic extracted to `hijri-core` (new dependency). All Hijri conversion algorithms now live in that package and are re-exported from `luxon-hijri` with identical signatures. Zero breaking changes to the public API.
- `src/hDates.ts`, `src/hMonths.ts`, `src/hWeekdays.ts`, `src/fcna.ts`, `src/utils.ts`, `src/toHijri.ts`, `src/toGregorian.ts` — all now delegate to `hijri-core`. The UAQ table, FCNA engine, month/weekday names, and conversion functions have a single source of truth across the hijri ecosystem.
- `hijri-core ^1.0.0` added as a runtime dependency. `luxon` stays as a runtime dependency (still needed by `formatHijriDate` for time and timezone tokens).
## [2.0.0] - 2026-02-25
### Added

View file

@ -1,6 +1,6 @@
{
"name": "luxon-hijri",
"version": "2.0.0",
"version": "2.1.0",
"description": "Hijri/Gregorian date conversion and formatting using the Umm al-Qura calendar. Built on Luxon. Supports toHijri, toGregorian, formatHijriDate, and isValidHijriDate.",
"author": "Aric Camarata",
"license": "MIT",
@ -53,6 +53,7 @@
"typescript"
],
"dependencies": {
"hijri-core": "file:../hijri-core",
"luxon": "^3.5.0"
},
"devDependencies": {

View file

@ -8,6 +8,9 @@ importers:
.:
dependencies:
hijri-core:
specifier: file:../hijri-core
version: file:../hijri-core
luxon:
specifier: ^3.5.0
version: 3.7.2
@ -407,6 +410,10 @@ packages:
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
hijri-core@file:../hijri-core:
resolution: {directory: ../hijri-core, type: directory}
engines: {node: '>=20'}
joycon@3.1.1:
resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==}
engines: {node: '>=10'}
@ -794,6 +801,8 @@ snapshots:
fsevents@2.3.3:
optional: true
hijri-core@file:../hijri-core: {}
joycon@3.1.1: {}
lilconfig@3.1.3: {}

View file

@ -1,301 +0,0 @@
// fcna.ts — FCNA/ISNA Hijri calendar engine
//
// The Fiqh Council of North America (FCNA) uses a global astronomical criterion:
// if the new moon conjunction occurs before 12:00 noon UTC on day D, the new
// Hijri month begins at midnight starting day D+1 (next calendar day); if at or
// after 12:00 UTC, the month begins at midnight starting day D+2.
//
// For years in the Umm al-Qura table (13181500 H), the UAQ month start date
// serves as the anchor for locating the nearest new moon. For years outside that
// range the anchor comes from the Islamic epoch plus mean synodic months.
//
// New moon times come from Jean Meeus, Astronomical Algorithms (2nd ed.),
// Chapter 49 — accurate to within a few minutes for 1000 CE to 3000 CE.
import { hDatesTable } from './hDates';
import type { HijriDate } from './types';
// ─── Constants ───────────────────────────────────────────────────────────────
const SYNODIC = 29.530588861; // Mean synodic month (days)
const JDE0 = 2451550.09766; // Meeus k=0: mean new moon ~2000-01-06
const JDE_UNIX = 2440587.5; // JDE of Unix epoch 1970-01-01 00:00 UTC
const MS_PER_DAY = 86_400_000;
const TO_RAD = Math.PI / 180;
// Approximate k index of 1 Muharram 1 AH in Meeus numbering.
// Derived: Islamic epoch JDE ≈ 1948438.5 → k ≈ (1948438.5 JDE0) / SYNODIC ≈ 17037.
const K_EPOCH = -17037;
// ─── Meeus Chapter 49: corrected new moon JDE ────────────────────────────────
function newMoonJDE(k: number): number {
const T = k / 1236.85;
const T2 = T * T;
const T3 = T2 * T;
const T4 = T3 * T;
// Mean JDE (Meeus Eq. 49.1)
let jde = JDE0
+ SYNODIC * k
+ 0.00015437 * T2
- 0.000000150 * T3
+ 0.00000000073 * T4;
// Sun's mean anomaly M (degrees)
const M = (2.5534
+ 29.10535670 * k
- 0.0000014 * T2
- 0.00000011 * T3) % 360;
// Moon's mean anomaly M' (degrees)
const Mprime = (201.5643
+ 385.81693528 * k
+ 0.0107582 * T2
+ 0.00001238 * T3
- 0.000000058 * T4) % 360;
// Moon's argument of latitude F (degrees)
const F = (160.7108
+ 390.67050284 * k
- 0.0016118 * T2
- 0.00000227 * T3
+ 0.000000011 * T4) % 360;
// Longitude of ascending node Omega (degrees)
const Omega = (124.7746
- 1.56375588 * k
+ 0.0020672 * T2
+ 0.00000215 * T3) % 360;
// Eccentricity correction factor E
const E = 1 - 0.002516 * T - 0.0000074 * T2;
const E2 = E * E;
// Angles to radians
const Mrad = M * TO_RAD;
const Mprad = Mprime * TO_RAD;
const Frad = F * TO_RAD;
const Orad = Omega * TO_RAD;
// Planetary correction (Meeus Table 49.a — new moon phase)
jde +=
- 0.40720 * Math.sin(Mprad)
+ 0.17241 * E * Math.sin(Mrad)
+ 0.01608 * Math.sin(2 * Mprad)
+ 0.01039 * Math.sin(2 * Frad)
+ 0.00739 * E * Math.sin(Mprad - Mrad)
- 0.00514 * E * Math.sin(Mprad + Mrad)
+ 0.00208 * E2 * Math.sin(2 * Mrad)
- 0.00111 * Math.sin(Mprad - 2 * Frad)
- 0.00057 * Math.sin(Mprad + 2 * Frad)
+ 0.00056 * E * Math.sin(2 * Mprad + Mrad)
- 0.00042 * Math.sin(3 * Mprad)
+ 0.00042 * E * Math.sin(Mrad + 2 * Frad)
+ 0.00038 * E * Math.sin(Mrad - 2 * Frad)
- 0.00024 * E * Math.sin(2 * Mprad - Mrad)
- 0.00017 * Math.sin(Orad)
- 0.00007 * Math.sin(Mprad + 2 * Mrad)
+ 0.00004 * Math.sin(2 * Mprad - 2 * Frad)
+ 0.00004 * Math.sin(3 * Mrad)
+ 0.00003 * Math.sin(Mprad + Mrad - 2 * Frad)
+ 0.00003 * Math.sin(2 * Mprad + 2 * Frad)
- 0.00003 * Math.sin(Mprad + Mrad + 2 * Frad)
+ 0.00003 * Math.sin(Mprad - Mrad + 2 * Frad)
- 0.00002 * Math.sin(Mprad - Mrad - 2 * Frad)
- 0.00002 * Math.sin(3 * Mprad + Mrad)
+ 0.00002 * Math.sin(4 * Mprad);
// Additional planetary corrections (Meeus Table 49.b)
const A1 = (299.77 + 0.107408 * k - 0.009173 * T2) * TO_RAD;
const A2 = (251.88 + 0.016321 * k) * TO_RAD;
const A3 = (251.83 + 26.651886 * k) * TO_RAD;
const A4 = (349.42 + 36.412478 * k) * TO_RAD;
const A5 = ( 84.66 + 18.206239 * k) * TO_RAD;
const A6 = (141.74 + 53.303771 * k) * TO_RAD;
const A7 = (207.14 + 2.453732 * k) * TO_RAD;
const A8 = (154.84 + 7.306860 * k) * TO_RAD;
const A9 = ( 34.52 + 27.261239 * k) * TO_RAD;
const A10 = (207.19 + 0.121824 * k) * TO_RAD;
const A11 = (291.34 + 1.844379 * k) * TO_RAD;
const A12 = (161.72 + 24.198154 * k) * TO_RAD;
const A13 = (239.56 + 25.513099 * k) * TO_RAD;
const A14 = (331.55 + 3.592518 * k) * TO_RAD;
jde +=
+ 0.000325 * Math.sin(A1)
+ 0.000165 * Math.sin(A2)
+ 0.000164 * Math.sin(A3)
+ 0.000126 * Math.sin(A4)
+ 0.000110 * Math.sin(A5)
+ 0.000062 * Math.sin(A6)
+ 0.000060 * Math.sin(A7)
+ 0.000056 * Math.sin(A8)
+ 0.000047 * Math.sin(A9)
+ 0.000042 * Math.sin(A10)
+ 0.000040 * Math.sin(A11)
+ 0.000037 * Math.sin(A12)
+ 0.000035 * Math.sin(A13)
+ 0.000023 * Math.sin(A14);
return jde;
}
// ─── JDE / UTC conversion ─────────────────────────────────────────────────────
function jdeToUtcMs(jde: number): number {
return (jde - JDE_UNIX) * MS_PER_DAY;
}
function utcMsToKApprox(ms: number): number {
const jde = ms / MS_PER_DAY + JDE_UNIX;
return (jde - JDE0) / SYNODIC;
}
// ─── Find nearest corrected new moon ─────────────────────────────────────────
// Returns the UTC ms of the corrected new moon closest to anchorMs.
// Searches k0-2 through k0+2 (5 candidates) to handle any estimation error.
function nearestNewMoonMs(anchorMs: number): number {
const k0 = Math.round(utcMsToKApprox(anchorMs));
let bestMs = 0;
let bestDist = Infinity;
for (let k = k0 - 2; k <= k0 + 2; k++) {
const ms = jdeToUtcMs(newMoonJDE(k));
const dist = Math.abs(ms - anchorMs);
if (dist < bestDist) {
bestDist = dist;
bestMs = ms;
}
}
return bestMs;
}
// ─── FCNA criterion ──────────────────────────────────────────────────────────
// Given a conjunction UTC ms, return the midnight UTC ms that starts the
// new FCNA Hijri month: D+1 if conjunction before 12:00 UTC, D+2 otherwise.
function fcnaCriterionMs(conjMs: number): number {
const midnight = Math.floor(conjMs / MS_PER_DAY) * MS_PER_DAY;
const noon = midnight + 12 * 3_600_000;
return conjMs < noon ? midnight + MS_PER_DAY : midnight + 2 * MS_PER_DAY;
}
// ─── UAQ anchor ──────────────────────────────────────────────────────────────
// Return the UTC ms of the UAQ month start for (hy, hm).
// For years 13181500 H: binary-search hDatesTable, sum dpm day counts.
// For years outside that range: estimate from Islamic epoch + mean month count.
export function uaqAnchorMs(hy: number, hm: number): number {
// Binary search for hy in table.
let lo = 0, hi = hDatesTable.length - 1, found = -1;
while (lo <= hi) {
const mid = (lo + hi) >>> 1;
const midHy = hDatesTable[mid].hy;
if (midHy === hy) { found = mid; break; }
else if (midHy < hy) lo = mid + 1;
else hi = mid - 1;
}
if (found !== -1 && hDatesTable[found].dpm !== 0) {
// In-range: sum prior-month day counts from table start date.
const r = hDatesTable[found];
let days = 0;
for (let i = 0; i < hm - 1; i++) {
days += (r.dpm >> i) & 1 ? 30 : 29;
}
return Date.UTC(r.gy, r.gm - 1, r.gd) + days * MS_PER_DAY;
}
// Out of range: estimate from Islamic epoch + mean months elapsed.
const monthsFromEpoch = (hy - 1) * 12 + (hm - 1);
const kApprox = K_EPOCH + monthsFromEpoch;
return jdeToUtcMs(newMoonJDE(kApprox));
}
// ─── FCNA month start ─────────────────────────────────────────────────────────
// Return UTC ms of midnight beginning the given FCNA Hijri month.
export function fcnaMonthStartMs(hy: number, hm: number): number {
const anchor = uaqAnchorMs(hy, hm);
const conjMs = nearestNewMoonMs(anchor);
return fcnaCriterionMs(conjMs);
}
// ─── FCNA month length ───────────────────────────────────────────────────────
export function fcnaDaysInMonth(hy: number, hm: number): number {
const thisStart = fcnaMonthStartMs(hy, hm);
const nextHy = hm < 12 ? hy : hy + 1;
const nextHm = hm < 12 ? hm + 1 : 1;
const nextStart = fcnaMonthStartMs(nextHy, nextHm);
return Math.round((nextStart - thisStart) / MS_PER_DAY);
}
// ─── FCNA Gregorian → Hijri ──────────────────────────────────────────────────
export function fcnaToHijri(gregorianDate: Date): HijriDate | null {
if (!(gregorianDate instanceof Date) || isNaN(gregorianDate.getTime())) {
throw new Error('Invalid Gregorian date');
}
// Use UTC date components. The FCNA criterion is UTC-based (conjunction before
// 12:00 UTC), so month boundaries are defined in UTC. Using UTC methods ensures
// fcnaToGregorian ↔ fcnaToHijri round-trips correctly in any host timezone.
const inputMs = Date.UTC(
gregorianDate.getUTCFullYear(),
gregorianDate.getUTCMonth(),
gregorianDate.getUTCDate(),
);
// Shift back ~15 days before estimating k so that kApprox resolves to the
// current month's conjunction rather than possibly the next month's.
const kApprox = utcMsToKApprox(inputMs - 15 * MS_PER_DAY);
const k0 = Math.floor(kApprox);
// Search k0-1, k0, k0+1 for the FCNA month containing inputMs.
for (let ki = k0 - 1; ki <= k0 + 1; ki++) {
const conjMs = jdeToUtcMs(newMoonJDE(ki));
const monthStart = fcnaCriterionMs(conjMs);
if (monthStart > inputMs) continue;
const nextConjMs = jdeToUtcMs(newMoonJDE(ki + 1));
const nextMonthStart = fcnaCriterionMs(nextConjMs);
if (inputMs < nextMonthStart) {
// inputMs falls in the month that began at monthStart (k = ki).
// Map ki → Hijri (hy, hm) via K_EPOCH offset.
const monthsFromEpoch = ki - K_EPOCH;
let hy = Math.floor(monthsFromEpoch / 12) + 1;
let hm = (monthsFromEpoch % 12) + 1;
// JavaScript % can return negative; normalize to 112.
if (hm <= 0) { hm += 12; hy--; }
if (hy < 1) return null; // before 1 AH
const hd = Math.round((inputMs - monthStart) / MS_PER_DAY) + 1;
return { hy, hm, hd };
}
}
return null;
}
// ─── FCNA Hijri → Gregorian ──────────────────────────────────────────────────
export function fcnaToGregorian(hy: number, hm: number, hd: number): Date | null {
if (hy < 1 || hm < 1 || hm > 12 || hd < 1) return null;
const days = fcnaDaysInMonth(hy, hm);
if (hd > days) return null;
const startMs = fcnaMonthStartMs(hy, hm);
return new Date(startMs + (hd - 1) * MS_PER_DAY);
}
// ─── FCNA validation ─────────────────────────────────────────────────────────
export function fcnaIsValid(hy: number, hm: number, hd: number): boolean {
if (hy < 1 || hm < 1 || hm > 12 || hd < 1) return false;
return hd <= fcnaDaysInMonth(hy, hm);
}

View file

@ -1,191 +1,3 @@
// hDates.ts
// Hijri Dates Reference Table
import { HijriYearRecord } from './types';
export type { HijriYearRecord };
export const hDatesTable: HijriYearRecord[] = [
{ hy: 1318, dpm: 0x02EA, gy: 1900, gm: 4, gd: 30 },
{ hy: 1319, dpm: 0x06E9, gy: 1901, gm: 4, gd: 19 },
{ hy: 1320, dpm: 0x0ED2, gy: 1902, gm: 4, gd: 9 },
{ hy: 1321, dpm: 0x0EA4, gy: 1903, gm: 3, gd: 30 },
{ hy: 1322, dpm: 0x0D4A, gy: 1904, gm: 3, gd: 18 },
{ hy: 1323, dpm: 0x0A96, gy: 1905, gm: 3, gd: 7 },
{ hy: 1324, dpm: 0x0536, gy: 1906, gm: 2, gd: 24 },
{ hy: 1325, dpm: 0x0AB5, gy: 1907, gm: 2, gd: 13 },
{ hy: 1326, dpm: 0x0DAA, gy: 1908, gm: 2, gd: 3 },
{ hy: 1327, dpm: 0x0BA4, gy: 1909, gm: 1, gd: 23 },
{ hy: 1328, dpm: 0x0B49, gy: 1910, gm: 1, gd: 12 },
{ hy: 1329, dpm: 0x0A93, gy: 1911, gm: 1, gd: 1 },
{ hy: 1330, dpm: 0x052B, gy: 1911, gm: 12, gd: 21 },
{ hy: 1331, dpm: 0x0A57, gy: 1912, gm: 12, gd: 9 },
{ hy: 1332, dpm: 0x04B6, gy: 1913, gm: 11, gd: 29 },
{ hy: 1333, dpm: 0x0AB5, gy: 1914, gm: 11, gd: 18 },
{ hy: 1334, dpm: 0x05AA, gy: 1915, gm: 11, gd: 8 },
{ hy: 1335, dpm: 0x0D55, gy: 1916, gm: 10, gd: 27 },
{ hy: 1336, dpm: 0x0D2A, gy: 1917, gm: 10, gd: 17 },
{ hy: 1337, dpm: 0x0A56, gy: 1918, gm: 10, gd: 6 },
{ hy: 1338, dpm: 0x04AE, gy: 1919, gm: 9, gd: 25 },
{ hy: 1339, dpm: 0x095D, gy: 1920, gm: 9, gd: 13 },
{ hy: 1340, dpm: 0x02EC, gy: 1921, gm: 9, gd: 3 },
{ hy: 1341, dpm: 0x06D5, gy: 1922, gm: 8, gd: 23 },
{ hy: 1342, dpm: 0x06AA, gy: 1923, gm: 8, gd: 13 },
{ hy: 1343, dpm: 0x0555, gy: 1924, gm: 8, gd: 1 },
{ hy: 1344, dpm: 0x04AB, gy: 1925, gm: 7, gd: 21 },
{ hy: 1345, dpm: 0x095B, gy: 1926, gm: 7, gd: 10 },
{ hy: 1346, dpm: 0x02BA, gy: 1927, gm: 6, gd: 30 },
{ hy: 1347, dpm: 0x0575, gy: 1928, gm: 6, gd: 18 },
{ hy: 1348, dpm: 0x0BB2, gy: 1929, gm: 6, gd: 8 },
{ hy: 1349, dpm: 0x0764, gy: 1930, gm: 5, gd: 29 },
{ hy: 1350, dpm: 0x0749, gy: 1931, gm: 5, gd: 18 },
{ hy: 1351, dpm: 0x0655, gy: 1932, gm: 5, gd: 6 },
{ hy: 1352, dpm: 0x02AB, gy: 1933, gm: 4, gd: 25 },
{ hy: 1353, dpm: 0x055B, gy: 1934, gm: 4, gd: 14 },
{ hy: 1354, dpm: 0x0ADA, gy: 1935, gm: 4, gd: 4 },
{ hy: 1355, dpm: 0x06D4, gy: 1936, gm: 3, gd: 24 },
{ hy: 1356, dpm: 0x0EC9, gy: 1937, gm: 3, gd: 13 },
{ hy: 1357, dpm: 0x0D92, gy: 1938, gm: 3, gd: 3 },
{ hy: 1358, dpm: 0x0D25, gy: 1939, gm: 2, gd: 20 },
{ hy: 1359, dpm: 0x0A4D, gy: 1940, gm: 2, gd: 9 },
{ hy: 1360, dpm: 0x02AD, gy: 1941, gm: 1, gd: 28 },
{ hy: 1361, dpm: 0x056D, gy: 1942, gm: 1, gd: 17 },
{ hy: 1362, dpm: 0x0B6A, gy: 1943, gm: 1, gd: 7 },
{ hy: 1363, dpm: 0x0B52, gy: 1943, gm: 12, gd: 28 },
{ hy: 1364, dpm: 0x0AA5, gy: 1944, gm: 12, gd: 16 },
{ hy: 1365, dpm: 0x0A4B, gy: 1945, gm: 12, gd: 5 },
{ hy: 1366, dpm: 0x0497, gy: 1946, gm: 11, gd: 24 },
{ hy: 1367, dpm: 0x0937, gy: 1947, gm: 11, gd: 13 },
{ hy: 1368, dpm: 0x02B6, gy: 1948, gm: 11, gd: 2 },
{ hy: 1369, dpm: 0x0575, gy: 1949, gm: 10, gd: 22 },
{ hy: 1370, dpm: 0x0D6A, gy: 1950, gm: 10, gd: 12 },
{ hy: 1371, dpm: 0x0D52, gy: 1951, gm: 10, gd: 2 },
{ hy: 1372, dpm: 0x0A96, gy: 1952, gm: 9, gd: 20 },
{ hy: 1373, dpm: 0x092D, gy: 1953, gm: 9, gd: 9 },
{ hy: 1374, dpm: 0x025D, gy: 1954, gm: 8, gd: 29 },
{ hy: 1375, dpm: 0x04DD, gy: 1955, gm: 8, gd: 18 },
{ hy: 1376, dpm: 0x0ADA, gy: 1956, gm: 8, gd: 7 },
{ hy: 1377, dpm: 0x05D4, gy: 1957, gm: 7, gd: 28 },
{ hy: 1378, dpm: 0x0DA9, gy: 1958, gm: 7, gd: 17 },
{ hy: 1379, dpm: 0x0D52, gy: 1959, gm: 7, gd: 7 },
{ hy: 1380, dpm: 0x0AAA, gy: 1960, gm: 6, gd: 25 },
{ hy: 1381, dpm: 0x04D6, gy: 1961, gm: 6, gd: 14 },
{ hy: 1382, dpm: 0x09B6, gy: 1962, gm: 6, gd: 3 },
{ hy: 1383, dpm: 0x0374, gy: 1963, gm: 5, gd: 24 },
{ hy: 1384, dpm: 0x0769, gy: 1964, gm: 5, gd: 12 },
{ hy: 1385, dpm: 0x0752, gy: 1965, gm: 5, gd: 2 },
{ hy: 1386, dpm: 0x06A5, gy: 1966, gm: 4, gd: 21 },
{ hy: 1387, dpm: 0x054B, gy: 1967, gm: 4, gd: 10 },
{ hy: 1388, dpm: 0x0AAB, gy: 1968, gm: 3, gd: 29 },
{ hy: 1389, dpm: 0x055A, gy: 1969, gm: 3, gd: 19 },
{ hy: 1390, dpm: 0x0AD5, gy: 1970, gm: 3, gd: 8 },
{ hy: 1391, dpm: 0x0DD2, gy: 1971, gm: 2, gd: 26 },
{ hy: 1392, dpm: 0x0DA4, gy: 1972, gm: 2, gd: 16 },
{ hy: 1393, dpm: 0x0D49, gy: 1973, gm: 2, gd: 4 },
{ hy: 1394, dpm: 0x0A95, gy: 1974, gm: 1, gd: 24 },
{ hy: 1395, dpm: 0x052D, gy: 1975, gm: 1, gd: 13 },
{ hy: 1396, dpm: 0x0A5D, gy: 1976, gm: 1, gd: 2 },
{ hy: 1397, dpm: 0x055A, gy: 1976, gm: 12, gd: 22 },
{ hy: 1398, dpm: 0x0AD5, gy: 1977, gm: 12, gd: 11 },
{ hy: 1399, dpm: 0x06AA, gy: 1978, gm: 12, gd: 1 },
{ hy: 1400, dpm: 0x0695, gy: 1979, gm: 11, gd: 20 },
{ hy: 1401, dpm: 0x052B, gy: 1980, gm: 11, gd: 8 },
{ hy: 1402, dpm: 0x0A57, gy: 1981, gm: 10, gd: 28 },
{ hy: 1403, dpm: 0x04AE, gy: 1982, gm: 10, gd: 18 },
{ hy: 1404, dpm: 0x0976, gy: 1983, gm: 10, gd: 7 },
{ hy: 1405, dpm: 0x056C, gy: 1984, gm: 9, gd: 26 },
{ hy: 1406, dpm: 0x0B55, gy: 1985, gm: 9, gd: 15 },
{ hy: 1407, dpm: 0x0AAA, gy: 1986, gm: 9, gd: 5 },
{ hy: 1408, dpm: 0x0A55, gy: 1987, gm: 8, gd: 25 },
{ hy: 1409, dpm: 0x04AD, gy: 1988, gm: 8, gd: 13 },
{ hy: 1410, dpm: 0x095D, gy: 1989, gm: 8, gd: 2 },
{ hy: 1411, dpm: 0x02DA, gy: 1990, gm: 7, gd: 23 },
{ hy: 1412, dpm: 0x05D9, gy: 1991, gm: 7, gd: 12 },
{ hy: 1413, dpm: 0x0DB2, gy: 1992, gm: 7, gd: 1 },
{ hy: 1414, dpm: 0x0BA4, gy: 1993, gm: 6, gd: 21 },
{ hy: 1415, dpm: 0x0B4A, gy: 1994, gm: 6, gd: 10 },
{ hy: 1416, dpm: 0x0A55, gy: 1995, gm: 5, gd: 30 },
{ hy: 1417, dpm: 0x02B5, gy: 1996, gm: 5, gd: 18 },
{ hy: 1418, dpm: 0x0575, gy: 1997, gm: 5, gd: 7 },
{ hy: 1419, dpm: 0x0B6A, gy: 1998, gm: 4, gd: 27 },
{ hy: 1420, dpm: 0x0BD2, gy: 1999, gm: 4, gd: 17 },
{ hy: 1421, dpm: 0x0BC4, gy: 2000, gm: 4, gd: 6 },
{ hy: 1422, dpm: 0x0B89, gy: 2001, gm: 3, gd: 26 },
{ hy: 1423, dpm: 0x0A95, gy: 2002, gm: 3, gd: 15 },
{ hy: 1424, dpm: 0x052D, gy: 2003, gm: 3, gd: 4 },
{ hy: 1425, dpm: 0x05AD, gy: 2004, gm: 2, gd: 21 },
{ hy: 1426, dpm: 0x0B6A, gy: 2005, gm: 2, gd: 10 },
{ hy: 1427, dpm: 0x06D4, gy: 2006, gm: 1, gd: 31 },
{ hy: 1428, dpm: 0x0DC9, gy: 2007, gm: 1, gd: 20 },
{ hy: 1429, dpm: 0x0D92, gy: 2008, gm: 1, gd: 10 },
{ hy: 1430, dpm: 0x0AA6, gy: 2008, gm: 12, gd: 29 },
{ hy: 1431, dpm: 0x0956, gy: 2009, gm: 12, gd: 18 },
{ hy: 1432, dpm: 0x02AE, gy: 2010, gm: 12, gd: 7 },
{ hy: 1433, dpm: 0x056D, gy: 2011, gm: 11, gd: 26 },
{ hy: 1434, dpm: 0x036A, gy: 2012, gm: 11, gd: 15 },
{ hy: 1435, dpm: 0x0B55, gy: 2013, gm: 11, gd: 4 },
{ hy: 1436, dpm: 0x0AAA, gy: 2014, gm: 10, gd: 25 },
{ hy: 1437, dpm: 0x094D, gy: 2015, gm: 10, gd: 14 },
{ hy: 1438, dpm: 0x049D, gy: 2016, gm: 10, gd: 2 },
{ hy: 1439, dpm: 0x095D, gy: 2017, gm: 9, gd: 21 },
{ hy: 1440, dpm: 0x02BA, gy: 2018, gm: 9, gd: 11 },
{ hy: 1441, dpm: 0x05B5, gy: 2019, gm: 8, gd: 31 },
{ hy: 1442, dpm: 0x05AA, gy: 2020, gm: 8, gd: 20 },
{ hy: 1443, dpm: 0x0D55, gy: 2021, gm: 8, gd: 9 },
{ hy: 1444, dpm: 0x0A9A, gy: 2022, gm: 7, gd: 30 },
{ hy: 1445, dpm: 0x092E, gy: 2023, gm: 7, gd: 19 },
{ hy: 1446, dpm: 0x026E, gy: 2024, gm: 7, gd: 7 },
{ hy: 1447, dpm: 0x055D, gy: 2025, gm: 6, gd: 26 },
{ hy: 1448, dpm: 0x0ADA, gy: 2026, gm: 6, gd: 16 },
{ hy: 1449, dpm: 0x06D4, gy: 2027, gm: 6, gd: 6 },
{ hy: 1450, dpm: 0x06A5, gy: 2028, gm: 5, gd: 25 },
{ hy: 1451, dpm: 0x054B, gy: 2029, gm: 5, gd: 14 },
{ hy: 1452, dpm: 0x0A97, gy: 2030, gm: 5, gd: 3 },
{ hy: 1453, dpm: 0x054E, gy: 2031, gm: 4, gd: 23 },
{ hy: 1454, dpm: 0x0AAE, gy: 2032, gm: 4, gd: 11 },
{ hy: 1455, dpm: 0x05AC, gy: 2033, gm: 4, gd: 1 },
{ hy: 1456, dpm: 0x0BA9, gy: 2034, gm: 3, gd: 21 },
{ hy: 1457, dpm: 0x0D92, gy: 2035, gm: 3, gd: 11 },
{ hy: 1458, dpm: 0x0B25, gy: 2036, gm: 2, gd: 28 },
{ hy: 1459, dpm: 0x064B, gy: 2037, gm: 2, gd: 16 },
{ hy: 1460, dpm: 0x0CAB, gy: 2038, gm: 2, gd: 5 },
{ hy: 1461, dpm: 0x055A, gy: 2039, gm: 1, gd: 26 },
{ hy: 1462, dpm: 0x0B55, gy: 2040, gm: 1, gd: 15 },
{ hy: 1463, dpm: 0x06D2, gy: 2041, gm: 1, gd: 4 },
{ hy: 1464, dpm: 0x0EA5, gy: 2041, gm: 12, gd: 24 },
{ hy: 1465, dpm: 0x0E4A, gy: 2042, gm: 12, gd: 14 },
{ hy: 1466, dpm: 0x0A95, gy: 2043, gm: 12, gd: 3 },
{ hy: 1467, dpm: 0x052D, gy: 2044, gm: 11, gd: 21 },
{ hy: 1468, dpm: 0x0AAD, gy: 2045, gm: 11, gd: 10 },
{ hy: 1469, dpm: 0x036C, gy: 2046, gm: 10, gd: 31 },
{ hy: 1470, dpm: 0x0759, gy: 2047, gm: 10, gd: 20 },
{ hy: 1471, dpm: 0x06D2, gy: 2048, gm: 10, gd: 9 },
{ hy: 1472, dpm: 0x0695, gy: 2049, gm: 9, gd: 28 },
{ hy: 1473, dpm: 0x052D, gy: 2050, gm: 9, gd: 17 },
{ hy: 1474, dpm: 0x0A5B, gy: 2051, gm: 9, gd: 6 },
{ hy: 1475, dpm: 0x04BA, gy: 2052, gm: 8, gd: 26 },
{ hy: 1476, dpm: 0x09BA, gy: 2053, gm: 8, gd: 15 },
{ hy: 1477, dpm: 0x03B4, gy: 2054, gm: 8, gd: 5 },
{ hy: 1478, dpm: 0x0B69, gy: 2055, gm: 7, gd: 25 },
{ hy: 1479, dpm: 0x0B52, gy: 2056, gm: 7, gd: 14 },
{ hy: 1480, dpm: 0x0AA6, gy: 2057, gm: 7, gd: 3 },
{ hy: 1481, dpm: 0x04B6, gy: 2058, gm: 6, gd: 22 },
{ hy: 1482, dpm: 0x096D, gy: 2059, gm: 6, gd: 11 },
{ hy: 1483, dpm: 0x02EC, gy: 2060, gm: 5, gd: 31 },
{ hy: 1484, dpm: 0x06D9, gy: 2061, gm: 5, gd: 20 },
{ hy: 1485, dpm: 0x0EB2, gy: 2062, gm: 5, gd: 10 },
{ hy: 1486, dpm: 0x0D54, gy: 2063, gm: 4, gd: 30 },
{ hy: 1487, dpm: 0x0D2A, gy: 2064, gm: 4, gd: 18 },
{ hy: 1488, dpm: 0x0A56, gy: 2065, gm: 4, gd: 7 },
{ hy: 1489, dpm: 0x04AE, gy: 2066, gm: 3, gd: 27 },
{ hy: 1490, dpm: 0x096D, gy: 2067, gm: 3, gd: 16 },
{ hy: 1491, dpm: 0x0D6A, gy: 2068, gm: 3, gd: 5 },
{ hy: 1492, dpm: 0x0B54, gy: 2069, gm: 2, gd: 23 },
{ hy: 1493, dpm: 0x0B29, gy: 2070, gm: 2, gd: 12 },
{ hy: 1494, dpm: 0x0A93, gy: 2071, gm: 2, gd: 1 },
{ hy: 1495, dpm: 0x052B, gy: 2072, gm: 1, gd: 21 },
{ hy: 1496, dpm: 0x0A57, gy: 2073, gm: 1, gd: 9 },
{ hy: 1497, dpm: 0x0536, gy: 2073, gm: 12, gd: 30 },
{ hy: 1498, dpm: 0x0AB5, gy: 2074, gm: 12, gd: 19 },
{ hy: 1499, dpm: 0x06AA, gy: 2075, gm: 12, gd: 9 },
{ hy: 1500, dpm: 0x0E93, gy: 2076, gm: 11, gd: 27 },
{ hy: 1501, dpm: 0, gy: 2077, gm: 11, gd: 17 }];
// hDates.ts — re-exports from hijri-core; table is maintained in the core package
export { hDatesTable } from 'hijri-core';
export type { HijriYearRecord } from 'hijri-core';

View file

@ -1,46 +1,2 @@
// hMonths.ts
// Hijri Month Names
export const hmLong = [
"Muharram", // 1st month
"Safar", // 2nd month
"Rabi'l Awwal", // 3rd month
"Rabi'l Thani", // 4th month
"Jumadal Awwal", // 5th month
"Jumadal Thani", // 6th month
"Rajab", // 7th month
"Sha'ban", // 8th month
"Ramadan", // 9th month
"Shawwal", // 10th month
"Dhul Qi'dah", // 11th month
"Dhul Hijjah" // 12th month
];
export const hmMedium = [
"Muharram",
"Safar",
"Rabi1",
"Rabi2",
"Jumada1",
"Jumada2",
"Rajab",
"Shaban",
"Ramadan",
"Shawwal",
"Dhul-Qidah",
"Dhul-Hijah"
];
export const hmShort = [
"Muh",
"Saf",
"Ra1",
"Ra2",
"Ju1",
"Ju2",
"Raj",
"Shb",
"Ram",
"Shw",
"DhQ",
"DhH"
];
// hMonths.ts — re-exports from hijri-core
export { hmLong, hmMedium, hmShort } from 'hijri-core';

View file

@ -1,25 +1,2 @@
// hWeekdays.ts
// Full names of Hijri weekdays
export const hwLong = [
"Yawm al-Ahad", // Sunday
"Yawm al-Ithnayn", // Monday
"Yawm ath-Thulatha'", // Tuesday
"Yawm al-Arba`a'", // Wednesday
"Yawm al-Khamis", // Thursday
"Yawm al-Jum`a", // Friday
"Yawm as-Sabt" // Saturday
];
// Abbreviated names of Hijri weekdays
export const hwShort = [
"Ahad", // Sunday
"Ithn", // Monday
"Thul", // Tuesday
"Arba", // Wednesday
"Kham", // Thursday
"Jum`a", // Friday
"Sabt" // Saturday
];
// Numeric representation of Hijri weekdays (1 for Sunday, 2 for Monday, etc.)
export const hwNumeric = [1, 2, 3, 4, 5, 6, 7];
// hWeekdays.ts — re-exports from hijri-core
export { hwLong, hwShort, hwNumeric } from 'hijri-core';

View file

@ -1,50 +1,9 @@
// toGregorian.ts
import { DateTime } from 'luxon';
import { hDatesTable } from './hDates';
import { fcnaToGregorian } from './fcna';
import { isValidHijriDate } from './utils';
// toGregorian.ts — thin wrapper over hijri-core; preserves throw-on-invalid behavior
import { toGregorian as coreToGregorian } from 'hijri-core';
import type { ConversionOptions } from './types';
export function toGregorian(hy: number, hm: number, hd: number, options?: ConversionOptions): Date | null {
if (options?.calendar === 'fcna') {
const result = fcnaToGregorian(hy, hm, hd);
if (result === null) throw new Error('Invalid Hijri date');
return result;
}
if (!isValidHijriDate(hy, hm, hd)) {
throw new Error('Invalid Hijri date');
}
// Binary search on hy (table is sorted ascending by Hijri year).
let lo = 0;
let hi = hDatesTable.length - 1;
let found = -1;
while (lo <= hi) {
const mid = (lo + hi) >>> 1;
const midHy = hDatesTable[mid].hy;
if (midHy === hy) {
found = mid;
break;
} else if (midHy < hy) {
lo = mid + 1;
} else {
hi = mid - 1;
}
}
if (found === -1) return null;
const record = hDatesTable[found];
let totalDays = 0;
for (let i = 0; i < hm - 1; i++) {
totalDays += (record.dpm >> i) & 1 ? 30 : 29;
}
totalDays += hd - 1;
const startDate = DateTime.utc(record.gy, record.gm, record.gd);
return startDate.plus({ days: totalDays }).toJSDate();
const result = coreToGregorian(hy, hm, hd, options);
if (result === null) throw new Error('Invalid Hijri date');
return result;
}

View file

@ -1,62 +1,2 @@
// toHijri.ts
import { hDatesTable } from './hDates';
import { fcnaToHijri } from './fcna';
import type { HijriDate, HijriYearRecord, ConversionOptions } from './types';
export function toHijri(gregorianDate: Date, options?: ConversionOptions): HijriDate | null {
if (options?.calendar === 'fcna') {
return fcnaToHijri(gregorianDate);
}
if (!(gregorianDate instanceof Date) || isNaN(gregorianDate.getTime())) {
throw new Error('Invalid Gregorian date');
}
// Normalize input to UTC midnight so comparisons are timezone-independent.
const inputUtc = Date.UTC(
gregorianDate.getFullYear(),
gregorianDate.getMonth(),
gregorianDate.getDate(),
);
// Binary search: find the last table entry whose Gregorian date <= input.
// Table is sorted ascending by (gy, gm, gd).
let lo = 0;
let hi = hDatesTable.length - 1;
let found = -1;
while (lo <= hi) {
const mid = (lo + hi) >>> 1;
const entry = hDatesTable[mid];
const entryUtc = Date.UTC(entry.gy, entry.gm - 1, entry.gd);
if (entryUtc <= inputUtc) {
found = mid;
lo = mid + 1;
} else {
hi = mid - 1;
}
}
// dpm === 0 means sentinel entry (marks end-of-table boundary, not a real year).
if (found === -1 || hDatesTable[found].dpm === 0) return null;
const record: HijriYearRecord = hDatesTable[found];
const startUtc = Date.UTC(record.gy, record.gm - 1, record.gd);
let remainingDays = Math.round((inputUtc - startUtc) / 86_400_000);
let hijriMonth = 0;
for (let i = 0; i < 12; i++) {
const daysInThisMonth = (record.dpm >> i) & 1 ? 30 : 29;
if (remainingDays < daysInThisMonth) {
hijriMonth = i + 1;
break;
}
remainingDays -= daysInThisMonth;
}
// hijriMonth remains 0 if the date falls beyond the last table entry's year.
if (hijriMonth === 0) return null;
return { hy: record.hy, hm: hijriMonth, hd: remainingDays + 1 };
}
// toHijri.ts — delegates to hijri-core
export { toHijri } from 'hijri-core';

View file

@ -1,23 +1,6 @@
// types.ts
export interface HijriDate {
hy: number; // Hijri year
hm: number; // Hijri month (112)
hd: number; // Hijri day (130)
}
// types.ts — re-exports from hijri-core for backward compatibility
export type { HijriDate, HijriYearRecord, ConversionOptions } from 'hijri-core';
export 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
}
// Calendar system selector.
// 'uaq' — Umm al-Qura (default): table-based, covers 13181500 H.
// 'fcna' — FCNA/ISNA: astronomical calculation, works for all Hijri years.
// CalendarSystem documents the built-in calendar identifiers.
// hijri-core accepts any string via registerCalendar(); this type covers the defaults.
export type CalendarSystem = 'uaq' | 'fcna';
export interface ConversionOptions {
calendar?: CalendarSystem;
}

View file

@ -1,37 +1,2 @@
// utils.ts
import { hDatesTable } from './hDates';
import { fcnaIsValid } from './fcna';
import type { ConversionOptions } from './types';
export function isValidHijriDate(hy: number, hm: number, hd: number, options?: ConversionOptions): boolean {
if (options?.calendar === 'fcna') {
return fcnaIsValid(hy, hm, hd);
}
// Binary search on hy.
let lo = 0;
let hi = hDatesTable.length - 1;
let found = -1;
while (lo <= hi) {
const mid = (lo + hi) >>> 1;
const midHy = hDatesTable[mid].hy;
if (midHy === hy) {
found = mid;
break;
} else if (midHy < hy) {
lo = mid + 1;
} else {
hi = mid - 1;
}
}
// dpm === 0 means sentinel entry (marks end-of-table boundary, not a real year).
if (found === -1 || hDatesTable[found].dpm === 0) return false;
const record = hDatesTable[found];
if (hm < 1 || hm > 12 || hd < 1) return false;
const daysInMonth = (record.dpm >> (hm - 1)) & 1 ? 30 : 29;
return hd <= daysInMonth;
}
// utils.ts — delegates to hijri-core
export { isValidHijriDate } from 'hijri-core';

View file

@ -10,7 +10,7 @@ export default defineConfig({
sourcemap: true,
target: 'es2020',
platform: 'node',
external: ['luxon'],
external: ['luxon', 'hijri-core'],
outExtension({ format }) {
return { js: format === 'cjs' ? '.cjs' : '.mjs' };
},