refactor: code quality improvements across the board

This commit is contained in:
Aric Camarata 2026-03-08 11:38:10 -04:00
parent 0727665eaa
commit f0757b1333
22 changed files with 2382 additions and 968 deletions

View file

@ -22,8 +22,22 @@ jobs:
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm run build
- run: node test.mjs
- run: node test-cjs.cjs
- run: node --test test.mjs
- run: node --test test-cjs.cjs
lint:
name: Lint
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 lint
- run: pnpm run format:check
typecheck:
name: Typecheck

24
.gitignore vendored
View file

@ -1,8 +1,30 @@
node_modules/
dist/
build/
out/
*.tgz
*.tsbuildinfo
*.log
npm-debug.log*
coverage/
.DS_Store
.claude/
.env
.env.*
!.env.example
.claude/
.vscode/
.idea/
*.swp
.pnp
.pnp.js
.cursor/
.copilot/
.aider*
.continue/
.codex/
.gemini/
.vscode/*
.aider/
.aider.chat.history.md
.windsurf/
.codeium/

6
.prettierrc Normal file
View file

@ -0,0 +1,6 @@
{
"singleQuote": true,
"trailingComma": "all",
"printWidth": 100,
"tabWidth": 2
}

View file

@ -7,12 +7,12 @@
Converts a Gregorian `Date` to a Hijri date object.
```typescript
function toHijri(date: Date, options?: ConversionOptions): HijriDate | null
function toHijri(date: Date, options?: ConversionOptions): HijriDate | null;
```
| Parameter | Type | Description |
| --- | --- | --- |
| `date` | `Date` | Gregorian date to convert |
| 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.
@ -26,19 +26,14 @@ UAQ uses local date components (`getFullYear`, `getMonth`, `getDate`) for timezo
Converts a Hijri date to a Gregorian `Date`.
```typescript
function toGregorian(
hy: number,
hm: number,
hd: number,
options?: ConversionOptions
): Date | null
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) |
| 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.
@ -50,12 +45,7 @@ Throws `Error('Invalid Hijri date')` for UAQ when the date is not in the referen
Returns `true` if the given Hijri date exists in the selected calendar.
```typescript
function isValidHijriDate(
hy: number,
hm: number,
hd: number,
options?: ConversionOptions
): boolean
function isValidHijriDate(hy: number, hm: number, hd: number, options?: ConversionOptions): boolean;
```
### `daysInHijriMonth(hy, hm, options?)`
@ -63,7 +53,7 @@ function isValidHijriDate(
Returns the number of days in a given Hijri month.
```typescript
function daysInHijriMonth(hy: number, hm: number, options?: ConversionOptions): number
function daysInHijriMonth(hy: number, hm: number, options?: ConversionOptions): number;
```
Returns 29 or 30. Returns 0 for UAQ when the year is out of range.
@ -75,7 +65,7 @@ Returns 29 or 30. Returns 0 for UAQ when the year is out of range.
Registers a calendar engine. Overwrites any existing engine with the same name.
```typescript
function registerCalendar(name: string, engine: CalendarEngine): void
function registerCalendar(name: string, engine: CalendarEngine): void;
```
### `getCalendar(name)`
@ -83,7 +73,7 @@ function registerCalendar(name: string, engine: CalendarEngine): void
Returns the registered engine for a given name. Throws if not found.
```typescript
function getCalendar(name: string): CalendarEngine
function getCalendar(name: string): CalendarEngine;
```
Throws `Error('Unknown Hijri calendar: "name". Available: ...')`.
@ -93,7 +83,7 @@ Throws `Error('Unknown Hijri calendar: "name". Available: ...')`.
Returns the names of all registered calendars.
```typescript
function listCalendars(): string[]
function listCalendars(): string[];
```
## Types
@ -114,11 +104,11 @@ One entry in the Umm al-Qura reference table.
```typescript
interface HijriYearRecord {
hy: number; // Hijri year
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
gy: number; // Gregorian year of 1 Muharram
gm: number; // Gregorian month of 1 Muharram (1-based)
gd: number; // Gregorian day of 1 Muharram
}
```
@ -146,15 +136,15 @@ interface ConversionOptions {
## Data exports
| Export | Type | Description |
| --- | --- | --- |
| 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 |
| `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 |
---

View file

@ -39,6 +39,7 @@ The Umm al-Qura calendar is the official Islamic calendar of Saudi Arabia. Month
### 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
@ -70,9 +71,11 @@ The Fiqh Council of North America uses a global astronomical criterion rather th
### 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.
@ -111,11 +114,11 @@ 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 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;
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 };
}
@ -124,7 +127,7 @@ const arithmeticEngine: CalendarEngine = {
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,
daysInMonth: (hy, hm) => (hm % 2 === 1 || hm === 12 ? 30 : 29),
};
registerCalendar('arithmetic', arithmeticEngine);

View file

@ -18,7 +18,7 @@ const greg = toGregorian(1446, 9, 1);
// FCNA
const hijriFcna = toHijri(new Date('2025-03-01'), { calendar: 'fcna' });
const gregFcna = toGregorian(1446, 9, 1, { calendar: 'fcna' });
const gregFcna = toGregorian(1446, 9, 1, { calendar: 'fcna' });
```
## Custom calendar registration

View file

@ -29,11 +29,11 @@ const greg = toGregorian(1446, 9, 1);
// FCNA/ISNA calendar
const hijriFcna = toHijri(new Date('2025-03-01'), { calendar: 'fcna' });
const gregFcna = toGregorian(1446, 9, 1, { calendar: 'fcna' });
const gregFcna = toGregorian(1446, 9, 1, { calendar: 'fcna' });
// Validation and month length
isValidHijriDate(1444, 9, 1); // true
daysInHijriMonth(1444, 9); // 29
isValidHijriDate(1444, 9, 1); // true
daysInHijriMonth(1444, 9); // 29
```
### Custom Calendar
@ -43,8 +43,12 @@ 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(); },
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,
};
@ -58,34 +62,34 @@ const result = toHijri(new Date(), { calendar: 'my-calendar' });
### 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` | Returns null on invalid input |
| `isValidHijriDate(hy, hm, hd, opts?)` | `number, number, number, ConversionOptions?` | `boolean` | |
| `daysInHijriMonth(hy, hm, opts?)` | `number, number, ConversionOptions?` | `number` | |
| 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` | Returns null on invalid input |
| `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[]` |
| Function | Parameters | Returns |
| -------------------------------- | ------------------------ | ---------------- |
| `registerCalendar(name, engine)` | `string, CalendarEngine` | `void` |
| `getCalendar(name)` | `string` | `CalendarEngine` |
| `listCalendars()` | none | `string[]` |
### Data exports
| Export | Type | Description |
| --- | --- | --- |
| 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) |
| `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

10
eslint.config.mjs Normal file
View file

@ -0,0 +1,10 @@
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
import prettier from 'eslint-config-prettier';
export default tseslint.config(
{ ignores: ['dist/', 'node_modules/', '*.cjs', '*.mjs'] },
js.configs.recommended,
...tseslint.configs.recommended,
prettier,
);

View file

@ -9,11 +9,20 @@
"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" }
"import": {
"types": "./dist/index.d.mts",
"default": "./dist/index.mjs"
},
"require": {
"types": "./dist/index.d.ts",
"default": "./dist/index.cjs"
}
}
},
"sideEffects": ["./dist/index.cjs", "./dist/index.mjs"],
"sideEffects": [
"./dist/index.cjs",
"./dist/index.mjs"
],
"files": [
"dist/index.cjs",
"dist/index.mjs",
@ -23,14 +32,19 @@
"CHANGELOG.md",
"LICENSE"
],
"engines": { "node": ">=20" },
"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"
"test": "node --test test.mjs && node --test test-cjs.cjs",
"prepublishOnly": "tsup",
"lint": "eslint .",
"format": "prettier --write .",
"format:check": "prettier --check ."
},
"keywords": [
"hijri",
@ -45,12 +59,25 @@
"typescript"
],
"devDependencies": {
"@types/node": "^22.0.0",
"@eslint/js": "^10.0.1",
"@types/node": "^22.15.0",
"eslint": "^10.0.3",
"eslint-config-prettier": "^10.1.8",
"prettier": "^3.8.1",
"tsup": "^8.0.0",
"typescript": "^5.5.0"
"typescript": "^5.5.0",
"typescript-eslint": "^8.56.1"
},
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
},
"repository": {
"type": "git",
"url": "git+https://github.com/acamarata/hijri-core.git"
},
"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" }
"bugs": {
"url": "https://github.com/acamarata/hijri-core/issues"
}
}

File diff suppressed because it is too large Load diff

5
src/constants.ts Normal file
View file

@ -0,0 +1,5 @@
/** Milliseconds in one day. */
export const MS_PER_DAY = 86_400_000;
/** Number of months in a Hijri year. */
export const MONTHS_PER_YEAR = 12;

View file

@ -5,188 +5,188 @@ import type { HijriYearRecord } from '../types';
// 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: 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: 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: 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: 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: 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: 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 },
{ 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 },
];

View file

@ -8,15 +8,15 @@
// Chapter 49, accurate to within a few minutes for 1000-3000 CE.
import { hDatesTable } from '../data/hDates';
import { MS_PER_DAY, MONTHS_PER_YEAR } from '../constants';
import type { CalendarEngine, HijriDate } from '../types';
// ─── Constants ───────────────────────────────────────────────────────────────
const SYNODIC = 29.530588861; // Mean synodic month (days)
const JDE0 = 2451550.09766; // Meeus k=0 (2nd ed. Ch.49: 2451550.09765; 0.864 s diff, within tolerance)
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;
const SYNODIC = 29.530588861; // Mean synodic month (days)
const JDE0 = 2451550.09766; // Meeus k=0 (2nd ed. Ch.49: 2451550.09765; 0.864 s diff, within tolerance)
const JDE_UNIX = 2440587.5; // JDE of Unix epoch 1970-01-01 00:00 UTC
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.
@ -25,104 +25,88 @@ const K_EPOCH = -17037;
// ─── Meeus Chapter 49: corrected new moon JDE ────────────────────────────────
function newMoonJDE(k: number): number {
const T = k / 1236.85;
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;
let jde = JDE0 + SYNODIC * k + 0.00015437 * T2 - 0.00000015 * T3 + 0.00000000073 * T4;
const M = (2.5534
+ 29.10535670 * k
- 0.0000014 * T2
- 0.00000011 * T3) % 360;
const M = (2.5534 + 29.1053567 * 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 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 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 Omega = (124.7746 - 1.56375588 * k + 0.0020672 * T2 + 0.00000215 * T3) % 360;
const E = 1 - 0.002516 * T - 0.0000074 * T2;
const E = 1 - 0.002516 * T - 0.0000074 * T2;
const E2 = E * E;
const Mrad = M * TO_RAD;
const Mrad = M * TO_RAD;
const Mprad = Mprime * TO_RAD;
const Frad = F * TO_RAD;
const Orad = Omega * 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);
-0.4072 * 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 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.30686 * 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;
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);
+0.000325 * Math.sin(A1) +
0.000165 * Math.sin(A2) +
0.000164 * Math.sin(A3) +
0.000126 * Math.sin(A4) +
0.00011 * Math.sin(A5) +
0.000062 * Math.sin(A6) +
0.00006 * Math.sin(A7) +
0.000056 * Math.sin(A8) +
0.000047 * Math.sin(A9) +
0.000042 * Math.sin(A10) +
0.00004 * Math.sin(A11) +
0.000037 * Math.sin(A12) +
0.000035 * Math.sin(A13) +
0.000023 * Math.sin(A14);
return jde;
}
@ -143,15 +127,15 @@ function utcMsToKApprox(ms: number): number {
// 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 bestMs = 0;
let bestDist = Infinity;
for (let k = k0 - 2; k <= k0 + 2; k++) {
const ms = jdeToUtcMs(newMoonJDE(k));
const ms = jdeToUtcMs(newMoonJDE(k));
const dist = Math.abs(ms - anchorMs);
if (dist < bestDist) {
bestDist = dist;
bestMs = ms;
bestMs = ms;
}
}
@ -163,7 +147,7 @@ function nearestNewMoonMs(anchorMs: number): number {
// 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;
const noon = midnight + 12 * 3_600_000;
return conjMs < noon ? midnight + MS_PER_DAY : midnight + 2 * MS_PER_DAY;
}
@ -173,13 +157,17 @@ function fcnaCriterionMs(conjMs: number): number {
// 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;
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 (midHy === hy) {
found = mid;
break;
} else if (midHy < hy) lo = mid + 1;
else hi = mid - 1;
}
if (found !== -1 && hDatesTable[found].dpm !== 0) {
@ -191,7 +179,7 @@ function uaqAnchorMs(hy: number, hm: number): number {
return Date.UTC(r.gy, r.gm - 1, r.gd) + days * MS_PER_DAY;
}
const monthsFromEpoch = (hy - 1) * 12 + (hm - 1);
const monthsFromEpoch = (hy - 1) * MONTHS_PER_YEAR + (hm - 1);
const kApprox = K_EPOCH + monthsFromEpoch;
return jdeToUtcMs(newMoonJDE(kApprox));
}
@ -207,9 +195,12 @@ function fcnaMonthStartMs(hy: number, hm: number): number {
// ─── FCNA month length ───────────────────────────────────────────────────────
function fcnaDaysInMonth(hy: number, hm: number): number {
if (hm < 1 || hm > MONTHS_PER_YEAR) {
throw new RangeError(`month must be 1-12, got ${hm}`);
}
const thisStart = fcnaMonthStartMs(hy, hm);
const nextHy = hm < 12 ? hy : hy + 1;
const nextHm = hm < 12 ? hm + 1 : 1;
const nextHy = hm < MONTHS_PER_YEAR ? hy : hy + 1;
const nextHm = hm < MONTHS_PER_YEAR ? hm + 1 : 1;
const nextStart = fcnaMonthStartMs(nextHy, nextHm);
return Math.round((nextStart - thisStart) / MS_PER_DAY);
}
@ -229,22 +220,25 @@ function fcnaToHijri(gregorianDate: Date): HijriDate | null {
);
const kApprox = utcMsToKApprox(inputMs - 15 * MS_PER_DAY);
const k0 = Math.floor(kApprox);
const k0 = Math.floor(kApprox);
for (let ki = k0 - 1; ki <= k0 + 1; ki++) {
const conjMs = jdeToUtcMs(newMoonJDE(ki));
const monthStart = fcnaCriterionMs(conjMs);
const conjMs = jdeToUtcMs(newMoonJDE(ki));
const monthStart = fcnaCriterionMs(conjMs);
if (monthStart > inputMs) continue;
const nextConjMs = jdeToUtcMs(newMoonJDE(ki + 1));
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--; }
let hy = Math.floor(monthsFromEpoch / MONTHS_PER_YEAR) + 1;
let hm = (monthsFromEpoch % MONTHS_PER_YEAR) + 1;
if (hm <= 0) {
hm += MONTHS_PER_YEAR;
hy--;
}
if (hy < 1) return null;
const hd = Math.round((inputMs - monthStart) / MS_PER_DAY) + 1;
@ -258,7 +252,7 @@ function fcnaToHijri(gregorianDate: Date): HijriDate | 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;
if (hy < 1 || hm < 1 || hm > MONTHS_PER_YEAR || hd < 1) return null;
const days = fcnaDaysInMonth(hy, hm);
if (hd > days) return null;
const startMs = fcnaMonthStartMs(hy, hm);
@ -268,7 +262,7 @@ function fcnaToGregorian(hy: number, hm: number, hd: number): Date | null {
// ─── FCNA validation ─────────────────────────────────────────────────────────
function fcnaIsValid(hy: number, hm: number, hd: number): boolean {
if (hy < 1 || hm < 1 || hm > 12 || hd < 1) return false;
if (hy < 1 || hm < 1 || hm > MONTHS_PER_YEAR || hd < 1) return false;
return hd <= fcnaDaysInMonth(hy, hm);
}
@ -276,8 +270,8 @@ function fcnaIsValid(hy: number, hm: number, hd: number): boolean {
export const fcnaEngine: CalendarEngine = {
id: 'fcna',
toHijri: fcnaToHijri,
toHijri: fcnaToHijri,
toGregorian: fcnaToGregorian,
isValid: fcnaIsValid,
isValid: fcnaIsValid,
daysInMonth: fcnaDaysInMonth,
};

View file

@ -5,7 +5,32 @@
// a 12-bit days-per-month bitmask. Dates outside that window return null.
import { hDatesTable } from '../data/hDates';
import type { CalendarEngine, HijriDate } from '../types';
import { MS_PER_DAY, MONTHS_PER_YEAR } from '../constants';
import type { CalendarEngine, HijriDate, HijriYearRecord } from '../types';
/**
* Binary search for a Hijri year entry in the UAQ table.
*
* Returns the entry whose `hy` matches exactly, or null if the year is not
* present in the table. The table covers Hijri years 1318 through 1501
* (the final entry is a sentinel with dpm === 0).
*
* @param hy - Hijri year to locate
* @returns the matching table entry, or null if not found
*/
function findYearEntry(hy: number): HijriYearRecord | null {
let lo = 0;
let hi = hDatesTable.length - 1;
while (lo <= hi) {
const mid = (lo + hi) >>> 1;
const midHy = hDatesTable[mid].hy;
if (midHy === hy) return hDatesTable[mid];
else if (midHy < hy) lo = mid + 1;
else hi = mid - 1;
}
return null;
}
// toHijri uses local date components (getFullYear, getMonth, getDate) so that
// the calendar-date lookup is timezone-safe regardless of the host environment.
@ -14,11 +39,7 @@ function uaqToHijri(date: Date): HijriDate | null {
throw new Error('Invalid Gregorian date');
}
const inputUtc = Date.UTC(
date.getFullYear(),
date.getMonth(),
date.getDate(),
);
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;
@ -43,10 +64,10 @@ function uaqToHijri(date: Date): HijriDate | 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 remaining = Math.round((inputUtc - startUtc) / MS_PER_DAY);
let hijriMonth = 0;
for (let i = 0; i < 12; i++) {
for (let i = 0; i < MONTHS_PER_YEAR; i++) {
const dim = (record.dpm >> i) & 1 ? 30 : 29;
if (remaining < dim) {
hijriMonth = i + 1;
@ -65,22 +86,9 @@ function uaqToGregorian(hy: number, hm: number, hd: number): Date | null {
return null;
}
// Binary search on hy.
let lo = 0;
let hi = hDatesTable.length - 1;
let found = -1;
const record = findYearEntry(hy);
if (!record) return null;
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++) {
@ -88,53 +96,35 @@ function uaqToGregorian(hy: number, hm: number, hd: number): Date | null {
}
totalDays += hd - 1;
return new Date(Date.UTC(record.gy, record.gm - 1, record.gd) + totalDays * 86_400_000);
return new Date(Date.UTC(record.gy, record.gm - 1, record.gd) + totalDays * MS_PER_DAY);
}
function uaqIsValid(hy: number, hm: number, hd: number): boolean {
if (hm < 1 || hm > 12 || hd < 1) return false;
if (hm < 1 || hm > MONTHS_PER_YEAR || hd < 1) return false;
let lo = 0;
let hi = hDatesTable.length - 1;
let found = -1;
const record = findYearEntry(hy);
if (!record || record.dpm === 0) return false;
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;
const dim = (record.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 (hm < 1 || hm > MONTHS_PER_YEAR) {
throw new RangeError(`month must be 1-12, got ${hm}`);
}
if (found === -1 || hDatesTable[found].dpm === 0) {
const record = findYearEntry(hy);
if (!record || record.dpm === 0) {
throw new RangeError(`Hijri year ${hy} is outside the UAQ table range (1318-1500).`);
}
return (hDatesTable[found].dpm >> (hm - 1)) & 1 ? 30 : 29;
return (record.dpm >> (hm - 1)) & 1 ? 30 : 29;
}
export const uaqEngine: CalendarEngine = {
id: 'uaq',
toHijri: uaqToHijri,
toHijri: uaqToHijri,
toGregorian: uaqToGregorian,
isValid: uaqIsValid,
isValid: uaqIsValid,
daysInMonth: uaqDaysInMonth,
};

View file

@ -1,4 +1,6 @@
// Register built-in engines at module load.
// Built-in engines are registered at module load so that 'uaq' and 'fcna' are
// available immediately on import. This module-level side effect is intentional
// and documented in the sideEffects field of package.json.
import { uaqEngine } from './engines/uaq';
import { fcnaEngine } from './engines/fcna';
import { registerCalendar } from './registry';
@ -9,6 +11,9 @@ registerCalendar('fcna', fcnaEngine);
// Registry
export { registerCalendar, getCalendar, listCalendars } from './registry';
// Constants
export { MS_PER_DAY, MONTHS_PER_YEAR } from './constants';
// Types
export type { HijriDate, HijriYearRecord, CalendarEngine, ConversionOptions } from './types';
@ -23,6 +28,17 @@ export { hwLong, hwShort, hwNumeric } from './names/weekdays';
import { getCalendar } from './registry';
import type { HijriDate, ConversionOptions } from './types';
/**
* Convert a Gregorian date to a Hijri date.
*
* Uses the UAQ (Umm al-Qura) calendar by default. Pass `{ calendar: 'fcna' }`
* or any registered calendar name via options to use a different engine.
*
* @param date - a valid JavaScript Date object
* @param options - conversion options (calendar engine selection)
* @returns the corresponding Hijri date, or null if the date is out of range
* @throws {Error} if `date` is not a valid Date instance
*/
export function toHijri(date: Date, options?: ConversionOptions): HijriDate | null {
if (!(date instanceof Date) || isNaN(date.getTime())) {
throw new Error('Invalid Gregorian date');
@ -30,6 +46,17 @@ export function toHijri(date: Date, options?: ConversionOptions): HijriDate | nu
return getCalendar(options?.calendar ?? 'uaq').toHijri(date);
}
/**
* Convert a Hijri date to a Gregorian date.
*
* Uses the UAQ calendar by default.
*
* @param hy - Hijri year
* @param hm - Hijri month (1-12)
* @param hd - Hijri day (1-30)
* @param options - conversion options (calendar engine selection)
* @returns a Date in UTC, or null if the input is invalid or out of range
*/
export function toGregorian(
hy: number,
hm: number,
@ -39,6 +66,15 @@ export function toGregorian(
return getCalendar(options?.calendar ?? 'uaq').toGregorian(hy, hm, hd);
}
/**
* Check whether a Hijri date is valid for the given calendar engine.
*
* @param hy - Hijri year
* @param hm - Hijri month (1-12)
* @param hd - Hijri day (1-30)
* @param options - conversion options (calendar engine selection)
* @returns true if the date is valid
*/
export function isValidHijriDate(
hy: number,
hm: number,
@ -48,10 +84,15 @@ export function isValidHijriDate(
return getCalendar(options?.calendar ?? 'uaq').isValid(hy, hm, hd);
}
export function daysInHijriMonth(
hy: number,
hm: number,
options?: ConversionOptions,
): number {
/**
* Return the number of days in a given Hijri month.
*
* @param hy - Hijri year
* @param hm - Hijri month (1-12)
* @param options - conversion options (calendar engine selection)
* @returns 29 or 30
* @throws {RangeError} if the month or year is out of range
*/
export function daysInHijriMonth(hy: number, hm: number, options?: ConversionOptions): number {
return getCalendar(options?.calendar ?? 'uaq').daysInMonth(hy, hm);
}

View file

@ -2,46 +2,46 @@
// 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
'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-Hijjah",
'Muharram',
'Safar',
'Rabi1',
'Rabi2',
'Jumada1',
'Jumada2',
'Rajab',
'Shaban',
'Ramadan',
'Shawwal',
'Dhul-Qidah',
'Dhul-Hijjah',
];
export const hmShort = [
"Muh",
"Saf",
"Ra1",
"Ra2",
"Ju1",
"Ju2",
"Raj",
"Shb",
"Ram",
"Shw",
"DhQ",
"DhH",
'Muh',
'Saf',
'Ra1',
'Ra2',
'Ju1',
'Ju2',
'Raj',
'Shb',
'Ram',
'Shw',
'DhQ',
'DhH',
];

View file

@ -2,23 +2,23 @@
// 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
'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
'Ahad', // Sunday
'Ithn', // Monday
'Thul', // Tuesday
'Arba', // Wednesday
'Kham', // Thursday
'Jum`a', // Friday
'Sabt', // Saturday
];
// Numeric representation: 1 = Sunday, 7 = Saturday.

View file

@ -2,21 +2,42 @@ import type { CalendarEngine } from './types';
const _engines = new Map<string, CalendarEngine>();
/**
* Register a calendar engine under the given name.
*
* Once registered, the engine can be selected via `{ calendar: name }` in any
* conversion function or retrieved directly with {@link getCalendar}.
*
* @param name - unique identifier for the calendar (e.g. 'uaq', 'fcna')
* @param engine - an object implementing the {@link CalendarEngine} interface
*/
export function registerCalendar(name: string, engine: CalendarEngine): void {
_engines.set(name, engine);
}
/**
* Retrieve a registered calendar engine by name.
*
* @param name - the calendar identifier passed to {@link registerCalendar}
* @returns the matching engine
* @throws {Error} if no engine is registered under that name
*/
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().`
`Unknown Hijri calendar: "${name}". Available: ${available}. Register custom calendars with registerCalendar().`,
);
}
return engine;
}
/**
* List the names of all registered calendar engines.
*
* @returns an array of calendar names (e.g. ['uaq', 'fcna'])
*/
export function listCalendars(): string[] {
return Array.from(_engines.keys());
}

View file

@ -5,11 +5,11 @@ export interface HijriDate {
}
export interface HijriYearRecord {
hy: number; // Hijri year
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
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.

View file

@ -1,8 +1,9 @@
'use strict';
// CJS test suite for hijri-core.
// Subset of test.mjs — verifies the CommonJS build works correctly.
// Subset of test.mjs to verify the CommonJS build works correctly.
const { describe, it } = require('node:test');
const assert = require('node:assert/strict');
const {
toHijri,
@ -19,126 +20,131 @@ const {
hwNumeric,
} = require('./dist/index.cjs');
let passed = 0;
let failed = 0;
// ─── Exports ────────────────────────────────────────────────────────────────
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);
describe('CJS exports', () => {
it('toHijri is a function', () => {
assert.equal(typeof toHijri, 'function');
});
it('toGregorian is a function', () => {
assert.equal(typeof toGregorian, 'function');
});
it('hDatesTable is an array with > 180 entries', () => {
assert.ok(Array.isArray(hDatesTable));
assert.ok(hDatesTable.length > 180);
});
it('hmLong[8] = Ramadan', () => {
assert.equal(hmLong[8], 'Ramadan');
});
it('hmShort[8] = Ram', () => {
assert.equal(hmShort[8], 'Ram');
});
it('hwLong[4] = Yawm al-Khamis', () => {
assert.equal(hwLong[4], 'Yawm al-Khamis');
});
it('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);
// ─── UAQ conversions ────────────────────────────────────────────────────────
describe('CJS UAQ conversions', () => {
it('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');
});
it('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');
});
it('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);
});
it('isValid: 1444/9/1 = true', () => {
assert.equal(isValidHijriDate(1444, 9, 1), true);
});
it('isValid: 1317/1/1 = false', () => {
assert.equal(isValidHijriDate(1317, 1, 1), false);
});
it('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);
// ─── Invalid month validation ───────────────────────────────────────────────
describe('CJS invalid month', () => {
it('daysInMonth throws for month 0', () => {
assert.throws(() => daysInHijriMonth(1444, 0), /month must be 1-12/);
});
it('daysInMonth throws for month 13', () => {
assert.throws(() => daysInHijriMonth(1444, 13), /month must be 1-12/);
});
it('isValid: month 13 = false', () => {
assert.equal(isValidHijriDate(1444, 13, 1), false);
});
});
// 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/,
);
// ─── FCNA conversions ───────────────────────────────────────────────────────
describe('CJS FCNA conversions', () => {
it('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');
});
it('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);
});
});
// 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);
// ─── Registry ───────────────────────────────────────────────────────────────
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);
describe('CJS registry', () => {
it('listCalendars includes uaq and fcna', () => {
const cals = listCalendars();
assert.ok(cals.includes('uaq'));
assert.ok(cals.includes('fcna'));
});
it('getCalendar throws for unknown', () => {
assert.throws(() => getCalendar('nope'), /Unknown Hijri calendar/);
});
it('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 returns null for out-of-range date', () => {
assert.strictEqual(toGregorian(1317, 1, 1), null);
});
// ─── Error cases ────────────────────────────────────────────────────────────
// Summary
const total = passed + failed;
console.log(`\n${total} tests total: ${passed} passed, ${failed} failed`);
if (failed > 0) process.exit(1);
describe('CJS error cases', () => {
it('toHijri throws on non-Date', () => {
assert.throws(() => toHijri('bad'), /Invalid Gregorian date/);
});
it('toGregorian returns null for out-of-range date', () => {
assert.strictEqual(toGregorian(1317, 1, 1), null);
});
});

431
test.mjs
View file

@ -1,6 +1,6 @@
// ESM test suite for hijri-core.
// Uses Node.js assert — no test framework needed.
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import {
toHijri,
@ -19,250 +19,255 @@ import {
hwNumeric,
} from './dist/index.mjs';
let passed = 0;
let failed = 0;
// ─── Exports exist ──────────────────────────────────────────────────────────
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]);
describe('exports', () => {
it('toHijri is a function', () => {
assert.equal(typeof toHijri, 'function');
});
it('toGregorian is a function', () => {
assert.equal(typeof toGregorian, 'function');
});
it('isValidHijriDate is a function', () => {
assert.equal(typeof isValidHijriDate, 'function');
});
it('daysInHijriMonth is a function', () => {
assert.equal(typeof daysInHijriMonth, 'function');
});
it('registerCalendar is a function', () => {
assert.equal(typeof registerCalendar, 'function');
});
it('getCalendar is a function', () => {
assert.equal(typeof getCalendar, 'function');
});
it('listCalendars is a function', () => {
assert.equal(typeof listCalendars, 'function');
});
it('hDatesTable is an array with > 180 entries', () => {
assert.ok(Array.isArray(hDatesTable));
assert.ok(hDatesTable.length > 180);
});
it('hmLong has 12 entries', () => {
assert.equal(hmLong.length, 12);
});
it('hmMedium has 12 entries', () => {
assert.equal(hmMedium.length, 12);
});
it('hmShort has 12 entries', () => {
assert.equal(hmShort.length, 12);
});
it('hwLong has 7 entries', () => {
assert.equal(hwLong.length, 7);
});
it('hwShort has 7 entries', () => {
assert.equal(hwShort.length, 7);
});
it('hwNumeric has 7 entries [1..7]', () => {
assert.equal(hwNumeric.length, 7);
assert.deepEqual(hwNumeric, [1, 2, 3, 4, 5, 6, 7]);
});
});
// ─── 2. UAQ toGregorian ───────────────────────────────────────────────────────
// ─── 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');
describe('UAQ toGregorian', () => {
it('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');
});
it('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');
});
it('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');
});
it('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 ───────────────────────────────────────────────────────────
// ─── 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);
describe('UAQ toHijri', () => {
it('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);
});
it('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 ──────────────────────────────────────────────────
// ─── 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);
describe('UAQ isValid', () => {
it('1444/9/1 = true', () => {
assert.equal(isValidHijriDate(1444, 9, 1), true);
});
it('1317/1/1 = false (before table)', () => {
assert.equal(isValidHijriDate(1317, 1, 1), false);
});
it('1501/1/1 = false (sentinel)', () => {
assert.equal(isValidHijriDate(1501, 1, 1), false);
});
it('month 0 = false', () => {
assert.equal(isValidHijriDate(1444, 0, 1), false);
});
it('month 13 = false', () => {
assert.equal(isValidHijriDate(1444, 13, 1), false);
});
});
// ─── 5. daysInHijriMonth ──────────────────────────────────────────────────────
// ─── 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);
describe('UAQ daysInMonth', () => {
it('Ramadan 1444 = 29 days', () => {
assert.equal(daysInHijriMonth(1444, 9), 29);
});
it('throws for month 0', () => {
assert.throws(() => daysInHijriMonth(1444, 0), /month must be 1-12/);
});
it('throws for month 13', () => {
assert.throws(() => daysInHijriMonth(1444, 13), /month must be 1-12/);
});
});
// ─── 6. FCNA toGregorian ──────────────────────────────────────────────────────
// ─── 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');
describe('FCNA toGregorian', () => {
it('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');
});
it('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 ──────────────────────────────────────────────────────────
// ─── 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);
describe('FCNA toHijri', () => {
it('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);
});
});
// ─── 8. FCNA round-trips ──────────────────────────────────────────────────────
// ─── 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);
describe('FCNA round-trips', () => {
it('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);
});
it('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 ──────────────────────────────────────────────────────────
// ─── 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);
describe('FCNA isValid', () => {
it('1/1/1 = true', () => {
assert.equal(isValidHijriDate(1, 1, 1, { calendar: 'fcna' }), true);
});
it('1600/1/1 = true', () => {
assert.equal(isValidHijriDate(1600, 1, 1, { calendar: 'fcna' }), true);
});
it('0/1/1 = false', () => {
assert.equal(isValidHijriDate(0, 1, 1, { calendar: 'fcna' }), false);
});
});
// ─── 10. listCalendars ────────────────────────────────────────────────────────
// ─── FCNA daysInMonth invalid month ─────────────────────────────────────────
test('listCalendars includes uaq and fcna', () => {
const cals = listCalendars();
assert.ok(cals.includes('uaq'));
assert.ok(cals.includes('fcna'));
describe('FCNA daysInMonth', () => {
it('throws for month 0', () => {
assert.throws(() => daysInHijriMonth(1446, 0, { calendar: 'fcna' }), /month must be 1-12/);
});
it('throws for month 13', () => {
assert.throws(() => daysInHijriMonth(1446, 13, { calendar: 'fcna' }), /month must be 1-12/);
});
});
// ─── 11. getCalendar throws for unknown ───────────────────────────────────────
// ─── Registry ───────────────────────────────────────────────────────────────
test('getCalendar throws for unknown calendar', () => {
assert.throws(
() => getCalendar('nonexistent'),
/Unknown Hijri calendar/,
);
describe('registry', () => {
it('listCalendars includes uaq and fcna', () => {
const cals = listCalendars();
assert.ok(cals.includes('uaq'));
assert.ok(cals.includes('fcna'));
});
it('getCalendar throws for unknown calendar', () => {
assert.throws(() => getCalendar('nonexistent'), /Unknown Hijri calendar/);
});
it('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);
});
});
// ─── 12. Custom calendar registration ────────────────────────────────────────
// ─── Error cases ────────────────────────────────────────────────────────────
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);
describe('error cases', () => {
it('toHijri throws on non-Date input', () => {
assert.throws(() => toHijri('2023-03-23'), /Invalid Gregorian date/);
});
it('toHijri throws on invalid Date', () => {
assert.throws(() => toHijri(new Date('invalid')), /Invalid Gregorian date/);
});
it('UAQ toGregorian returns null for out-of-range date', () => {
assert.strictEqual(toGregorian(1317, 1, 1), null);
});
});
// ─── 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 returns null for out-of-range date', () => {
assert.strictEqual(toGregorian(1317, 1, 1), null);
});
// ─── Summary ─────────────────────────────────────────────────────────────────
const total = passed + failed;
console.log(`\n${total} tests total: ${passed} passed, ${failed} failed`);
if (failed > 0) process.exit(1);

View file

@ -9,9 +9,12 @@
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"outDir": "dist",
"rootDir": "src",
"types": ["node"]
},
"include": ["src"]
"include": ["src"],
"exclude": ["node_modules", "dist"]
}