diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bc1190..7dc56cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,19 @@ # Changelog + + 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 diff --git a/package.json b/package.json index f6a597c..e39d66f 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0e018f4..3422c63 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: {} diff --git a/src/fcna.ts b/src/fcna.ts deleted file mode 100644 index 4ab4ca6..0000000 --- a/src/fcna.ts +++ /dev/null @@ -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 (1318–1500 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 1318–1500 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 1–12. - 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); -} diff --git a/src/hDates.ts b/src/hDates.ts index ae4318f..98bac91 100644 --- a/src/hDates.ts +++ b/src/hDates.ts @@ -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'; diff --git a/src/hMonths.ts b/src/hMonths.ts index 5daf92c..3c73ea1 100644 --- a/src/hMonths.ts +++ b/src/hMonths.ts @@ -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'; diff --git a/src/hWeekdays.ts b/src/hWeekdays.ts index 2f95171..3cbc1a6 100644 --- a/src/hWeekdays.ts +++ b/src/hWeekdays.ts @@ -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'; diff --git a/src/toGregorian.ts b/src/toGregorian.ts index 9e1d60e..4c5e36e 100644 --- a/src/toGregorian.ts +++ b/src/toGregorian.ts @@ -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; } diff --git a/src/toHijri.ts b/src/toHijri.ts index b2fb482..ca2b4f5 100644 --- a/src/toHijri.ts +++ b/src/toHijri.ts @@ -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'; diff --git a/src/types.ts b/src/types.ts index dfcc489..5967de3 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,23 +1,6 @@ -// types.ts -export interface HijriDate { - hy: number; // Hijri year - hm: number; // Hijri month (1–12) - hd: number; // Hijri day (1–30) -} +// 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 1318–1500 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; -} diff --git a/src/utils.ts b/src/utils.ts index 7899127..d40348d 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -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'; diff --git a/tsup.config.ts b/tsup.config.ts index 7f60ff6..a673d98 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -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' }; },