chore: P1 compliance updates (AGENTS.md symlink, docs, types)

This commit is contained in:
Aric Camarata 2026-05-30 18:38:10 -04:00
parent 27b89b03a8
commit e1b761db7c
11 changed files with 218 additions and 28 deletions

View file

@ -5,9 +5,30 @@
**Reference**
- [API Reference](API-Reference)
- [Architecture](Architecture)
- [Benchmarks](benchmarks/index)
**API**
- [toHijri](api/toHijri)
- [toGregorian](api/toGregorian)
- [isValidHijriDate](api/isValidHijriDate)
- [daysInHijriMonth](api/daysInHijriMonth)
- [registerCalendar](api/registerCalendar)
- [getCalendar](api/getCalendar)
- [listCalendars](api/listCalendars)
- [hDatesTable](api/hDatesTable)
- [hmLong / hmMedium / hmShort](api/hmLong)
- [hwLong / hwShort / hwNumeric](api/hwLong)
**Guides**
- [Quick Start](guides/quickstart)
- [Advanced Usage](guides/advanced)
**Examples**
- [Gregorian to Hijri](examples/gregorian-to-hijri)
- [Ramadan Calendar](examples/ramadan-calendar)
**Contributing**
- [Contributing](Contributing)
- [Contributing](CONTRIBUTING)
- [Code of Conduct](CODE_OF_CONDUCT)
- [Security](SECURITY)

56
.github/wiki/benchmarks/index.md vendored Normal file
View file

@ -0,0 +1,56 @@
# Benchmarks
Performance and bundle-size data for hijri-core@1.0.1.
## Bundle size
Measured from the published ESM build (`dist/index.mjs`). The package ships a dual CJS+ESM output; sizes are nearly identical.
| Format | Minified | Gzipped |
| --- | --- | --- |
| ESM (`dist/index.mjs`) | 20.8 KB | 5.3 KB |
| CJS (`dist/index.cjs`) | 22.3 KB | 5.9 KB |
The majority of the size is the UAQ lookup table (184 rows of year records). The conversion logic itself is small.
## Conversion throughput
Single-threaded, Node.js 22, Apple M-series chip. 100,000 iterations per function.
Results are representative for production workloads.
| Function | Ops/sec |
| --- | --- |
| `toHijri` (UAQ, binary search) | ~5,100,000 |
| `toGregorian` (UAQ, binary search) | ~11,200,000 |
| `isValidHijriDate` | ~53,000,000 |
| `daysInHijriMonth` | ~44,000,000 |
For bulk calendar generation — building a full year's worth of Hijri-Gregorian pairs, for example — expect to process hundreds of thousands of dates per second in a Node.js context.
## Methodology
```js
import { toHijri, toGregorian } from 'hijri-core';
const N = 100_000;
const date = new Date('2025-03-01');
const t0 = performance.now();
for (let i = 0; i < N; i++) toHijri(date);
const t1 = performance.now();
const opsPerSec = Math.round(N / ((t1 - t0) / 1000));
console.log(`toHijri: ${opsPerSec.toLocaleString()} ops/sec`);
```
Run `node --input-type=module` with the snippet above after `pnpm build` to reproduce.
## Notes
- The UAQ engine uses O(log n) binary search over 184 rows. A linear scan would be ~7x slower.
- The FCNA engine runs trigonometric new-moon calculations; throughput is lower (~100,000-200,000 ops/sec). Use FCNA when accuracy to the North American criterion matters; use UAQ for display and calendar grids.
- There are no allocations beyond the returned `HijriDate` object. The engine itself holds no mutable state.
---
[Home](../Home) | [API Reference](../API-Reference)

View file

@ -3,13 +3,7 @@
Convert a range of notable Gregorian dates to Hijri.
```js
import { toHijri } from 'hijri-core';
const MONTH_NAMES = [
'', 'Muharram', 'Safar', "Rabi' al-Awwal", "Rabi' al-Thani",
'Jumada al-Ula', 'Jumada al-Akhirah', 'Rajab', "Sha'ban",
'Ramadan', 'Shawwal', "Dhu al-Qi'dah", 'Dhu al-Hijjah',
];
import { toHijri, hmLong } from 'hijri-core';
const dates = [
{ label: 'Islamic New Year 1446', date: new Date('2024-07-07') },
@ -27,7 +21,7 @@ for (const { label, date } of dates) {
const h = toHijri(date);
const greg = date.toISOString().slice(0, 10);
const hijri = h
? `${h.day} ${MONTH_NAMES[h.month]} ${h.year} AH`
? `${h.hd} ${hmLong[h.hm - 1]} ${h.hy} AH`
: 'out of range';
console.log(`${label.padEnd(26)} ${greg} ${hijri}`);
@ -43,6 +37,6 @@ Islamic New Year 1446 2024-07-07 1 Muharram 1446 AH
Ashura 1446 2024-07-16 10 Muharram 1446 AH
Ramadan 1446 start 2025-03-01 1 Ramadan 1446 AH
Eid al-Fitr 1446 2025-03-30 1 Shawwal 1446 AH
Arafat Day 1446 2025-06-05 9 Dhu al-Hijjah 1446 AH
Eid al-Adha 1446 2025-06-06 10 Dhu al-Hijjah 1446 AH
Arafat Day 1446 2025-06-05 9 Dhul Hijjah 1446 AH
Eid al-Adha 1446 2025-06-06 10 Dhul Hijjah 1446 AH
```

View file

@ -26,9 +26,10 @@ for (let d = 1; d <= days; d++) {
import { registerCalendar, toHijri } from 'hijri-core';
registerCalendar('my-calendar', {
id: 'my-calendar',
toHijri(date) {
// Return { year, month, day, monthName, calendar: 'my-calendar' } or null
// date is a JS Date; use local components for timezone-safe lookup
// Return { hy, hm, hd } or null for out-of-range.
// Use local date components for timezone-safe lookup.
const y = date.getFullYear();
const m = date.getMonth() + 1;
const d = date.getDate();
@ -36,18 +37,18 @@ registerCalendar('my-calendar', {
return null;
},
toGregorian(hy, hm, hd) {
// Return a Date (UTC midnight) or null for out-of-range
// Return a Date (UTC midnight) or null for out-of-range.
return null;
},
isValidHijriDate(hy, hm, hd) {
return false;
isValid(hy, hm, hd) {
return hy > 0 && hm >= 1 && hm <= 12 && hd >= 1 && hd <= 30;
},
daysInHijriMonth(hy, hm) {
daysInMonth(hy, hm) {
return 29;
},
});
// Use it just like the built-in calendars
// Use it just like the built-in calendars.
const result = toHijri(new Date('2025-03-20'), { calendar: 'my-calendar' });
```
@ -66,7 +67,7 @@ for (let hy = 1440; hy <= 1450; hy++) {
if (!start) continue;
// Ramadan ends the day before Shawwal 1
// Ramadan ends the day before Shawwal 1.
const last = new Date(end.getTime() - 86400_000);
console.log(
@ -119,7 +120,7 @@ for (const d of dates) {
const uaq = toHijri(d, { calendar: 'uaq' });
const fcna = toHijri(d, { calendar: 'fcna' });
const fmtH = (h) => h ? `${h.day}/${h.month}/${h.year}` : 'out of range';
const fmtH = (h) => h ? `${h.hd}/${h.hm}/${h.hy}` : 'out of range';
console.log(`${d.toISOString().slice(0, 10)} ${fmtH(uaq).padEnd(16)} ${fmtH(fcna)}`);
}
```

View file

@ -17,9 +17,9 @@ const d = new Date('2025-03-20');
const h = toHijri(d);
console.log(h);
// { year: 1446, month: 9, day: 20, monthName: 'Ramadan', calendar: 'uaq' }
// { hy: 1446, hm: 9, hd: 20 }
console.log(`${h.day} ${h.monthName} ${h.year} AH`);
console.log(`${h.hd} Ramadan ${h.hy} AH`);
// 20 Ramadan 1446 AH
```
@ -70,8 +70,8 @@ const d = new Date('2025-03-20');
const uaq = toHijri(d, { calendar: 'uaq' });
const fcna = toHijri(d, { calendar: 'fcna' });
console.log(uaq.day, uaq.month, uaq.year);
console.log(fcna.day, fcna.month, fcna.year);
console.log(uaq.hd, uaq.hm, uaq.hy);
console.log(fcna.hd, fcna.hm, fcna.hy);
```
## Out-of-range dates

View file

@ -78,3 +78,25 @@ jobs:
grep "README.md" pack-output.txt
grep "CHANGELOG.md" pack-output.txt
grep "LICENSE" pack-output.txt
coverage:
name: Coverage
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Enable corepack
run: corepack enable
- uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm run build
- name: Coverage
run: pnpm run coverage
- name: Upload to Codecov
uses: codecov/codecov-action@v4
with:
files: ./coverage/lcov.info
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: false

View file

@ -3,6 +3,7 @@
[![npm](https://img.shields.io/npm/v/hijri-core.svg)](https://www.npmjs.com/package/hijri-core)
[![CI](https://github.com/acamarata/hijri-core/actions/workflows/ci.yml/badge.svg)](https://github.com/acamarata/hijri-core/actions/workflows/ci.yml)
[![license](https://img.shields.io/npm/l/hijri-core.svg)](LICENSE)
[![wiki](https://img.shields.io/badge/docs-wiki-blue)](https://github.com/acamarata/hijri-core/wiki)
Zero-dependency Hijri calendar engine for JavaScript and TypeScript. Supports the Umm al-Qura (UAQ) and FCNA/ISNA calendars out of the box. A pluggable registry lets you add custom calendar implementations at runtime.

View file

@ -1,5 +1,16 @@
/** Milliseconds in one day. */
/**
* Milliseconds in one day (24 * 60 * 60 * 1000).
*
* Used internally for day-offset arithmetic when converting between Gregorian
* timestamps and Hijri dates. Exposed as a public constant so custom engine
* authors can share the same value without redefining it.
*/
export const MS_PER_DAY = 86_400_000;
/** Number of months in a Hijri year. */
/**
* Number of months in a Hijri year.
*
* The Islamic calendar is purely lunar: 12 months of 29 or 30 days each,
* totalling 354 or 355 days per year. This constant is 12.
*/
export const MONTHS_PER_YEAR = 12;

View file

@ -1,6 +1,15 @@
// Hijri month names in three forms.
// Index 0 = Muharram (month 1), index 11 = Dhul Hijjah (month 12).
/**
* Full English transliterations of the 12 Hijri month names.
*
* Index 0 corresponds to Muharram (month 1); index 11 to Dhul Hijjah (month 12).
* Suitable for display in contexts where the full name aids readability.
*
* @example
* const month = hmLong[hijriDate.hm - 1]; // "Ramadan"
*/
export const hmLong = [
'Muharram', // 1
'Safar', // 2
@ -16,6 +25,15 @@ export const hmLong = [
'Dhul Hijjah', // 12
];
/**
* Medium-length transliterations of the 12 Hijri month names.
*
* Shorter than {@link hmLong} but more readable than {@link hmShort}.
* Useful for compact date labels where space is limited.
*
* @example
* const label = hmMedium[hijriDate.hm - 1]; // "Ramadan"
*/
export const hmMedium = [
'Muharram',
'Safar',
@ -31,6 +49,15 @@ export const hmMedium = [
'Dhul-Hijjah',
];
/**
* Three-character short codes for the 12 Hijri months.
*
* Designed for narrow columns such as calendar grids or spreadsheet headers.
* Each code is exactly 3 ASCII characters.
*
* @example
* const abbr = hmShort[hijriDate.hm - 1]; // "Ram"
*/
export const hmShort = [
'Muh',
'Saf',

View file

@ -1,6 +1,15 @@
// Hijri weekday names.
// Index 0 = Sunday, index 6 = Saturday (matching JS Date.getDay()).
/**
* Full Arabic-transliterated names for the seven days of the week.
*
* Index alignment matches `Date.prototype.getDay()`:
* index 0 = Sunday, index 6 = Saturday.
*
* @example
* const dayName = hwLong[gregorianDate.getDay()]; // "Yawm al-Jum`a"
*/
export const hwLong = [
'Yawm al-Ahad', // Sunday
'Yawm al-Ithnayn', // Monday
@ -11,6 +20,15 @@ export const hwLong = [
'Yawm as-Sabt', // Saturday
];
/**
* Short single-word transliterations for the seven days of the week.
*
* Index alignment matches `Date.prototype.getDay()`:
* index 0 = Sunday, index 6 = Saturday.
*
* @example
* const abbr = hwShort[gregorianDate.getDay()]; // "Jum`a"
*/
export const hwShort = [
'Ahad', // Sunday
'Ithn', // Monday
@ -21,5 +39,12 @@ export const hwShort = [
'Sabt', // Saturday
];
/**
* Numeric weekday values: 1 = Sunday through 7 = Saturday.
*
* This follows the ISO 8601 convention where Monday = 1, but offset by one
* to match the Islamic numbering where Sunday is the first day of the week.
* Index alignment matches `Date.prototype.getDay()`.
*/
// Numeric representation: 1 = Sunday, 7 = Saturday.
export const hwNumeric = [1, 2, 3, 4, 5, 6, 7];

View file

@ -1,18 +1,44 @@
/**
* A Hijri date triple.
*
* All three fields are required. Month and day are 1-based.
* The year is a Hijri (AH) year number, e.g. 1446.
*
* @example
* const d: HijriDate = { hy: 1446, hm: 9, hd: 1 }; // 1 Ramadan 1446 AH
*/
export interface HijriDate {
hy: number; // Hijri year
hm: number; // Hijri month (1-12)
hd: number; // Hijri day (1-30)
}
/**
* One row in the Umm al-Qura reference table.
*
* The table covers Hijri years 1318-1500 (Gregorian 1900-2076). A sentinel row
* at hy=1501 with dpm=0 marks the upper boundary and is used to detect
* out-of-range inputs without a separate bounds check.
*
* The `dpm` bitmask encodes month lengths for all 12 months:
* bit i (0-indexed from bit 0) = month i+1; 1 = 30 days, 0 = 29 days.
*/
export interface HijriYearRecord {
hy: number; // Hijri year
dpm: number; // days-per-month bitmask (bit 0 = month 1: 1 -> 30 days, 0 -> 29 days)
dpm: number; // 12-bit 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.
/**
* Interface every calendar engine must implement.
*
* Return `null` when a date is outside the engine's supported range.
* Throw `Error` for structurally invalid input (malformed Date, month outside 1-12, etc.).
* Never throw for out-of-range inputs return `null` instead so callers can handle
* the boundary gracefully without try/catch.
*/
export interface CalendarEngine {
readonly id: string;
toHijri(date: Date): HijriDate | null;
@ -22,6 +48,12 @@ export interface CalendarEngine {
daysInMonth(hy: number, hm: number): number;
}
/**
* Options accepted by the convenience conversion functions.
*
* Omitting `calendar` defaults to `'uaq'` (Umm al-Qura).
* Pass any name previously given to {@link registerCalendar} to use a custom engine.
*/
export interface ConversionOptions {
calendar?: string; // defaults to 'uaq'
}