mirror of
https://github.com/acamarata/hijri-core.git
synced 2026-06-30 18:54:27 +00:00
feat: initial release of hijri-core v1.0.0
Zero-dependency Hijri calendar engine with pluggable calendar registry. Built-in Umm al-Qura (UAQ) and FCNA/ISNA calendar engines. Supports N additional calendars via registerCalendar(). Pure Date.UTC arithmetic, binary search on 184-entry UAQ table, Meeus Ch.49 new moon formula for FCNA. 39 ESM + 20 CJS tests passing. Dual CJS/ESM build with full TypeScript declarations.
This commit is contained in:
commit
bb326d071c
27 changed files with 2860 additions and 0 deletions
18
.editorconfig
Normal file
18
.editorconfig
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.{ts,js,json,yaml,yml,md}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.{c,h}]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
62
.github/workflows/ci.yml
vendored
Normal file
62
.github/workflows/ci.yml
vendored
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test (Node ${{ matrix.node }})
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node: [20, 22, 24]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm run build
|
||||
- run: node test.mjs
|
||||
- run: node test-cjs.cjs
|
||||
|
||||
typecheck:
|
||||
name: Typecheck
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm run typecheck
|
||||
|
||||
pack-check:
|
||||
name: Pack check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm run build
|
||||
- name: Verify pack contents
|
||||
run: |
|
||||
npm pack --dry-run 2>&1 | tee pack-output.txt
|
||||
grep "dist/index.cjs" pack-output.txt
|
||||
grep "dist/index.mjs" pack-output.txt
|
||||
grep "dist/index.d.ts" pack-output.txt
|
||||
grep "dist/index.d.mts" pack-output.txt
|
||||
grep "README.md" pack-output.txt
|
||||
grep "CHANGELOG.md" pack-output.txt
|
||||
grep "LICENSE" pack-output.txt
|
||||
22
.github/workflows/wiki-sync.yml
vendored
Normal file
22
.github/workflows/wiki-sync.yml
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
name: Wiki sync
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- '.wiki/**'
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
name: Sync .wiki to GitHub Wiki
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Sync wiki pages
|
||||
uses: nicenshtein/wiki-page-creator-action@v1
|
||||
env:
|
||||
GH_PAT: ${{ secrets.GH_PAT }}
|
||||
with:
|
||||
owner: acamarata
|
||||
repo-name: hijri-core
|
||||
md-folder: .wiki
|
||||
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
node_modules/
|
||||
dist/
|
||||
*.tgz
|
||||
*.log
|
||||
.DS_Store
|
||||
.claude/
|
||||
.env
|
||||
.env.*
|
||||
1
.npmrc
Normal file
1
.npmrc
Normal file
|
|
@ -0,0 +1 @@
|
|||
package-import-method=hardlink
|
||||
1
.nvmrc
Normal file
1
.nvmrc
Normal file
|
|
@ -0,0 +1 @@
|
|||
24
|
||||
161
.wiki/API-Reference.md
Normal file
161
.wiki/API-Reference.md
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
# API Reference
|
||||
|
||||
## Conversion functions
|
||||
|
||||
### `toHijri(date, options?)`
|
||||
|
||||
Converts a Gregorian `Date` to a Hijri date object.
|
||||
|
||||
```typescript
|
||||
function toHijri(date: Date, options?: ConversionOptions): HijriDate | null
|
||||
```
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `date` | `Date` | Gregorian date to convert |
|
||||
| `options.calendar` | `string` | Calendar name. Defaults to `'uaq'` |
|
||||
|
||||
Returns `HijriDate` or `null` if the date falls outside the calendar's supported range.
|
||||
|
||||
Throws `Error('Invalid Gregorian date')` if `date` is not a valid `Date` instance.
|
||||
|
||||
UAQ uses local date components (`getFullYear`, `getMonth`, `getDate`) for timezone-safe lookup. FCNA uses UTC components because its criterion is UTC-based.
|
||||
|
||||
### `toGregorian(hy, hm, hd, options?)`
|
||||
|
||||
Converts a Hijri date to a Gregorian `Date`.
|
||||
|
||||
```typescript
|
||||
function toGregorian(
|
||||
hy: number,
|
||||
hm: number,
|
||||
hd: number,
|
||||
options?: ConversionOptions
|
||||
): Date | null
|
||||
```
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `hy` | `number` | Hijri year |
|
||||
| `hm` | `number` | Hijri month (1-12) |
|
||||
| `hd` | `number` | Hijri day (1-30) |
|
||||
| `options.calendar` | `string` | Calendar name. Defaults to `'uaq'` |
|
||||
|
||||
Returns a UTC midnight `Date` or `null` if the input is out of range.
|
||||
|
||||
Throws `Error('Invalid Hijri date')` for UAQ when the date is not in the reference table.
|
||||
|
||||
### `isValidHijriDate(hy, hm, hd, options?)`
|
||||
|
||||
Returns `true` if the given Hijri date exists in the selected calendar.
|
||||
|
||||
```typescript
|
||||
function isValidHijriDate(
|
||||
hy: number,
|
||||
hm: number,
|
||||
hd: number,
|
||||
options?: ConversionOptions
|
||||
): boolean
|
||||
```
|
||||
|
||||
### `daysInHijriMonth(hy, hm, options?)`
|
||||
|
||||
Returns the number of days in a given Hijri month.
|
||||
|
||||
```typescript
|
||||
function daysInHijriMonth(hy: number, hm: number, options?: ConversionOptions): number
|
||||
```
|
||||
|
||||
Returns 29 or 30. Returns 0 for UAQ when the year is out of range.
|
||||
|
||||
## Registry functions
|
||||
|
||||
### `registerCalendar(name, engine)`
|
||||
|
||||
Registers a calendar engine. Overwrites any existing engine with the same name.
|
||||
|
||||
```typescript
|
||||
function registerCalendar(name: string, engine: CalendarEngine): void
|
||||
```
|
||||
|
||||
### `getCalendar(name)`
|
||||
|
||||
Returns the registered engine for a given name. Throws if not found.
|
||||
|
||||
```typescript
|
||||
function getCalendar(name: string): CalendarEngine
|
||||
```
|
||||
|
||||
Throws `Error('Unknown Hijri calendar: "name". Available: ...')`.
|
||||
|
||||
### `listCalendars()`
|
||||
|
||||
Returns the names of all registered calendars.
|
||||
|
||||
```typescript
|
||||
function listCalendars(): string[]
|
||||
```
|
||||
|
||||
## Types
|
||||
|
||||
### `HijriDate`
|
||||
|
||||
```typescript
|
||||
interface HijriDate {
|
||||
hy: number; // Hijri year
|
||||
hm: number; // Hijri month (1-12)
|
||||
hd: number; // Hijri day (1-30)
|
||||
}
|
||||
```
|
||||
|
||||
### `HijriYearRecord`
|
||||
|
||||
One entry in the Umm al-Qura reference table.
|
||||
|
||||
```typescript
|
||||
interface HijriYearRecord {
|
||||
hy: number; // Hijri year
|
||||
dpm: number; // 12-bit days-per-month bitmask
|
||||
gy: number; // Gregorian year of 1 Muharram
|
||||
gm: number; // Gregorian month of 1 Muharram (1-based)
|
||||
gd: number; // Gregorian day of 1 Muharram
|
||||
}
|
||||
```
|
||||
|
||||
The `dpm` bitmask encodes month lengths. Bit `i` (0-indexed from bit 0) corresponds to month `i+1`: `1` = 30 days, `0` = 29 days.
|
||||
|
||||
### `CalendarEngine`
|
||||
|
||||
```typescript
|
||||
interface CalendarEngine {
|
||||
readonly id: string;
|
||||
toHijri(date: Date): HijriDate | null;
|
||||
toGregorian(hy: number, hm: number, hd: number): Date | null;
|
||||
isValid(hy: number, hm: number, hd: number): boolean;
|
||||
daysInMonth(hy: number, hm: number): number;
|
||||
}
|
||||
```
|
||||
|
||||
### `ConversionOptions`
|
||||
|
||||
```typescript
|
||||
interface ConversionOptions {
|
||||
calendar?: string; // defaults to 'uaq'
|
||||
}
|
||||
```
|
||||
|
||||
## Data exports
|
||||
|
||||
| Export | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `hDatesTable` | `HijriYearRecord[]` | Full UAQ table, 184 entries (1318-1500 H) plus sentinel |
|
||||
| `hmLong` | `string[]` | Long month names. Index 0 = Muharram |
|
||||
| `hmMedium` | `string[]` | Medium month names |
|
||||
| `hmShort` | `string[]` | Short month codes (3 chars) |
|
||||
| `hwLong` | `string[]` | Long weekday names. Index 0 = Sunday |
|
||||
| `hwShort` | `string[]` | Short weekday names |
|
||||
| `hwNumeric` | `number[]` | Weekday numbers, 1 = Sunday, 7 = Saturday |
|
||||
|
||||
---
|
||||
|
||||
[Home](Home) | [Architecture](Architecture)
|
||||
137
.wiki/Architecture.md
Normal file
137
.wiki/Architecture.md
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
# Architecture
|
||||
|
||||
## Calendar registry pattern
|
||||
|
||||
The library separates the conversion API from calendar implementations. A registry maps string keys to engine objects. This allows adding N calendar systems without modifying core code.
|
||||
|
||||
```
|
||||
toHijri(date, { calendar: 'uaq' })
|
||||
|
|
||||
v
|
||||
getCalendar('uaq') <-- Map<string, CalendarEngine>
|
||||
|
|
||||
v
|
||||
uaqEngine.toHijri(date)
|
||||
```
|
||||
|
||||
The two built-in engines register at module load time. Custom engines register at any point before use.
|
||||
|
||||
## CalendarEngine interface
|
||||
|
||||
Every calendar engine must implement four methods:
|
||||
|
||||
```typescript
|
||||
interface CalendarEngine {
|
||||
readonly id: string;
|
||||
toHijri(date: Date): HijriDate | null;
|
||||
toGregorian(hy: number, hm: number, hd: number): Date | null;
|
||||
isValid(hy: number, hm: number, hd: number): boolean;
|
||||
daysInMonth(hy: number, hm: number): number;
|
||||
}
|
||||
```
|
||||
|
||||
Return `null` when a date is outside the engine's supported range. Throw `Error` when given structurally invalid input (malformed Date, or Hijri params that fail basic range checks).
|
||||
|
||||
## UAQ engine
|
||||
|
||||
The Umm al-Qura calendar is the official Islamic calendar of Saudi Arabia. Month start dates are computed by observation and committee decision, not by a fixed astronomical formula. The reference table encodes the result of those decisions.
|
||||
|
||||
### Data format
|
||||
|
||||
Each `HijriYearRecord` stores:
|
||||
- The Gregorian date of 1 Muharram for that Hijri year
|
||||
- A 12-bit `dpm` bitmask: bit `i` (0-indexed) = month length for month `i+1`. Bit 1 = 30 days, bit 0 = 29 days
|
||||
|
||||
The table covers Hijri years 1318-1500 (Gregorian 1900-2076). A sentinel entry at Hijri year 1501 with `dpm = 0` marks the upper boundary.
|
||||
|
||||
### toHijri (Gregorian to Hijri)
|
||||
|
||||
1. Normalize input to UTC midnight using local date components (`getFullYear`, `getMonth`, `getDate`). This makes the result independent of the host timezone.
|
||||
2. Binary search the table for the last entry whose Gregorian start date is before or on the input.
|
||||
3. The sentinel check (`dpm === 0`) rejects out-of-range inputs.
|
||||
4. Walk the `dpm` bitmask to consume months until the remaining day count falls within a single month.
|
||||
|
||||
### toGregorian (Hijri to Gregorian)
|
||||
|
||||
1. Validate with `uaqIsValid`.
|
||||
2. Binary search the table for the exact Hijri year.
|
||||
3. Sum month lengths from the `dpm` bitmask for months 1 through `hm - 1`.
|
||||
4. Add `hd - 1` to get total days offset.
|
||||
5. Return `new Date(Date.UTC(gy, gm - 1, gd) + totalDays * 86_400_000)`. Pure millisecond arithmetic, no date library needed.
|
||||
|
||||
### Binary search
|
||||
|
||||
Both `toHijri` and `toGregorian` use O(log n) binary search over the 184-entry table. The UAQ table is ordered by both Hijri year and Gregorian date (they are monotonically increasing together).
|
||||
|
||||
## FCNA engine
|
||||
|
||||
The Fiqh Council of North America uses a global astronomical criterion rather than local sighting or a pre-computed table.
|
||||
|
||||
### Criterion
|
||||
|
||||
If the new moon conjunction occurs before 12:00 noon UTC on calendar day D:
|
||||
- The new Hijri month begins at midnight starting day D+1.
|
||||
|
||||
If at or after 12:00 noon UTC:
|
||||
- The new Hijri month begins at midnight starting day D+2.
|
||||
|
||||
This makes every Hijri month start deterministic from the astronomical conjunction time.
|
||||
|
||||
### New moon calculation
|
||||
|
||||
New moon Julian Day Ephemeris (JDE) comes from Jean Meeus, _Astronomical Algorithms_, 2nd ed., Chapter 49. The formula includes the mean JDE (Eq. 49.1), eccentricity corrections, and 24 planetary perturbation terms. Accuracy is within a few minutes for 1000-3000 CE.
|
||||
|
||||
The k index in Meeus numbering counts synodic months from a reference point near 2000-01-06. The Islamic epoch anchor (`K_EPOCH = -17037`) maps Meeus k to Hijri month numbers.
|
||||
|
||||
### UAQ anchor
|
||||
|
||||
For Hijri years in the UAQ table (1318-1500 H), the engine uses the table's 1 Muharram date as the starting anchor for finding the nearest new moon. This gives FCNA and UAQ consistent alignment for those years. For years outside the table, the engine estimates the anchor from the Islamic epoch plus mean synodic month count.
|
||||
|
||||
### toHijri (FCNA)
|
||||
|
||||
1. Convert input to UTC midnight (FCNA criterion is UTC-based, so UTC components are correct).
|
||||
2. Estimate the approximate k index from 15 days before the input date.
|
||||
3. Search k0-1, k0, k0+1 for the FCNA month containing the input date.
|
||||
4. Map the k index back to a Hijri (hy, hm) pair via `K_EPOCH` offset.
|
||||
|
||||
### toGregorian (FCNA)
|
||||
|
||||
1. Compute the UAQ anchor for (hy, hm).
|
||||
2. Find the nearest new moon to that anchor.
|
||||
3. Apply the FCNA criterion to get the month start midnight.
|
||||
4. Add `hd - 1` days in milliseconds.
|
||||
|
||||
## Writing a custom engine
|
||||
|
||||
Minimal working example:
|
||||
|
||||
```typescript
|
||||
import { registerCalendar, type CalendarEngine } from 'hijri-core';
|
||||
|
||||
// A fixed-offset arithmetic calendar (not accurate — for illustration only).
|
||||
function hijriFromMs(ms: number) {
|
||||
const HIJRI_EPOCH_MS = -42521974440000; // approx
|
||||
const MEAN_MONTH_MS = 29.530588861 * 86_400_000;
|
||||
const months = Math.floor((ms - HIJRI_EPOCH_MS) / MEAN_MONTH_MS);
|
||||
const hy = Math.floor(months / 12) + 1;
|
||||
const hm = (months % 12) + 1;
|
||||
const hd = Math.floor(((ms - HIJRI_EPOCH_MS) % MEAN_MONTH_MS) / 86_400_000) + 1;
|
||||
return { hy, hm: hm <= 0 ? hm + 12 : hm, hd };
|
||||
}
|
||||
|
||||
const arithmeticEngine: CalendarEngine = {
|
||||
id: 'arithmetic',
|
||||
toHijri: (date) => hijriFromMs(date.getTime()),
|
||||
toGregorian: (hy, hm, hd) => null, // left as an exercise
|
||||
isValid: (hy, hm, hd) => hy > 0 && hm >= 1 && hm <= 12 && hd >= 1 && hd <= 30,
|
||||
daysInMonth: (hy, hm) => (hm % 2 === 1 || hm === 12) ? 30 : 29,
|
||||
};
|
||||
|
||||
registerCalendar('arithmetic', arithmeticEngine);
|
||||
```
|
||||
|
||||
Any engine registered this way works with all four convenience functions (`toHijri`, `toGregorian`, `isValidHijriDate`, `daysInHijriMonth`) by passing `{ calendar: 'arithmetic' }`.
|
||||
|
||||
---
|
||||
|
||||
[Home](Home) | [API Reference](API-Reference)
|
||||
53
.wiki/Home.md
Normal file
53
.wiki/Home.md
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
# hijri-core
|
||||
|
||||
Zero-dependency Hijri calendar engine for JavaScript and TypeScript.
|
||||
|
||||
Two built-in calendars: Umm al-Qura (UAQ) and FCNA/ISNA. Additional calendars can be registered at runtime through the engine registry.
|
||||
|
||||
## Quick example
|
||||
|
||||
```typescript
|
||||
import { toHijri, toGregorian } from 'hijri-core';
|
||||
|
||||
// UAQ (default)
|
||||
const hijri = toHijri(new Date(2025, 2, 1));
|
||||
// { hy: 1446, hm: 9, hd: 1 }
|
||||
|
||||
const greg = toGregorian(1446, 9, 1);
|
||||
// Date: 2025-03-01
|
||||
|
||||
// FCNA
|
||||
const hijriFcna = toHijri(new Date('2025-03-01'), { calendar: 'fcna' });
|
||||
const gregFcna = toGregorian(1446, 9, 1, { calendar: 'fcna' });
|
||||
```
|
||||
|
||||
## Custom calendar registration
|
||||
|
||||
```typescript
|
||||
import { registerCalendar, toHijri, type CalendarEngine } from 'hijri-core';
|
||||
|
||||
const myEngine: CalendarEngine = {
|
||||
id: 'my-calendar',
|
||||
toHijri: (date) => ({ hy: 1446, hm: 1, hd: 1 }),
|
||||
toGregorian: (hy, hm, hd) => new Date(),
|
||||
isValid: (hy, hm, hd) => hy > 0 && hm >= 1 && hm <= 12 && hd >= 1,
|
||||
daysInMonth: (hy, hm) => 30,
|
||||
};
|
||||
|
||||
registerCalendar('my-calendar', myEngine);
|
||||
toHijri(new Date(), { calendar: 'my-calendar' });
|
||||
```
|
||||
|
||||
## Key facts
|
||||
|
||||
- Zero dependencies
|
||||
- Dual CJS + ESM build
|
||||
- Full TypeScript declarations
|
||||
- Extensible calendar registry
|
||||
- UAQ table covers 1318-1500 H (1900-2076 CE)
|
||||
- FCNA engine works for any year using Meeus astronomical algorithms
|
||||
|
||||
## Pages
|
||||
|
||||
- [API Reference](API-Reference)
|
||||
- [Architecture](Architecture)
|
||||
25
CHANGELOG.md
Normal file
25
CHANGELOG.md
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
|
||||
## [1.0.0] - 2026-02-25
|
||||
|
||||
### Added
|
||||
|
||||
- UAQ (Umm al-Qura) calendar engine with binary search over a 184-year reference table (1318-1500 H / 1900-2076 CE)
|
||||
- FCNA/ISNA calendar engine based on Jean Meeus Chapter 49 new moon algorithm with the FCNA conjunction criterion
|
||||
- Calendar registry (`registerCalendar`, `getCalendar`, `listCalendars`) for pluggable calendar support
|
||||
- `toHijri(date, options?)` convenience wrapper
|
||||
- `toGregorian(hy, hm, hd, options?)` convenience wrapper
|
||||
- `isValidHijriDate(hy, hm, hd, options?)` validation function
|
||||
- `daysInHijriMonth(hy, hm, options?)` month-length function
|
||||
- `hDatesTable` export of the full Umm al-Qura data
|
||||
- `hmLong`, `hmMedium`, `hmShort` Hijri month name arrays
|
||||
- `hwLong`, `hwShort`, `hwNumeric` Hijri weekday name arrays
|
||||
- Full TypeScript type declarations for all exports
|
||||
- Dual CJS and ESM build (tsup)
|
||||
- `CalendarEngine` interface for writing custom calendar implementations
|
||||
- Node 20/22/24 CI matrix
|
||||
- GitHub Wiki with API reference and architecture documentation
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2024-2026 Aric Camarata
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
132
README.md
Normal file
132
README.md
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
# hijri-core
|
||||
|
||||
[](https://www.npmjs.com/package/hijri-core)
|
||||
[](https://github.com/acamarata/hijri-core/actions/workflows/ci.yml)
|
||||
[](LICENSE)
|
||||
|
||||
Zero-dependency Hijri calendar engine. Supports the Umm al-Qura (UAQ) and FCNA/ISNA calendars out of the box. Extensible via a calendar registry for custom implementations.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install hijri-core
|
||||
# or
|
||||
pnpm add hijri-core
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```typescript
|
||||
import { toHijri, toGregorian, isValidHijriDate, daysInHijriMonth } from 'hijri-core';
|
||||
|
||||
// Gregorian to Hijri (UAQ, default)
|
||||
const hijri = toHijri(new Date(2025, 2, 1));
|
||||
// { hy: 1446, hm: 9, hd: 1 }
|
||||
|
||||
// Hijri to Gregorian (UAQ)
|
||||
const greg = toGregorian(1446, 9, 1);
|
||||
// Date: 2025-03-01
|
||||
|
||||
// FCNA/ISNA calendar
|
||||
const hijriFcna = toHijri(new Date('2025-03-01'), { calendar: 'fcna' });
|
||||
const gregFcna = toGregorian(1446, 9, 1, { calendar: 'fcna' });
|
||||
|
||||
// Validation and month length
|
||||
isValidHijriDate(1444, 9, 1); // true
|
||||
daysInHijriMonth(1444, 9); // 29
|
||||
```
|
||||
|
||||
### Custom Calendar
|
||||
|
||||
```typescript
|
||||
import { registerCalendar, toHijri, type CalendarEngine } from 'hijri-core';
|
||||
|
||||
const myEngine: CalendarEngine = {
|
||||
id: 'my-calendar',
|
||||
toHijri: (date) => { /* your logic */ return { hy: 1446, hm: 1, hd: 1 }; },
|
||||
toGregorian: (hy, hm, hd) => { /* your logic */ return new Date(); },
|
||||
isValid: (hy, hm, hd) => hy > 0 && hm >= 1 && hm <= 12 && hd >= 1,
|
||||
daysInMonth: (hy, hm) => 30,
|
||||
};
|
||||
|
||||
registerCalendar('my-calendar', myEngine);
|
||||
|
||||
const result = toHijri(new Date(), { calendar: 'my-calendar' });
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
### Conversion functions
|
||||
|
||||
| Function | Parameters | Returns | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| `toHijri(date, opts?)` | `Date`, `ConversionOptions?` | `HijriDate \| null` | Throws on invalid Date |
|
||||
| `toGregorian(hy, hm, hd, opts?)` | `number, number, number, ConversionOptions?` | `Date \| null` | Throws on invalid Hijri |
|
||||
| `isValidHijriDate(hy, hm, hd, opts?)` | `number, number, number, ConversionOptions?` | `boolean` | |
|
||||
| `daysInHijriMonth(hy, hm, opts?)` | `number, number, ConversionOptions?` | `number` | |
|
||||
|
||||
`ConversionOptions.calendar` defaults to `'uaq'`. Pass `'fcna'` or any registered calendar name.
|
||||
|
||||
### Registry functions
|
||||
|
||||
| Function | Parameters | Returns |
|
||||
| --- | --- | --- |
|
||||
| `registerCalendar(name, engine)` | `string, CalendarEngine` | `void` |
|
||||
| `getCalendar(name)` | `string` | `CalendarEngine` |
|
||||
| `listCalendars()` | none | `string[]` |
|
||||
|
||||
### Data exports
|
||||
|
||||
| Export | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `hDatesTable` | `HijriYearRecord[]` | Full Umm al-Qura reference table (184 entries) |
|
||||
| `hmLong` | `string[]` | Long month names (e.g., "Ramadan") |
|
||||
| `hmMedium` | `string[]` | Medium month names (e.g., "Ramadan") |
|
||||
| `hmShort` | `string[]` | Short month names (e.g., "Ram") |
|
||||
| `hwLong` | `string[]` | Long weekday names |
|
||||
| `hwShort` | `string[]` | Short weekday names |
|
||||
| `hwNumeric` | `number[]` | Weekday numbers (1 = Sunday) |
|
||||
|
||||
## Custom Calendars
|
||||
|
||||
Implement the `CalendarEngine` interface to add any calendar system:
|
||||
|
||||
```typescript
|
||||
interface CalendarEngine {
|
||||
readonly id: string;
|
||||
toHijri(date: Date): HijriDate | null;
|
||||
toGregorian(hy: number, hm: number, hd: number): Date | null;
|
||||
isValid(hy: number, hm: number, hd: number): boolean;
|
||||
daysInMonth(hy: number, hm: number): number;
|
||||
}
|
||||
```
|
||||
|
||||
Register with `registerCalendar('my-id', myEngine)`. Then pass `{ calendar: 'my-id' }` to any conversion function.
|
||||
|
||||
## Compatibility
|
||||
|
||||
- Node.js 20, 22, 24
|
||||
- Modern browsers (ESM build)
|
||||
- CommonJS and ESM
|
||||
|
||||
## TypeScript
|
||||
|
||||
```typescript
|
||||
import type { HijriDate, HijriYearRecord, CalendarEngine, ConversionOptions } from 'hijri-core';
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
Full API reference and architecture notes: [GitHub Wiki](https://github.com/acamarata/hijri-core/wiki)
|
||||
|
||||
## Related
|
||||
|
||||
- [luxon-hijri](https://github.com/acamarata/luxon-hijri) - Hijri formatting with Luxon
|
||||
- [dayjs-hijri-plus](https://github.com/gmbh/dayjs-hijri-plus) - Day.js Hijri plugin
|
||||
- [date-fns-hijri](https://github.com/edisdev/date-fns-hijri) - date-fns Hijri helpers
|
||||
- [moment-hijri-plus](https://github.com/moment/moment) - Moment.js Hijri plugin
|
||||
- [temporal-hijri](https://github.com/acamarata/temporal-hijri) - Temporal API Hijri support
|
||||
|
||||
## License
|
||||
|
||||
MIT. Copyright (c) 2024-2026 Aric Camarata.
|
||||
57
package.json
Normal file
57
package.json
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
{
|
||||
"name": "hijri-core",
|
||||
"version": "1.0.0",
|
||||
"description": "Zero-dependency Hijri calendar engine with pluggable calendar support. Includes Umm al-Qura (UAQ) and FCNA/ISNA calendars. Extensible registry for custom calendars.",
|
||||
"author": "Aric Camarata",
|
||||
"license": "MIT",
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": { "types": "./dist/index.d.mts", "default": "./dist/index.mjs" },
|
||||
"require": { "types": "./dist/index.d.ts", "default": "./dist/index.cjs" }
|
||||
}
|
||||
},
|
||||
"sideEffects": false,
|
||||
"files": [
|
||||
"dist/index.cjs",
|
||||
"dist/index.mjs",
|
||||
"dist/index.d.ts",
|
||||
"dist/index.d.mts",
|
||||
"README.md",
|
||||
"CHANGELOG.md",
|
||||
"LICENSE"
|
||||
],
|
||||
"engines": { "node": ">=20" },
|
||||
"packageManager": "pnpm@10.30.1",
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"pretest": "tsup",
|
||||
"test": "node test.mjs && node test-cjs.cjs",
|
||||
"prepublishOnly": "tsup"
|
||||
},
|
||||
"keywords": [
|
||||
"hijri",
|
||||
"islamic",
|
||||
"calendar",
|
||||
"umm-al-qura",
|
||||
"uaq",
|
||||
"fcna",
|
||||
"isna",
|
||||
"gregorian",
|
||||
"converter",
|
||||
"typescript"
|
||||
],
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.0",
|
||||
"tsup": "^8.0.0",
|
||||
"typescript": "^5.5.0"
|
||||
},
|
||||
"publishConfig": { "access": "public", "registry": "https://registry.npmjs.org/" },
|
||||
"repository": { "type": "git", "url": "git+https://github.com/acamarata/hijri-core.git" },
|
||||
"homepage": "https://github.com/acamarata/hijri-core#readme",
|
||||
"bugs": { "url": "https://github.com/acamarata/hijri-core/issues" }
|
||||
}
|
||||
925
pnpm-lock.yaml
Normal file
925
pnpm-lock.yaml
Normal file
|
|
@ -0,0 +1,925 @@
|
|||
lockfileVersion: '9.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^22.0.0
|
||||
version: 22.19.11
|
||||
tsup:
|
||||
specifier: ^8.0.0
|
||||
version: 8.5.1(typescript@5.9.3)
|
||||
typescript:
|
||||
specifier: ^5.5.0
|
||||
version: 5.9.3
|
||||
|
||||
packages:
|
||||
|
||||
'@esbuild/aix-ppc64@0.27.3':
|
||||
resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [aix]
|
||||
|
||||
'@esbuild/android-arm64@0.27.3':
|
||||
resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-arm@0.27.3':
|
||||
resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-x64@0.27.3':
|
||||
resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/darwin-arm64@0.27.3':
|
||||
resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/darwin-x64@0.27.3':
|
||||
resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/freebsd-arm64@0.27.3':
|
||||
resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/freebsd-x64@0.27.3':
|
||||
resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/linux-arm64@0.27.3':
|
||||
resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-arm@0.27.3':
|
||||
resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ia32@0.27.3':
|
||||
resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-loong64@0.27.3':
|
||||
resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-mips64el@0.27.3':
|
||||
resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [mips64el]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ppc64@0.27.3':
|
||||
resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-riscv64@0.27.3':
|
||||
resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-s390x@0.27.3':
|
||||
resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-x64@0.27.3':
|
||||
resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/netbsd-arm64@0.27.3':
|
||||
resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/netbsd-x64@0.27.3':
|
||||
resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/openbsd-arm64@0.27.3':
|
||||
resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/openbsd-x64@0.27.3':
|
||||
resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/openharmony-arm64@0.27.3':
|
||||
resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [openharmony]
|
||||
|
||||
'@esbuild/sunos-x64@0.27.3':
|
||||
resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [sunos]
|
||||
|
||||
'@esbuild/win32-arm64@0.27.3':
|
||||
resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-ia32@0.27.3':
|
||||
resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-x64@0.27.3':
|
||||
resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@jridgewell/gen-mapping@0.3.13':
|
||||
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
|
||||
|
||||
'@jridgewell/resolve-uri@3.1.2':
|
||||
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
'@jridgewell/sourcemap-codec@1.5.5':
|
||||
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
|
||||
|
||||
'@jridgewell/trace-mapping@0.3.31':
|
||||
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
|
||||
|
||||
'@rollup/rollup-android-arm-eabi@4.59.0':
|
||||
resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
|
||||
'@rollup/rollup-android-arm64@4.59.0':
|
||||
resolution: {integrity: sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@rollup/rollup-darwin-arm64@4.59.0':
|
||||
resolution: {integrity: sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@rollup/rollup-darwin-x64@4.59.0':
|
||||
resolution: {integrity: sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@rollup/rollup-freebsd-arm64@4.59.0':
|
||||
resolution: {integrity: sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==}
|
||||
cpu: [arm64]
|
||||
os: [freebsd]
|
||||
|
||||
'@rollup/rollup-freebsd-x64@4.59.0':
|
||||
resolution: {integrity: sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@rollup/rollup-linux-arm-gnueabihf@4.59.0':
|
||||
resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.59.0':
|
||||
resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.59.0':
|
||||
resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.59.0':
|
||||
resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-loong64-gnu@4.59.0':
|
||||
resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-loong64-musl@4.59.0':
|
||||
resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-ppc64-gnu@4.59.0':
|
||||
resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-ppc64-musl@4.59.0':
|
||||
resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.59.0':
|
||||
resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-musl@4.59.0':
|
||||
resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-s390x-gnu@4.59.0':
|
||||
resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.59.0':
|
||||
resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.59.0':
|
||||
resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-openbsd-x64@4.59.0':
|
||||
resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==}
|
||||
cpu: [x64]
|
||||
os: [openbsd]
|
||||
|
||||
'@rollup/rollup-openharmony-arm64@4.59.0':
|
||||
resolution: {integrity: sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==}
|
||||
cpu: [arm64]
|
||||
os: [openharmony]
|
||||
|
||||
'@rollup/rollup-win32-arm64-msvc@4.59.0':
|
||||
resolution: {integrity: sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@rollup/rollup-win32-ia32-msvc@4.59.0':
|
||||
resolution: {integrity: sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@rollup/rollup-win32-x64-gnu@4.59.0':
|
||||
resolution: {integrity: sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@rollup/rollup-win32-x64-msvc@4.59.0':
|
||||
resolution: {integrity: sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@types/estree@1.0.8':
|
||||
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
||||
|
||||
'@types/node@22.19.11':
|
||||
resolution: {integrity: sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==}
|
||||
|
||||
acorn@8.16.0:
|
||||
resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
hasBin: true
|
||||
|
||||
any-promise@1.3.0:
|
||||
resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==}
|
||||
|
||||
bundle-require@5.1.0:
|
||||
resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
peerDependencies:
|
||||
esbuild: '>=0.18'
|
||||
|
||||
cac@6.7.14:
|
||||
resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
chokidar@4.0.3:
|
||||
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
|
||||
engines: {node: '>= 14.16.0'}
|
||||
|
||||
commander@4.1.1:
|
||||
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
confbox@0.1.8:
|
||||
resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==}
|
||||
|
||||
consola@3.4.2:
|
||||
resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==}
|
||||
engines: {node: ^14.18.0 || >=16.10.0}
|
||||
|
||||
debug@4.4.3:
|
||||
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
|
||||
engines: {node: '>=6.0'}
|
||||
peerDependencies:
|
||||
supports-color: '*'
|
||||
peerDependenciesMeta:
|
||||
supports-color:
|
||||
optional: true
|
||||
|
||||
esbuild@0.27.3:
|
||||
resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
fdir@6.5.0:
|
||||
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
peerDependencies:
|
||||
picomatch: ^3 || ^4
|
||||
peerDependenciesMeta:
|
||||
picomatch:
|
||||
optional: true
|
||||
|
||||
fix-dts-default-cjs-exports@1.0.1:
|
||||
resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==}
|
||||
|
||||
fsevents@2.3.3:
|
||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
os: [darwin]
|
||||
|
||||
joycon@3.1.1:
|
||||
resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
lilconfig@3.1.3:
|
||||
resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
lines-and-columns@1.2.4:
|
||||
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
|
||||
|
||||
load-tsconfig@0.2.5:
|
||||
resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
|
||||
magic-string@0.30.21:
|
||||
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
||||
|
||||
mlly@1.8.0:
|
||||
resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==}
|
||||
|
||||
ms@2.1.3:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
|
||||
mz@2.7.0:
|
||||
resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
|
||||
|
||||
object-assign@4.1.1:
|
||||
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
pathe@2.0.3:
|
||||
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
|
||||
|
||||
picocolors@1.1.1:
|
||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||
|
||||
picomatch@4.0.3:
|
||||
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
pirates@4.0.7:
|
||||
resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
pkg-types@1.3.1:
|
||||
resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==}
|
||||
|
||||
postcss-load-config@6.0.1:
|
||||
resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==}
|
||||
engines: {node: '>= 18'}
|
||||
peerDependencies:
|
||||
jiti: '>=1.21.0'
|
||||
postcss: '>=8.0.9'
|
||||
tsx: ^4.8.1
|
||||
yaml: ^2.4.2
|
||||
peerDependenciesMeta:
|
||||
jiti:
|
||||
optional: true
|
||||
postcss:
|
||||
optional: true
|
||||
tsx:
|
||||
optional: true
|
||||
yaml:
|
||||
optional: true
|
||||
|
||||
readdirp@4.1.2:
|
||||
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
|
||||
engines: {node: '>= 14.18.0'}
|
||||
|
||||
resolve-from@5.0.0:
|
||||
resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
rollup@4.59.0:
|
||||
resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==}
|
||||
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
||||
hasBin: true
|
||||
|
||||
source-map@0.7.6:
|
||||
resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==}
|
||||
engines: {node: '>= 12'}
|
||||
|
||||
sucrase@3.35.1:
|
||||
resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
hasBin: true
|
||||
|
||||
thenify-all@1.6.0:
|
||||
resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
|
||||
engines: {node: '>=0.8'}
|
||||
|
||||
thenify@3.3.1:
|
||||
resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
|
||||
|
||||
tinyexec@0.3.2:
|
||||
resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==}
|
||||
|
||||
tinyglobby@0.2.15:
|
||||
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
tree-kill@1.2.2:
|
||||
resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
|
||||
hasBin: true
|
||||
|
||||
ts-interface-checker@0.1.13:
|
||||
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
|
||||
|
||||
tsup@8.5.1:
|
||||
resolution: {integrity: sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@microsoft/api-extractor': ^7.36.0
|
||||
'@swc/core': ^1
|
||||
postcss: ^8.4.12
|
||||
typescript: '>=4.5.0'
|
||||
peerDependenciesMeta:
|
||||
'@microsoft/api-extractor':
|
||||
optional: true
|
||||
'@swc/core':
|
||||
optional: true
|
||||
postcss:
|
||||
optional: true
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
typescript@5.9.3:
|
||||
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
|
||||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
|
||||
ufo@1.6.3:
|
||||
resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==}
|
||||
|
||||
undici-types@6.21.0:
|
||||
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
||||
|
||||
snapshots:
|
||||
|
||||
'@esbuild/aix-ppc64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-x64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-arm64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-x64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-arm64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-x64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ia32@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-loong64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-mips64el@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ppc64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-riscv64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-s390x@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-x64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-arm64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-x64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-arm64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-x64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openharmony-arm64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/sunos-x64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-arm64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-ia32@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-x64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@jridgewell/gen-mapping@0.3.13':
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
'@jridgewell/trace-mapping': 0.3.31
|
||||
|
||||
'@jridgewell/resolve-uri@3.1.2': {}
|
||||
|
||||
'@jridgewell/sourcemap-codec@1.5.5': {}
|
||||
|
||||
'@jridgewell/trace-mapping@0.3.31':
|
||||
dependencies:
|
||||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
'@rollup/rollup-android-arm-eabi@4.59.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-android-arm64@4.59.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-darwin-arm64@4.59.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-darwin-x64@4.59.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-freebsd-arm64@4.59.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-freebsd-x64@4.59.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-arm-gnueabihf@4.59.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.59.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.59.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.59.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-loong64-gnu@4.59.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-loong64-musl@4.59.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-ppc64-gnu@4.59.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-ppc64-musl@4.59.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.59.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-riscv64-musl@4.59.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-s390x-gnu@4.59.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.59.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.59.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-openbsd-x64@4.59.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-openharmony-arm64@4.59.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-win32-arm64-msvc@4.59.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-win32-ia32-msvc@4.59.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-win32-x64-gnu@4.59.0':
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-win32-x64-msvc@4.59.0':
|
||||
optional: true
|
||||
|
||||
'@types/estree@1.0.8': {}
|
||||
|
||||
'@types/node@22.19.11':
|
||||
dependencies:
|
||||
undici-types: 6.21.0
|
||||
|
||||
acorn@8.16.0: {}
|
||||
|
||||
any-promise@1.3.0: {}
|
||||
|
||||
bundle-require@5.1.0(esbuild@0.27.3):
|
||||
dependencies:
|
||||
esbuild: 0.27.3
|
||||
load-tsconfig: 0.2.5
|
||||
|
||||
cac@6.7.14: {}
|
||||
|
||||
chokidar@4.0.3:
|
||||
dependencies:
|
||||
readdirp: 4.1.2
|
||||
|
||||
commander@4.1.1: {}
|
||||
|
||||
confbox@0.1.8: {}
|
||||
|
||||
consola@3.4.2: {}
|
||||
|
||||
debug@4.4.3:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
|
||||
esbuild@0.27.3:
|
||||
optionalDependencies:
|
||||
'@esbuild/aix-ppc64': 0.27.3
|
||||
'@esbuild/android-arm': 0.27.3
|
||||
'@esbuild/android-arm64': 0.27.3
|
||||
'@esbuild/android-x64': 0.27.3
|
||||
'@esbuild/darwin-arm64': 0.27.3
|
||||
'@esbuild/darwin-x64': 0.27.3
|
||||
'@esbuild/freebsd-arm64': 0.27.3
|
||||
'@esbuild/freebsd-x64': 0.27.3
|
||||
'@esbuild/linux-arm': 0.27.3
|
||||
'@esbuild/linux-arm64': 0.27.3
|
||||
'@esbuild/linux-ia32': 0.27.3
|
||||
'@esbuild/linux-loong64': 0.27.3
|
||||
'@esbuild/linux-mips64el': 0.27.3
|
||||
'@esbuild/linux-ppc64': 0.27.3
|
||||
'@esbuild/linux-riscv64': 0.27.3
|
||||
'@esbuild/linux-s390x': 0.27.3
|
||||
'@esbuild/linux-x64': 0.27.3
|
||||
'@esbuild/netbsd-arm64': 0.27.3
|
||||
'@esbuild/netbsd-x64': 0.27.3
|
||||
'@esbuild/openbsd-arm64': 0.27.3
|
||||
'@esbuild/openbsd-x64': 0.27.3
|
||||
'@esbuild/openharmony-arm64': 0.27.3
|
||||
'@esbuild/sunos-x64': 0.27.3
|
||||
'@esbuild/win32-arm64': 0.27.3
|
||||
'@esbuild/win32-ia32': 0.27.3
|
||||
'@esbuild/win32-x64': 0.27.3
|
||||
|
||||
fdir@6.5.0(picomatch@4.0.3):
|
||||
optionalDependencies:
|
||||
picomatch: 4.0.3
|
||||
|
||||
fix-dts-default-cjs-exports@1.0.1:
|
||||
dependencies:
|
||||
magic-string: 0.30.21
|
||||
mlly: 1.8.0
|
||||
rollup: 4.59.0
|
||||
|
||||
fsevents@2.3.3:
|
||||
optional: true
|
||||
|
||||
joycon@3.1.1: {}
|
||||
|
||||
lilconfig@3.1.3: {}
|
||||
|
||||
lines-and-columns@1.2.4: {}
|
||||
|
||||
load-tsconfig@0.2.5: {}
|
||||
|
||||
magic-string@0.30.21:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
mlly@1.8.0:
|
||||
dependencies:
|
||||
acorn: 8.16.0
|
||||
pathe: 2.0.3
|
||||
pkg-types: 1.3.1
|
||||
ufo: 1.6.3
|
||||
|
||||
ms@2.1.3: {}
|
||||
|
||||
mz@2.7.0:
|
||||
dependencies:
|
||||
any-promise: 1.3.0
|
||||
object-assign: 4.1.1
|
||||
thenify-all: 1.6.0
|
||||
|
||||
object-assign@4.1.1: {}
|
||||
|
||||
pathe@2.0.3: {}
|
||||
|
||||
picocolors@1.1.1: {}
|
||||
|
||||
picomatch@4.0.3: {}
|
||||
|
||||
pirates@4.0.7: {}
|
||||
|
||||
pkg-types@1.3.1:
|
||||
dependencies:
|
||||
confbox: 0.1.8
|
||||
mlly: 1.8.0
|
||||
pathe: 2.0.3
|
||||
|
||||
postcss-load-config@6.0.1:
|
||||
dependencies:
|
||||
lilconfig: 3.1.3
|
||||
|
||||
readdirp@4.1.2: {}
|
||||
|
||||
resolve-from@5.0.0: {}
|
||||
|
||||
rollup@4.59.0:
|
||||
dependencies:
|
||||
'@types/estree': 1.0.8
|
||||
optionalDependencies:
|
||||
'@rollup/rollup-android-arm-eabi': 4.59.0
|
||||
'@rollup/rollup-android-arm64': 4.59.0
|
||||
'@rollup/rollup-darwin-arm64': 4.59.0
|
||||
'@rollup/rollup-darwin-x64': 4.59.0
|
||||
'@rollup/rollup-freebsd-arm64': 4.59.0
|
||||
'@rollup/rollup-freebsd-x64': 4.59.0
|
||||
'@rollup/rollup-linux-arm-gnueabihf': 4.59.0
|
||||
'@rollup/rollup-linux-arm-musleabihf': 4.59.0
|
||||
'@rollup/rollup-linux-arm64-gnu': 4.59.0
|
||||
'@rollup/rollup-linux-arm64-musl': 4.59.0
|
||||
'@rollup/rollup-linux-loong64-gnu': 4.59.0
|
||||
'@rollup/rollup-linux-loong64-musl': 4.59.0
|
||||
'@rollup/rollup-linux-ppc64-gnu': 4.59.0
|
||||
'@rollup/rollup-linux-ppc64-musl': 4.59.0
|
||||
'@rollup/rollup-linux-riscv64-gnu': 4.59.0
|
||||
'@rollup/rollup-linux-riscv64-musl': 4.59.0
|
||||
'@rollup/rollup-linux-s390x-gnu': 4.59.0
|
||||
'@rollup/rollup-linux-x64-gnu': 4.59.0
|
||||
'@rollup/rollup-linux-x64-musl': 4.59.0
|
||||
'@rollup/rollup-openbsd-x64': 4.59.0
|
||||
'@rollup/rollup-openharmony-arm64': 4.59.0
|
||||
'@rollup/rollup-win32-arm64-msvc': 4.59.0
|
||||
'@rollup/rollup-win32-ia32-msvc': 4.59.0
|
||||
'@rollup/rollup-win32-x64-gnu': 4.59.0
|
||||
'@rollup/rollup-win32-x64-msvc': 4.59.0
|
||||
fsevents: 2.3.3
|
||||
|
||||
source-map@0.7.6: {}
|
||||
|
||||
sucrase@3.35.1:
|
||||
dependencies:
|
||||
'@jridgewell/gen-mapping': 0.3.13
|
||||
commander: 4.1.1
|
||||
lines-and-columns: 1.2.4
|
||||
mz: 2.7.0
|
||||
pirates: 4.0.7
|
||||
tinyglobby: 0.2.15
|
||||
ts-interface-checker: 0.1.13
|
||||
|
||||
thenify-all@1.6.0:
|
||||
dependencies:
|
||||
thenify: 3.3.1
|
||||
|
||||
thenify@3.3.1:
|
||||
dependencies:
|
||||
any-promise: 1.3.0
|
||||
|
||||
tinyexec@0.3.2: {}
|
||||
|
||||
tinyglobby@0.2.15:
|
||||
dependencies:
|
||||
fdir: 6.5.0(picomatch@4.0.3)
|
||||
picomatch: 4.0.3
|
||||
|
||||
tree-kill@1.2.2: {}
|
||||
|
||||
ts-interface-checker@0.1.13: {}
|
||||
|
||||
tsup@8.5.1(typescript@5.9.3):
|
||||
dependencies:
|
||||
bundle-require: 5.1.0(esbuild@0.27.3)
|
||||
cac: 6.7.14
|
||||
chokidar: 4.0.3
|
||||
consola: 3.4.2
|
||||
debug: 4.4.3
|
||||
esbuild: 0.27.3
|
||||
fix-dts-default-cjs-exports: 1.0.1
|
||||
joycon: 3.1.1
|
||||
picocolors: 1.1.1
|
||||
postcss-load-config: 6.0.1
|
||||
resolve-from: 5.0.0
|
||||
rollup: 4.59.0
|
||||
source-map: 0.7.6
|
||||
sucrase: 3.35.1
|
||||
tinyexec: 0.3.2
|
||||
tinyglobby: 0.2.15
|
||||
tree-kill: 1.2.2
|
||||
optionalDependencies:
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- jiti
|
||||
- supports-color
|
||||
- tsx
|
||||
- yaml
|
||||
|
||||
typescript@5.9.3: {}
|
||||
|
||||
ufo@1.6.3: {}
|
||||
|
||||
undici-types@6.21.0: {}
|
||||
2
pnpm-workspace.yaml
Normal file
2
pnpm-workspace.yaml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
onlyBuiltDependencies:
|
||||
- esbuild
|
||||
192
src/data/hDates.ts
Normal file
192
src/data/hDates.ts
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
import type { HijriYearRecord } from '../types';
|
||||
|
||||
// Umm al-Qura reference table: Hijri years 1318-1501.
|
||||
// Each entry records the 1 Muharram Gregorian date and a 12-bit days-per-month
|
||||
// bitmask. Bit i (0-indexed from bit 0) corresponds to month i+1: 1 = 30 days,
|
||||
// 0 = 29 days. The final sentinel entry (hy 1501, dpm 0) marks the upper bound.
|
||||
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 },
|
||||
];
|
||||
283
src/engines/fcna.ts
Normal file
283
src/engines/fcna.ts
Normal file
|
|
@ -0,0 +1,283 @@
|
|||
// FCNA engine: Fiqh Council of North America / ISNA calendar.
|
||||
//
|
||||
// The FCNA 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. If at or
|
||||
// after 12:00 UTC, the month begins at midnight starting day D+2.
|
||||
//
|
||||
// New moon times come from Jean Meeus, Astronomical Algorithms (2nd ed.),
|
||||
// Chapter 49, accurate to within a few minutes for 1000-3000 CE.
|
||||
|
||||
import { hDatesTable } from '../data/hDates';
|
||||
import type { CalendarEngine, 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.
|
||||
// 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;
|
||||
|
||||
let jde = JDE0
|
||||
+ SYNODIC * k
|
||||
+ 0.00015437 * T2
|
||||
- 0.000000150 * T3
|
||||
+ 0.00000000073 * T4;
|
||||
|
||||
const M = (2.5534
|
||||
+ 29.10535670 * k
|
||||
- 0.0000014 * T2
|
||||
- 0.00000011 * T3) % 360;
|
||||
|
||||
const Mprime = (201.5643
|
||||
+ 385.81693528 * k
|
||||
+ 0.0107582 * T2
|
||||
+ 0.00001238 * T3
|
||||
- 0.000000058 * T4) % 360;
|
||||
|
||||
const F = (160.7108
|
||||
+ 390.67050284 * k
|
||||
- 0.0016118 * T2
|
||||
- 0.00000227 * T3
|
||||
+ 0.000000011 * T4) % 360;
|
||||
|
||||
const Omega = (124.7746
|
||||
- 1.56375588 * k
|
||||
+ 0.0020672 * T2
|
||||
+ 0.00000215 * T3) % 360;
|
||||
|
||||
const E = 1 - 0.002516 * T - 0.0000074 * T2;
|
||||
const E2 = E * E;
|
||||
|
||||
const Mrad = M * TO_RAD;
|
||||
const Mprad = Mprime * TO_RAD;
|
||||
const Frad = F * TO_RAD;
|
||||
const Orad = Omega * TO_RAD;
|
||||
|
||||
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);
|
||||
|
||||
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 ─────────────────────────────────────────
|
||||
|
||||
// Searches k0-2 through k0+2 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 ──────────────────────────────────────────────────────────
|
||||
|
||||
// Returns the midnight UTC ms that starts the new FCNA Hijri month.
|
||||
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 ──────────────────────────────────────────────────────────────
|
||||
|
||||
// Returns the UTC ms of the UAQ month start for (hy, hm).
|
||||
// In-range years (1318-1500 H): binary-search table, sum dpm day counts.
|
||||
// Out-of-range years: estimate from Islamic epoch + mean synodic month count.
|
||||
function uaqAnchorMs(hy: number, hm: number): number {
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
|
||||
const monthsFromEpoch = (hy - 1) * 12 + (hm - 1);
|
||||
const kApprox = K_EPOCH + monthsFromEpoch;
|
||||
return jdeToUtcMs(newMoonJDE(kApprox));
|
||||
}
|
||||
|
||||
// ─── FCNA month start ─────────────────────────────────────────────────────────
|
||||
|
||||
function fcnaMonthStartMs(hy: number, hm: number): number {
|
||||
const anchor = uaqAnchorMs(hy, hm);
|
||||
const conjMs = nearestNewMoonMs(anchor);
|
||||
return fcnaCriterionMs(conjMs);
|
||||
}
|
||||
|
||||
// ─── FCNA month length ───────────────────────────────────────────────────────
|
||||
|
||||
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 ──────────────────────────────────────────────────
|
||||
|
||||
function fcnaToHijri(gregorianDate: Date): HijriDate | null {
|
||||
if (!(gregorianDate instanceof Date) || isNaN(gregorianDate.getTime())) {
|
||||
throw new Error('Invalid Gregorian date');
|
||||
}
|
||||
|
||||
// FCNA criterion is UTC-based, so UTC date components ensure correct round-trips.
|
||||
const inputMs = Date.UTC(
|
||||
gregorianDate.getUTCFullYear(),
|
||||
gregorianDate.getUTCMonth(),
|
||||
gregorianDate.getUTCDate(),
|
||||
);
|
||||
|
||||
const kApprox = utcMsToKApprox(inputMs - 15 * MS_PER_DAY);
|
||||
const k0 = Math.floor(kApprox);
|
||||
|
||||
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) {
|
||||
const monthsFromEpoch = ki - K_EPOCH;
|
||||
let hy = Math.floor(monthsFromEpoch / 12) + 1;
|
||||
let hm = (monthsFromEpoch % 12) + 1;
|
||||
if (hm <= 0) { hm += 12; hy--; }
|
||||
if (hy < 1) return null;
|
||||
|
||||
const hd = Math.round((inputMs - monthStart) / MS_PER_DAY) + 1;
|
||||
return { hy, hm, hd };
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// ─── FCNA Hijri -> Gregorian ──────────────────────────────────────────────────
|
||||
|
||||
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 ─────────────────────────────────────────────────────────
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// ─── Engine export ────────────────────────────────────────────────────────────
|
||||
|
||||
export const fcnaEngine: CalendarEngine = {
|
||||
id: 'fcna',
|
||||
toHijri: fcnaToHijri,
|
||||
toGregorian: fcnaToGregorian,
|
||||
isValid: fcnaIsValid,
|
||||
daysInMonth: fcnaDaysInMonth,
|
||||
};
|
||||
138
src/engines/uaq.ts
Normal file
138
src/engines/uaq.ts
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
// UAQ engine: Umm al-Qura calendar (official Saudi Arabian Islamic calendar).
|
||||
//
|
||||
// Conversions are table-driven. The reference table covers Hijri years 1318-1500
|
||||
// (Gregorian 1900-2076). Each entry records the Gregorian date of 1 Muharram and
|
||||
// a 12-bit days-per-month bitmask. Dates outside that window return null.
|
||||
|
||||
import { hDatesTable } from '../data/hDates';
|
||||
import type { CalendarEngine, HijriDate } from '../types';
|
||||
|
||||
// toHijri uses local date components (getFullYear, getMonth, getDate) so that
|
||||
// the calendar-date lookup is timezone-safe regardless of the host environment.
|
||||
function uaqToHijri(date: Date): HijriDate | null {
|
||||
if (!(date instanceof Date) || isNaN(date.getTime())) {
|
||||
throw new Error('Invalid Gregorian date');
|
||||
}
|
||||
|
||||
const inputUtc = Date.UTC(
|
||||
date.getFullYear(),
|
||||
date.getMonth(),
|
||||
date.getDate(),
|
||||
);
|
||||
|
||||
// Binary search: find the last table entry whose Gregorian start date <= input.
|
||||
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 is the sentinel entry (hy 1501) marking the upper bound.
|
||||
if (found === -1 || hDatesTable[found].dpm === 0) return null;
|
||||
|
||||
const record = hDatesTable[found];
|
||||
const startUtc = Date.UTC(record.gy, record.gm - 1, record.gd);
|
||||
let remaining = Math.round((inputUtc - startUtc) / 86_400_000);
|
||||
let hijriMonth = 0;
|
||||
|
||||
for (let i = 0; i < 12; i++) {
|
||||
const dim = (record.dpm >> i) & 1 ? 30 : 29;
|
||||
if (remaining < dim) {
|
||||
hijriMonth = i + 1;
|
||||
break;
|
||||
}
|
||||
remaining -= dim;
|
||||
}
|
||||
|
||||
if (hijriMonth === 0) return null;
|
||||
|
||||
return { hy: record.hy, hm: hijriMonth, hd: remaining + 1 };
|
||||
}
|
||||
|
||||
function uaqToGregorian(hy: number, hm: number, hd: number): Date | null {
|
||||
if (!uaqIsValid(hy, hm, hd)) {
|
||||
throw new Error('Invalid Hijri date');
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
return new Date(Date.UTC(record.gy, record.gm - 1, record.gd) + totalDays * 86_400_000);
|
||||
}
|
||||
|
||||
function uaqIsValid(hy: number, hm: number, hd: number): boolean {
|
||||
if (hm < 1 || hm > 12 || hd < 1) return false;
|
||||
|
||||
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 || hDatesTable[found].dpm === 0) return false;
|
||||
|
||||
const dim = (hDatesTable[found].dpm >> (hm - 1)) & 1 ? 30 : 29;
|
||||
return hd <= dim;
|
||||
}
|
||||
|
||||
function uaqDaysInMonth(hy: number, hm: number): number {
|
||||
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 || hDatesTable[found].dpm === 0) return 0;
|
||||
return (hDatesTable[found].dpm >> (hm - 1)) & 1 ? 30 : 29;
|
||||
}
|
||||
|
||||
export const uaqEngine: CalendarEngine = {
|
||||
id: 'uaq',
|
||||
toHijri: uaqToHijri,
|
||||
toGregorian: uaqToGregorian,
|
||||
isValid: uaqIsValid,
|
||||
daysInMonth: uaqDaysInMonth,
|
||||
};
|
||||
57
src/index.ts
Normal file
57
src/index.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
// Register built-in engines at module load.
|
||||
import { uaqEngine } from './engines/uaq';
|
||||
import { fcnaEngine } from './engines/fcna';
|
||||
import { registerCalendar } from './registry';
|
||||
|
||||
registerCalendar('uaq', uaqEngine);
|
||||
registerCalendar('fcna', fcnaEngine);
|
||||
|
||||
// Registry
|
||||
export { registerCalendar, getCalendar, listCalendars } from './registry';
|
||||
|
||||
// Types
|
||||
export type { HijriDate, HijriYearRecord, CalendarEngine, ConversionOptions } from './types';
|
||||
|
||||
// Data
|
||||
export { hDatesTable } from './data/hDates';
|
||||
|
||||
// Names
|
||||
export { hmLong, hmMedium, hmShort } from './names/months';
|
||||
export { hwLong, hwShort, hwNumeric } from './names/weekdays';
|
||||
|
||||
// Convenience wrappers
|
||||
import { getCalendar } from './registry';
|
||||
import type { HijriDate, ConversionOptions } from './types';
|
||||
|
||||
export function toHijri(date: Date, options?: ConversionOptions): HijriDate | null {
|
||||
if (!(date instanceof Date) || isNaN(date.getTime())) {
|
||||
throw new Error('Invalid Gregorian date');
|
||||
}
|
||||
return getCalendar(options?.calendar ?? 'uaq').toHijri(date);
|
||||
}
|
||||
|
||||
export function toGregorian(
|
||||
hy: number,
|
||||
hm: number,
|
||||
hd: number,
|
||||
options?: ConversionOptions,
|
||||
): Date | null {
|
||||
return getCalendar(options?.calendar ?? 'uaq').toGregorian(hy, hm, hd);
|
||||
}
|
||||
|
||||
export function isValidHijriDate(
|
||||
hy: number,
|
||||
hm: number,
|
||||
hd: number,
|
||||
options?: ConversionOptions,
|
||||
): boolean {
|
||||
return getCalendar(options?.calendar ?? 'uaq').isValid(hy, hm, hd);
|
||||
}
|
||||
|
||||
export function daysInHijriMonth(
|
||||
hy: number,
|
||||
hm: number,
|
||||
options?: ConversionOptions,
|
||||
): number {
|
||||
return getCalendar(options?.calendar ?? 'uaq').daysInMonth(hy, hm);
|
||||
}
|
||||
47
src/names/months.ts
Normal file
47
src/names/months.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
// Hijri month names in three forms.
|
||||
// Index 0 = Muharram (month 1), index 11 = Dhul Hijjah (month 12).
|
||||
|
||||
export const hmLong = [
|
||||
"Muharram", // 1
|
||||
"Safar", // 2
|
||||
"Rabi'l Awwal", // 3
|
||||
"Rabi'l Thani", // 4
|
||||
"Jumadal Awwal", // 5
|
||||
"Jumadal Thani", // 6
|
||||
"Rajab", // 7
|
||||
"Sha'ban", // 8
|
||||
"Ramadan", // 9
|
||||
"Shawwal", // 10
|
||||
"Dhul Qi'dah", // 11
|
||||
"Dhul Hijjah", // 12
|
||||
];
|
||||
|
||||
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",
|
||||
];
|
||||
25
src/names/weekdays.ts
Normal file
25
src/names/weekdays.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
// Hijri weekday names.
|
||||
// Index 0 = Sunday, index 6 = Saturday (matching JS Date.getDay()).
|
||||
|
||||
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
|
||||
];
|
||||
|
||||
export const hwShort = [
|
||||
"Ahad", // Sunday
|
||||
"Ithn", // Monday
|
||||
"Thul", // Tuesday
|
||||
"Arba", // Wednesday
|
||||
"Kham", // Thursday
|
||||
"Jum`a", // Friday
|
||||
"Sabt", // Saturday
|
||||
];
|
||||
|
||||
// Numeric representation: 1 = Sunday, 7 = Saturday.
|
||||
export const hwNumeric = [1, 2, 3, 4, 5, 6, 7];
|
||||
22
src/registry.ts
Normal file
22
src/registry.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import type { CalendarEngine } from './types';
|
||||
|
||||
const _engines = new Map<string, CalendarEngine>();
|
||||
|
||||
export function registerCalendar(name: string, engine: CalendarEngine): void {
|
||||
_engines.set(name, engine);
|
||||
}
|
||||
|
||||
export function getCalendar(name: string): CalendarEngine {
|
||||
const engine = _engines.get(name);
|
||||
if (!engine) {
|
||||
const available = listCalendars().join(', ');
|
||||
throw new Error(
|
||||
`Unknown Hijri calendar: "${name}". Available: ${available}. Register custom calendars with registerCalendar().`
|
||||
);
|
||||
}
|
||||
return engine;
|
||||
}
|
||||
|
||||
export function listCalendars(): string[] {
|
||||
return Array.from(_engines.keys());
|
||||
}
|
||||
26
src/types.ts
Normal file
26
src/types.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
export interface HijriDate {
|
||||
hy: number; // Hijri year
|
||||
hm: number; // Hijri month (1-12)
|
||||
hd: number; // Hijri day (1-30)
|
||||
}
|
||||
|
||||
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 (1-based)
|
||||
gd: number; // Gregorian day of 1 Muharram
|
||||
}
|
||||
|
||||
// Any calendar engine must implement this interface.
|
||||
export interface CalendarEngine {
|
||||
readonly id: string;
|
||||
toHijri(date: Date): HijriDate | null;
|
||||
toGregorian(hy: number, hm: number, hd: number): Date | null;
|
||||
isValid(hy: number, hm: number, hd: number): boolean;
|
||||
daysInMonth(hy: number, hm: number): number;
|
||||
}
|
||||
|
||||
export interface ConversionOptions {
|
||||
calendar?: string; // defaults to 'uaq'
|
||||
}
|
||||
144
test-cjs.cjs
Normal file
144
test-cjs.cjs
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
'use strict';
|
||||
|
||||
// CJS test suite for hijri-core.
|
||||
// Subset of test.mjs — verifies the CommonJS build works correctly.
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const {
|
||||
toHijri,
|
||||
toGregorian,
|
||||
isValidHijriDate,
|
||||
daysInHijriMonth,
|
||||
registerCalendar,
|
||||
getCalendar,
|
||||
listCalendars,
|
||||
hDatesTable,
|
||||
hmLong,
|
||||
hmShort,
|
||||
hwLong,
|
||||
hwNumeric,
|
||||
} = require('./dist/index.cjs');
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
function test(name, fn) {
|
||||
try {
|
||||
fn();
|
||||
console.log(`[${name}]... PASS`);
|
||||
passed++;
|
||||
} catch (err) {
|
||||
console.error(`[${name}]... FAIL: ${err.message}`);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
// Exports
|
||||
test('CJS exports: toHijri is a function', () => {
|
||||
assert.equal(typeof toHijri, 'function');
|
||||
});
|
||||
test('CJS exports: toGregorian is a function', () => {
|
||||
assert.equal(typeof toGregorian, 'function');
|
||||
});
|
||||
test('CJS exports: hDatesTable is an array', () => {
|
||||
assert.ok(Array.isArray(hDatesTable));
|
||||
assert.ok(hDatesTable.length > 180);
|
||||
});
|
||||
test('CJS exports: hmLong[8] = Ramadan', () => {
|
||||
assert.equal(hmLong[8], 'Ramadan');
|
||||
});
|
||||
test('CJS exports: hmShort[8] = Ram', () => {
|
||||
assert.equal(hmShort[8], 'Ram');
|
||||
});
|
||||
test('CJS exports: hwLong[4] = Yawm al-Khamis', () => {
|
||||
assert.equal(hwLong[4], 'Yawm al-Khamis');
|
||||
});
|
||||
test('CJS exports: hwNumeric[0] = 1', () => {
|
||||
assert.equal(hwNumeric[0], 1);
|
||||
});
|
||||
|
||||
// UAQ conversions
|
||||
test('CJS UAQ toGregorian: 1444/9/1 = 2023-03-23', () => {
|
||||
const d = toGregorian(1444, 9, 1);
|
||||
assert.ok(d instanceof Date);
|
||||
assert.equal(d.toISOString().slice(0, 10), '2023-03-23');
|
||||
});
|
||||
test('CJS UAQ toGregorian: 1446/9/1 = 2025-03-01', () => {
|
||||
const d = toGregorian(1446, 9, 1);
|
||||
assert.ok(d instanceof Date);
|
||||
assert.equal(d.toISOString().slice(0, 10), '2025-03-01');
|
||||
});
|
||||
test('CJS UAQ toHijri: 2023-03-23 = 1444/9/1', () => {
|
||||
const h = toHijri(new Date(2023, 2, 23, 12));
|
||||
assert.ok(h !== null);
|
||||
assert.equal(h.hy, 1444);
|
||||
assert.equal(h.hm, 9);
|
||||
assert.equal(h.hd, 1);
|
||||
});
|
||||
test('CJS UAQ isValid: 1444/9/1 = true', () => {
|
||||
assert.equal(isValidHijriDate(1444, 9, 1), true);
|
||||
});
|
||||
test('CJS UAQ isValid: 1317/1/1 = false', () => {
|
||||
assert.equal(isValidHijriDate(1317, 1, 1), false);
|
||||
});
|
||||
test('CJS UAQ daysInMonth: Ramadan 1444 = 29', () => {
|
||||
assert.equal(daysInHijriMonth(1444, 9), 29);
|
||||
});
|
||||
|
||||
// FCNA conversions
|
||||
test('CJS FCNA toGregorian: 1446/9/1 = 2025-03-01', () => {
|
||||
const d = toGregorian(1446, 9, 1, { calendar: 'fcna' });
|
||||
assert.ok(d instanceof Date);
|
||||
assert.equal(d.toISOString().slice(0, 10), '2025-03-01');
|
||||
});
|
||||
test('CJS FCNA toHijri: 2025-03-01 = 1446/9/1', () => {
|
||||
const h = toHijri(new Date('2025-03-01'), { calendar: 'fcna' });
|
||||
assert.ok(h !== null);
|
||||
assert.equal(h.hy, 1446);
|
||||
assert.equal(h.hm, 9);
|
||||
assert.equal(h.hd, 1);
|
||||
});
|
||||
|
||||
// Registry
|
||||
test('CJS listCalendars includes uaq and fcna', () => {
|
||||
const cals = listCalendars();
|
||||
assert.ok(cals.includes('uaq'));
|
||||
assert.ok(cals.includes('fcna'));
|
||||
});
|
||||
test('CJS getCalendar throws for unknown', () => {
|
||||
assert.throws(
|
||||
() => getCalendar('nope'),
|
||||
/Unknown Hijri calendar/,
|
||||
);
|
||||
});
|
||||
|
||||
// Custom calendar
|
||||
test('CJS registerCalendar: custom engine', () => {
|
||||
const mockEngine = {
|
||||
id: 'mock-cjs',
|
||||
toHijri: () => ({ hy: 888, hm: 2, hd: 5 }),
|
||||
toGregorian: () => new Date(Date.UTC(2001, 0, 1)),
|
||||
isValid: (hy, hm, hd) => hy > 0 && hm >= 1 && hm <= 12 && hd >= 1,
|
||||
daysInMonth: () => 29,
|
||||
};
|
||||
registerCalendar('mock-cjs', mockEngine);
|
||||
|
||||
const h = toHijri(new Date(2020, 0, 1), { calendar: 'mock-cjs' });
|
||||
assert.ok(h !== null);
|
||||
assert.equal(h.hy, 888);
|
||||
assert.equal(h.hm, 2);
|
||||
assert.equal(h.hd, 5);
|
||||
});
|
||||
|
||||
// Error cases
|
||||
test('CJS toHijri throws on non-Date', () => {
|
||||
assert.throws(() => toHijri('bad'), /Invalid Gregorian date/);
|
||||
});
|
||||
test('CJS toGregorian throws on invalid Hijri', () => {
|
||||
assert.throws(() => toGregorian(1317, 1, 1), /Invalid Hijri date/);
|
||||
});
|
||||
|
||||
// Summary
|
||||
const total = passed + failed;
|
||||
console.log(`\n${total} tests total: ${passed} passed, ${failed} failed`);
|
||||
if (failed > 0) process.exit(1);
|
||||
271
test.mjs
Normal file
271
test.mjs
Normal file
|
|
@ -0,0 +1,271 @@
|
|||
// ESM test suite for hijri-core.
|
||||
// Uses Node.js assert — no test framework needed.
|
||||
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
toHijri,
|
||||
toGregorian,
|
||||
isValidHijriDate,
|
||||
daysInHijriMonth,
|
||||
registerCalendar,
|
||||
getCalendar,
|
||||
listCalendars,
|
||||
hDatesTable,
|
||||
hmLong,
|
||||
hmMedium,
|
||||
hmShort,
|
||||
hwLong,
|
||||
hwShort,
|
||||
hwNumeric,
|
||||
} from './dist/index.mjs';
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
function test(name, fn) {
|
||||
try {
|
||||
fn();
|
||||
console.log(`[${name}]... PASS`);
|
||||
passed++;
|
||||
} catch (err) {
|
||||
console.error(`[${name}]... FAIL: ${err.message}`);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 1. Exports exist ─────────────────────────────────────────────────────────
|
||||
|
||||
test('exports: toHijri is a function', () => {
|
||||
assert.equal(typeof toHijri, 'function');
|
||||
});
|
||||
test('exports: toGregorian is a function', () => {
|
||||
assert.equal(typeof toGregorian, 'function');
|
||||
});
|
||||
test('exports: isValidHijriDate is a function', () => {
|
||||
assert.equal(typeof isValidHijriDate, 'function');
|
||||
});
|
||||
test('exports: daysInHijriMonth is a function', () => {
|
||||
assert.equal(typeof daysInHijriMonth, 'function');
|
||||
});
|
||||
test('exports: registerCalendar is a function', () => {
|
||||
assert.equal(typeof registerCalendar, 'function');
|
||||
});
|
||||
test('exports: getCalendar is a function', () => {
|
||||
assert.equal(typeof getCalendar, 'function');
|
||||
});
|
||||
test('exports: listCalendars is a function', () => {
|
||||
assert.equal(typeof listCalendars, 'function');
|
||||
});
|
||||
test('exports: hDatesTable is an array', () => {
|
||||
assert.ok(Array.isArray(hDatesTable));
|
||||
assert.ok(hDatesTable.length > 180);
|
||||
});
|
||||
test('exports: hmLong has 12 entries', () => {
|
||||
assert.equal(hmLong.length, 12);
|
||||
});
|
||||
test('exports: hmMedium has 12 entries', () => {
|
||||
assert.equal(hmMedium.length, 12);
|
||||
});
|
||||
test('exports: hmShort has 12 entries', () => {
|
||||
assert.equal(hmShort.length, 12);
|
||||
});
|
||||
test('exports: hwLong has 7 entries', () => {
|
||||
assert.equal(hwLong.length, 7);
|
||||
});
|
||||
test('exports: hwShort has 7 entries', () => {
|
||||
assert.equal(hwShort.length, 7);
|
||||
});
|
||||
test('exports: hwNumeric has 7 entries', () => {
|
||||
assert.equal(hwNumeric.length, 7);
|
||||
assert.deepEqual(hwNumeric, [1, 2, 3, 4, 5, 6, 7]);
|
||||
});
|
||||
|
||||
// ─── 2. UAQ toGregorian ───────────────────────────────────────────────────────
|
||||
|
||||
test('UAQ toGregorian: 1444/9/1 = 2023-03-23', () => {
|
||||
const d = toGregorian(1444, 9, 1);
|
||||
assert.ok(d instanceof Date);
|
||||
assert.equal(d.toISOString().slice(0, 10), '2023-03-23');
|
||||
});
|
||||
test('UAQ toGregorian: 1446/9/1 = 2025-03-01', () => {
|
||||
const d = toGregorian(1446, 9, 1);
|
||||
assert.ok(d instanceof Date);
|
||||
assert.equal(d.toISOString().slice(0, 10), '2025-03-01');
|
||||
});
|
||||
test('UAQ toGregorian: 1446/10/1 = 2025-03-30', () => {
|
||||
const d = toGregorian(1446, 10, 1);
|
||||
assert.ok(d instanceof Date);
|
||||
assert.equal(d.toISOString().slice(0, 10), '2025-03-30');
|
||||
});
|
||||
test('UAQ toGregorian: 1318/1/1 = 1900-04-30', () => {
|
||||
const d = toGregorian(1318, 1, 1);
|
||||
assert.ok(d instanceof Date);
|
||||
assert.equal(d.toISOString().slice(0, 10), '1900-04-30');
|
||||
});
|
||||
|
||||
// ─── 3. UAQ toHijri ───────────────────────────────────────────────────────────
|
||||
|
||||
test('UAQ toHijri: 2023-03-23 = 1444/9/1', () => {
|
||||
const h = toHijri(new Date(2023, 2, 23, 12));
|
||||
assert.ok(h !== null);
|
||||
assert.equal(h.hy, 1444);
|
||||
assert.equal(h.hm, 9);
|
||||
assert.equal(h.hd, 1);
|
||||
});
|
||||
test('UAQ toHijri: 2025-03-01 = 1446/9/1', () => {
|
||||
const h = toHijri(new Date(2025, 2, 1, 12));
|
||||
assert.ok(h !== null);
|
||||
assert.equal(h.hy, 1446);
|
||||
assert.equal(h.hm, 9);
|
||||
assert.equal(h.hd, 1);
|
||||
});
|
||||
|
||||
// ─── 4. UAQ isValidHijriDate ──────────────────────────────────────────────────
|
||||
|
||||
test('UAQ isValid: 1444/9/1 = true', () => {
|
||||
assert.equal(isValidHijriDate(1444, 9, 1), true);
|
||||
});
|
||||
test('UAQ isValid: 1317/1/1 = false (before table)', () => {
|
||||
assert.equal(isValidHijriDate(1317, 1, 1), false);
|
||||
});
|
||||
test('UAQ isValid: 1501/1/1 = false (sentinel)', () => {
|
||||
assert.equal(isValidHijriDate(1501, 1, 1), false);
|
||||
});
|
||||
test('UAQ isValid: month 0 = false', () => {
|
||||
assert.equal(isValidHijriDate(1444, 0, 1), false);
|
||||
});
|
||||
|
||||
// ─── 5. daysInHijriMonth ──────────────────────────────────────────────────────
|
||||
|
||||
test('UAQ daysInMonth: Ramadan 1444 = 29 days', () => {
|
||||
// 1444 dpm = 0x0A9A; bit 8 (month 9) = (0x0A9A >> 8) & 1 = 0x0A & 1 = 0 -> 29
|
||||
assert.equal(daysInHijriMonth(1444, 9), 29);
|
||||
});
|
||||
|
||||
// ─── 6. FCNA toGregorian ──────────────────────────────────────────────────────
|
||||
|
||||
test('FCNA toGregorian: 1446/9/1 = 2025-03-01', () => {
|
||||
const d = toGregorian(1446, 9, 1, { calendar: 'fcna' });
|
||||
assert.ok(d instanceof Date);
|
||||
assert.equal(d.toISOString().slice(0, 10), '2025-03-01');
|
||||
});
|
||||
test('FCNA toGregorian: 1446/10/1 = 2025-03-30', () => {
|
||||
const d = toGregorian(1446, 10, 1, { calendar: 'fcna' });
|
||||
assert.ok(d instanceof Date);
|
||||
assert.equal(d.toISOString().slice(0, 10), '2025-03-30');
|
||||
});
|
||||
|
||||
// ─── 7. FCNA toHijri ──────────────────────────────────────────────────────────
|
||||
|
||||
test('FCNA toHijri: 2025-03-01 = 1446/9/1', () => {
|
||||
// Use UTC date for FCNA (criterion is UTC-based).
|
||||
const h = toHijri(new Date('2025-03-01'), { calendar: 'fcna' });
|
||||
assert.ok(h !== null);
|
||||
assert.equal(h.hy, 1446);
|
||||
assert.equal(h.hm, 9);
|
||||
assert.equal(h.hd, 1);
|
||||
});
|
||||
|
||||
// ─── 8. FCNA round-trips ──────────────────────────────────────────────────────
|
||||
|
||||
test('FCNA round-trip: 1446/9/1 toGregorian->toHijri', () => {
|
||||
const greg = toGregorian(1446, 9, 1, { calendar: 'fcna' });
|
||||
assert.ok(greg !== null);
|
||||
const hijri = toHijri(greg, { calendar: 'fcna' });
|
||||
assert.ok(hijri !== null);
|
||||
assert.equal(hijri.hy, 1446);
|
||||
assert.equal(hijri.hm, 9);
|
||||
assert.equal(hijri.hd, 1);
|
||||
});
|
||||
test('FCNA round-trip: 1446/10/15 toGregorian->toHijri', () => {
|
||||
const greg = toGregorian(1446, 10, 15, { calendar: 'fcna' });
|
||||
assert.ok(greg !== null);
|
||||
const hijri = toHijri(greg, { calendar: 'fcna' });
|
||||
assert.ok(hijri !== null);
|
||||
assert.equal(hijri.hy, 1446);
|
||||
assert.equal(hijri.hm, 10);
|
||||
assert.equal(hijri.hd, 15);
|
||||
});
|
||||
|
||||
// ─── 9. FCNA isValid ──────────────────────────────────────────────────────────
|
||||
|
||||
test('FCNA isValid: 1/1/1 = true', () => {
|
||||
assert.equal(isValidHijriDate(1, 1, 1, { calendar: 'fcna' }), true);
|
||||
});
|
||||
test('FCNA isValid: 1600/1/1 = true', () => {
|
||||
assert.equal(isValidHijriDate(1600, 1, 1, { calendar: 'fcna' }), true);
|
||||
});
|
||||
test('FCNA isValid: 0/1/1 = false', () => {
|
||||
assert.equal(isValidHijriDate(0, 1, 1, { calendar: 'fcna' }), false);
|
||||
});
|
||||
|
||||
// ─── 10. listCalendars ────────────────────────────────────────────────────────
|
||||
|
||||
test('listCalendars includes uaq and fcna', () => {
|
||||
const cals = listCalendars();
|
||||
assert.ok(cals.includes('uaq'));
|
||||
assert.ok(cals.includes('fcna'));
|
||||
});
|
||||
|
||||
// ─── 11. getCalendar throws for unknown ───────────────────────────────────────
|
||||
|
||||
test('getCalendar throws for unknown calendar', () => {
|
||||
assert.throws(
|
||||
() => getCalendar('nonexistent'),
|
||||
/Unknown Hijri calendar/,
|
||||
);
|
||||
});
|
||||
|
||||
// ─── 12. Custom calendar registration ────────────────────────────────────────
|
||||
|
||||
test('registerCalendar: custom engine works', () => {
|
||||
const mockEngine = {
|
||||
id: 'mock',
|
||||
toHijri: (_date) => ({ hy: 999, hm: 1, hd: 1 }),
|
||||
toGregorian: (_hy, _hm, _hd) => new Date(Date.UTC(2000, 0, 1)),
|
||||
isValid: (hy, hm, hd) => hy > 0 && hm >= 1 && hm <= 12 && hd >= 1,
|
||||
daysInMonth: (_hy, _hm) => 30,
|
||||
};
|
||||
registerCalendar('mock', mockEngine);
|
||||
|
||||
const cals = listCalendars();
|
||||
assert.ok(cals.includes('mock'));
|
||||
|
||||
const h = toHijri(new Date(2020, 0, 1), { calendar: 'mock' });
|
||||
assert.ok(h !== null);
|
||||
assert.equal(h.hy, 999);
|
||||
|
||||
const g = toGregorian(1, 1, 1, { calendar: 'mock' });
|
||||
assert.ok(g instanceof Date);
|
||||
assert.equal(g.toISOString().slice(0, 10), '2000-01-01');
|
||||
|
||||
assert.equal(isValidHijriDate(1, 1, 1, { calendar: 'mock' }), true);
|
||||
assert.equal(daysInHijriMonth(1, 1, { calendar: 'mock' }), 30);
|
||||
});
|
||||
|
||||
// ─── 13. Error cases ──────────────────────────────────────────────────────────
|
||||
|
||||
test('toHijri throws on non-Date input', () => {
|
||||
assert.throws(
|
||||
() => toHijri('2023-03-23'),
|
||||
/Invalid Gregorian date/,
|
||||
);
|
||||
});
|
||||
test('toHijri throws on invalid Date', () => {
|
||||
assert.throws(
|
||||
() => toHijri(new Date('invalid')),
|
||||
/Invalid Gregorian date/,
|
||||
);
|
||||
});
|
||||
test('UAQ toGregorian throws on invalid Hijri date', () => {
|
||||
assert.throws(
|
||||
() => toGregorian(1317, 1, 1),
|
||||
/Invalid Hijri date/,
|
||||
);
|
||||
});
|
||||
|
||||
// ─── Summary ─────────────────────────────────────────────────────────────────
|
||||
|
||||
const total = passed + failed;
|
||||
console.log(`\n${total} tests total: ${passed} passed, ${failed} failed`);
|
||||
if (failed > 0) process.exit(1);
|
||||
16
tsconfig.json
Normal file
16
tsconfig.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
14
tsup.config.ts
Normal file
14
tsup.config.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { defineConfig } from 'tsup';
|
||||
|
||||
export default defineConfig({
|
||||
entry: ['src/index.ts'],
|
||||
format: ['cjs', 'esm'],
|
||||
dts: true,
|
||||
clean: true,
|
||||
outDir: 'dist',
|
||||
splitting: false,
|
||||
sourcemap: true,
|
||||
target: 'es2020',
|
||||
platform: 'node',
|
||||
outExtension: ({ format }) => ({ js: format === 'cjs' ? '.cjs' : '.mjs' }),
|
||||
});
|
||||
Loading…
Reference in a new issue