diff --git a/.github/wiki/_Sidebar.md b/.github/wiki/_Sidebar.md index 15d70db..55e4aa6 100644 --- a/.github/wiki/_Sidebar.md +++ b/.github/wiki/_Sidebar.md @@ -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) diff --git a/.github/wiki/benchmarks/index.md b/.github/wiki/benchmarks/index.md new file mode 100644 index 0000000..323d6db --- /dev/null +++ b/.github/wiki/benchmarks/index.md @@ -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 +``` diff --git a/.github/wiki/guides/advanced.md b/.github/wiki/guides/advanced.md index 74f9270..66dc1a4 100644 --- a/.github/wiki/guides/advanced.md +++ b/.github/wiki/guides/advanced.md @@ -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`); diff --git a/.github/wiki/guides/quickstart.md b/.github/wiki/guides/quickstart.md index 39be8e2..8c07fe7 100644 --- a/.github/wiki/guides/quickstart.md +++ b/.github/wiki/guides/quickstart.md @@ -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 } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5743612..4159d1a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/.github/workflows/wiki-sync.yml b/.github/workflows/wiki-sync.yml index e226b26..cc840b1 100644 --- a/.github/workflows/wiki-sync.yml +++ b/.github/workflows/wiki-sync.yml @@ -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 diff --git a/README.md b/README.md index 644ce3d..7122cd4 100644 --- a/README.md +++ b/README.md @@ -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). diff --git a/src/formatPatterns.ts b/src/formatPatterns.ts index 67bf49e..3351414 100644 --- a/src/formatPatterns.ts +++ b/src/formatPatterns.ts @@ -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 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 = { diff --git a/src/hDates.ts b/src/hDates.ts index 47afd50..e9c9d5d 100644 --- a/src/hDates.ts +++ b/src/hDates.ts @@ -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'; diff --git a/src/hMonths.ts b/src/hMonths.ts index 324d9f6..b03c3c8 100644 --- a/src/hMonths.ts +++ b/src/hMonths.ts @@ -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'; diff --git a/src/hWeekdays.ts b/src/hWeekdays.ts index a8002c7..cd07c32 100644 --- a/src/hWeekdays.ts +++ b/src/hWeekdays.ts @@ -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'; diff --git a/src/toGregorian.ts b/src/toGregorian.ts index 10ef83f..ec53c89 100644 --- a/src/toGregorian.ts +++ b/src/toGregorian.ts @@ -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'; diff --git a/src/toHijri.ts b/src/toHijri.ts index 6fadb37..04acb90 100644 --- a/src/toHijri.ts +++ b/src/toHijri.ts @@ -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'; diff --git a/src/types.ts b/src/types.ts index 3c6f820..b988bd3 100644 --- a/src/types.ts +++ b/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'; diff --git a/src/utils.ts b/src/utils.ts index 7f04ef3..f82fbf0 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -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';