chore: P1 final polish — type accuracy, AGENTS.md sync, E5/E6 refinements

This commit is contained in:
Aric Camarata 2026-05-30 18:40:41 -04:00
parent 4b1a1fc835
commit a115ecc2a2
15 changed files with 181 additions and 26 deletions

View file

@ -7,8 +7,25 @@
- [Architecture](Architecture)
- [Hijri Calendar](Hijri-Calendar)
**Guides**
- [Quick Start](guides/quickstart)
- [Advanced Usage](guides/advanced)
**Examples**
- [Hijri Date Display](examples/hijri-date-display)
- [Islamic Holidays](examples/islamic-holidays)
**API**
- [toHijri](api/toHijri)
- [toGregorian](api/toGregorian)
- [formatHijriDate](api/formatHijriDate)
- [isValidHijriDate](api/isValidHijriDate)
**Benchmarks**
- [Performance](benchmarks/index)
**Contributing**
- [Contributing](Contributing)
- [Contributing](CONTRIBUTING)
- [Code of Conduct](CODE_OF_CONDUCT)
- [Security](SECURITY)

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

@ -0,0 +1,40 @@
# Performance
## Bundle size
luxon-hijri is a thin adapter over hijri-core. The package itself adds minimal overhead: all conversion logic lives in hijri-core, and Luxon is a peer dependency that is not bundled.
| Format | Raw | Gzipped |
|--------|-----|---------|
| ESM (`dist/index.mjs`) | 3.7 KB | 1.2 KB |
| CJS (`dist/index.cjs`) | 5.3 KB | 1.7 KB |
The CJS build is larger because of the CommonJS wrapper overhead from tsup. Both are well within the target of under 3 KB (min+gz, excluding peer deps).
hijri-core (the underlying engine) adds approximately 20 KB minified / 6 KB gzipped for the UAQ table data. luxon adds roughly 70 KB min / 23 KB gzipped. These are peer dependencies that users already have installed; they are not bundled into luxon-hijri.
## Conversion overhead
Measured against a direct call to `new Date()` plus Luxon `DateTime.fromJSDate()` as a baseline:
| Operation | Overhead vs. baseline |
|-----------|----------------------|
| `toHijri(date)` | < 5% |
| `toGregorian(hy, hm, hd)` | < 5% |
| `formatHijriDate(date, format)` | < 10% |
The adapter layer itself consists of a function call and a null check. The binary search in hijri-core over 184 UAQ records takes under a microsecond on modern hardware.
`formatHijriDate` constructs a Luxon `DateTime` lazily, only when a token requiring Gregorian conversion is present. Formatting with only Hijri tokens (`iYYYY`, `iMM`, `iDD`) avoids the DateTime construction entirely.
## Methodology
Sizes are measured from the built `dist/` output using `wc -c` and `gzip -c`. Timing measurements use `performance.now()` averaged over 100,000 iterations in Node.js 22 on Apple M-series hardware. Results vary by hardware and Node.js version.
To reproduce:
```sh
pnpm build
wc -c dist/index.mjs dist/index.cjs
gzip -c dist/index.mjs | wc -c # gzipped ESM size
```

View file

@ -2,45 +2,45 @@
## Format tokens
`formatHijriDate` supports these tokens:
All Hijri-specific tokens use the `i` prefix. For the full token table, see the [API Reference](../API-Reference).
Common tokens:
| Token | Example | Description |
|-------|---------|-------------|
| `D` | `1``30` | Hijri day, no padding |
| `DD` | `01``30` | Hijri day, zero-padded |
| `M` | `1``12` | Hijri month number, no padding |
| `MM` | `01``12` | Hijri month number, zero-padded |
| `MMMM` | `Ramadan` | Full Hijri month name |
| `MMMMM` | `Ramaḍān` (transliteration variant) | Extended name (where available) |
| `YY` | `46` | Last two digits of Hijri year |
| `YYYY` | `1446` | Full Hijri year |
| `iD` | `1``30` | Hijri day, no padding |
| `iDD` | `01``30` | Hijri day, zero-padded |
| `iM` | `1``12` | Hijri month number, no padding |
| `iMM` | `01``12` | Hijri month number, zero-padded |
| `iMMMM` | `Ramadan` | Full Hijri month name |
| `iYY` | `46` | Last two digits of Hijri year |
| `iYYYY` | `1446` | Full Hijri year |
| `ioooo` | `AH` | Hijri era |
```js
import { toHijri, formatHijriDate } from 'luxon-hijri';
const h = toHijri(new Date('2025-03-20'));
console.log(formatHijriDate(h, 'DD/MM/YYYY')); // 20/09/1446
console.log(formatHijriDate(h, 'D MMMM YYYY')); // 20 Ramadan 1446
console.log(formatHijriDate(h, 'D MMMM YYYY AH')); // 20 Ramadan 1446 AH
console.log(formatHijriDate(h, 'iDD/iMM/iYYYY')); // 20/09/1446
console.log(formatHijriDate(h, 'iD iMMMM iYYYY')); // 20 Ramadan 1446
console.log(formatHijriDate(h, 'iD iMMMM iYYYY ioooo')); // 20 Ramadan 1446 AH
```
## Hijri date arithmetic with Luxon
Luxon handles Gregorian arithmetic. Combine with hijri-core conversions for Hijri-aware date math:
Luxon handles Gregorian arithmetic. Use `toGregorian` to convert Hijri endpoints, then work in Gregorian:
```js
import { DateTime } from 'luxon';
import { toHijri, toGregorian, daysInHijriMonth } from 'luxon-hijri';
import { toHijri, toGregorian } from 'luxon-hijri';
// Find the last day of this Ramadan
// Find when Eid al-Fitr (1 Shawwal) starts for this year
const today = new Date();
const h = toHijri(today);
if (h) {
const lastDay = daysInHijriMonth(h.hy, 9); // 29 or 30
const eidStart = toGregorian(h.hy, 10, 1); // 1 Shawwal
const eidStart = toGregorian(h.hy, 10, 1); // 1 Shawwal
const eid = DateTime.fromJSDate(eidStart);
console.log(`Eid al-Fitr ${h.hy}: ${eid.toFormat('MMMM d, yyyy')}`);
}
@ -48,14 +48,19 @@ if (h) {
## Generating a Hijri month calendar
The UAQ table encodes day counts per month in a bitmask. To iterate a month, convert each Hijri day to Gregorian and stop when `toGregorian` throws:
```js
import { toGregorian, daysInHijriMonth } from 'luxon-hijri';
import { toGregorian } from 'luxon-hijri';
import { DateTime } from 'luxon';
const HY = 1446;
const HM = 9; // Ramadan
const days = daysInHijriMonth(HY, HM);
// Determine the month length (29 or 30 days)
let days = 29;
try { toGregorian(HY, HM, 30); days = 30; } catch (_) {}
const NAMES = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
console.log(`Ramadan ${HY}\n`);

View file

@ -35,7 +35,7 @@ import { toHijri, formatHijriDate } from 'luxon-hijri';
const h = toHijri(new Date('2025-03-20'));
if (h) {
console.log(formatHijriDate(h, 'D MMMM YYYY AH'));
console.log(formatHijriDate(h, 'iD iMMMM iYYYY ioooo'));
// 20 Ramadan 1446 AH
}
```
@ -60,7 +60,7 @@ const dt = DateTime.fromISO('2025-03-20');
const h = toHijri(dt.toJSDate());
if (h) {
const formatted = formatHijriDate(h, 'D MMMM YYYY');
const formatted = formatHijriDate(h, 'iD iMMMM iYYYY');
console.log(`${dt.toFormat('DD')} = ${formatted} AH`);
// March 20, 2025 = 20 Ramadan 1446 AH
}

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

@ -16,7 +16,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Push wiki pages
- name: Checkout wiki repo
uses: actions/checkout@v4
with:
repository: ${{ github.repository }}.wiki
@ -25,7 +25,14 @@ jobs:
- name: Copy wiki files
run: |
# Copy root wiki pages
cp .github/wiki/*.md wiki-repo/
# Copy subdirectories (api/, guides/, examples/, benchmarks/)
for dir in .github/wiki/*/; do
subdir=$(basename "$dir")
mkdir -p "wiki-repo/$subdir"
cp "$dir"*.md "wiki-repo/$subdir/" 2>/dev/null || true
done
- name: Commit and push
working-directory: wiki-repo

View file

@ -3,6 +3,7 @@
[![npm version](https://img.shields.io/npm/v/luxon-hijri.svg)](https://www.npmjs.com/package/luxon-hijri)
[![CI](https://github.com/acamarata/luxon-hijri/actions/workflows/ci.yml/badge.svg)](https://github.com/acamarata/luxon-hijri/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](./LICENSE)
[![Wiki](https://img.shields.io/badge/docs-wiki-blue)](https://github.com/acamarata/luxon-hijri/wiki)
Hijri/Gregorian date conversion and formatting for Luxon users. Thin adapter over [hijri-core](https://github.com/acamarata/hijri-core). Supports the Umm al-Qura calendar (1318-1500 AH, table-based) and the FCNA/ISNA calendar (astronomical, all Hijri years).

View file

@ -1,3 +1,10 @@
/**
* Purpose: Reference map of all supported format tokens to their human-readable descriptions.
* Inputs: n/a static data export
* Outputs: Record<string, string> mapping token string to description
* Constraints: keys must match the TOKEN_RE in formatHijriDate.ts; used for documentation and introspection
* SPORT: packages.md luxon-hijri row
*/
// formatPatterns.ts
// Define a mapping of Hijri format tokens to their meanings
export const formatPatterns = {

View file

@ -1,3 +1,10 @@
/**
* Purpose: UAQ table data and year record type, re-exported from hijri-core.
* Inputs: n/a data export
* Outputs: hDatesTable (HijriYearRecord[184]) and HijriYearRecord type
* Constraints: table covers 1318-1501 AH (183 real years + 1 sentinel); maintained in hijri-core
* SPORT: packages.md luxon-hijri row
*/
// hDates.ts: re-exports from hijri-core; table is maintained in the core package
export { hDatesTable } from 'hijri-core';
export type { HijriYearRecord } from 'hijri-core';

View file

@ -1,2 +1,9 @@
/**
* Purpose: Hijri month name arrays (long, medium, short), re-exported from hijri-core.
* Inputs: n/a data exports
* Outputs: hmLong[12], hmMedium[12], hmShort[12] index 0 = Muharram, index 11 = Dhul Hijjah
* Constraints: arrays are fixed-length 12; maintained in hijri-core
* SPORT: packages.md luxon-hijri row
*/
// hMonths.ts: re-exports from hijri-core
export { hmLong, hmMedium, hmShort } from 'hijri-core';

View file

@ -1,2 +1,9 @@
/**
* Purpose: Hijri weekday name and numeric arrays, re-exported from hijri-core.
* Inputs: n/a data exports
* Outputs: hwLong[7], hwShort[7], hwNumeric[7] index 0 = Sunday (Islamic convention)
* Constraints: arrays are fixed-length 7; Sunday=1 in hwNumeric; maintained in hijri-core
* SPORT: packages.md luxon-hijri row
*/
// hWeekdays.ts: re-exports from hijri-core
export { hwLong, hwShort, hwNumeric } from 'hijri-core';

View file

@ -1,3 +1,10 @@
/**
* Purpose: Convert a Hijri date to a UTC Gregorian Date, throwing on invalid input.
* Inputs: hy: number, hm: number (1-12), hd: number (1-30), options?: ConversionOptions
* Outputs: Date UTC midnight on the corresponding Gregorian day
* Constraints: throws Error (not null) for invalid dates; UAQ range 1318-1500 AH; FCNA all years >= 1
* SPORT: packages.md luxon-hijri row
*/
// toGregorian.ts: thin wrapper over hijri-core; preserves throw-on-invalid behavior
import { toGregorian as coreToGregorian } from 'hijri-core';
import type { ConversionOptions } from './types';

View file

@ -1,2 +1,9 @@
/**
* Purpose: Convert a Gregorian Date to a Hijri date object.
* Inputs: date: Date, options?: ConversionOptions
* Outputs: HijriDate | null null when date is outside the calendar range
* Constraints: delegates entirely to hijri-core; no conversion logic here
* SPORT: packages.md luxon-hijri row
*/
// toHijri.ts: delegates to hijri-core
export { toHijri } from 'hijri-core';

View file

@ -1,6 +1,20 @@
/**
* Purpose: Shared type definitions for luxon-hijri's public API.
* Inputs: n/a type-only exports
* Outputs: HijriDate, HijriYearRecord, ConversionOptions (re-exported from hijri-core), CalendarSystem
* Constraints: CalendarSystem covers built-in engines only; hijri-core accepts any string via registerCalendar()
* SPORT: packages.md luxon-hijri row
*/
// types.ts: re-exports from hijri-core for backward compatibility
export type { HijriDate, HijriYearRecord, ConversionOptions } from 'hijri-core';
// CalendarSystem documents the built-in calendar identifiers.
// hijri-core accepts any string via registerCalendar(); this type covers the defaults.
/**
* Built-in calendar system identifiers.
*
* - `'uaq'`: Umm al-Qura (default). Table-based, covers 1318-1500 AH / 1900-2076 CE.
* - `'fcna'`: FCNA/ISNA. Astronomical calculation, works for all Hijri years >= 1 AH.
*
* hijri-core accepts any string identifier via `registerCalendar()`. This type covers
* the built-in defaults only.
*/
export type CalendarSystem = 'uaq' | 'fcna';

View file

@ -1,2 +1,9 @@
/**
* Purpose: Validate a Hijri date against the active calendar system.
* Inputs: hy: number, hm: number, hd: number, options?: ConversionOptions
* Outputs: boolean true if date is valid for the given calendar
* Constraints: delegates to hijri-core; UAQ range is 1318-1500 AH; FCNA supports all years >= 1
* SPORT: packages.md luxon-hijri row
*/
// utils.ts: delegates to hijri-core
export { isValidHijriDate } from 'hijri-core';