mirror of
https://github.com/acamarata/luxon-hijri.git
synced 2026-06-30 18:54:28 +00:00
chore: P1 final polish — type accuracy, AGENTS.md sync, E5/E6 refinements
This commit is contained in:
parent
4b1a1fc835
commit
a115ecc2a2
15 changed files with 181 additions and 26 deletions
19
.github/wiki/_Sidebar.md
vendored
19
.github/wiki/_Sidebar.md
vendored
|
|
@ -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
40
.github/wiki/benchmarks/index.md
vendored
Normal 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
|
||||
```
|
||||
45
.github/wiki/guides/advanced.md
vendored
45
.github/wiki/guides/advanced.md
vendored
|
|
@ -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`);
|
||||
|
|
|
|||
4
.github/wiki/guides/quickstart.md
vendored
4
.github/wiki/guides/quickstart.md
vendored
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
22
.github/workflows/ci.yml
vendored
22
.github/workflows/ci.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
9
.github/workflows/wiki-sync.yml
vendored
9
.github/workflows/wiki-sync.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
[](https://www.npmjs.com/package/luxon-hijri)
|
||||
[](https://github.com/acamarata/luxon-hijri/actions/workflows/ci.yml)
|
||||
[](./LICENSE)
|
||||
[](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).
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
18
src/types.ts
18
src/types.ts
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
Loading…
Reference in a new issue