From 250dda20d4c3b06f783b8e684ba7183028bbe135 Mon Sep 17 00:00:00 2001 From: Aric Camarata Date: Fri, 29 May 2026 07:15:50 -0400 Subject: [PATCH] chore: E6 polish wiki content + ADR-015 CI updates (P1) --- .github/wiki/CODE_OF_CONDUCT.md | 29 ++++++++ .github/wiki/CONTRIBUTING.md | 54 +++++++++++++++ .github/wiki/SECURITY.md | 30 +++++++++ .github/wiki/_Footer.md | 1 + .github/wiki/_Sidebar.md | 18 +++++ .github/wiki/benchmarks/index.md | 47 +++++++++++++ .github/wiki/examples/basic-usage.md | 76 +++++++++++++++++++++ .github/wiki/guides/advanced.md | 91 ++++++++++++++++++++++++++ .github/wiki/guides/quickstart.md | 98 ++++++++++++++++++++++++++++ .github/workflows/ci.yml | 12 ++-- README.md | 2 +- 11 files changed, 453 insertions(+), 5 deletions(-) create mode 100644 .github/wiki/CODE_OF_CONDUCT.md create mode 100644 .github/wiki/CONTRIBUTING.md create mode 100644 .github/wiki/SECURITY.md create mode 100644 .github/wiki/_Footer.md create mode 100644 .github/wiki/_Sidebar.md create mode 100644 .github/wiki/benchmarks/index.md create mode 100644 .github/wiki/examples/basic-usage.md create mode 100644 .github/wiki/guides/advanced.md create mode 100644 .github/wiki/guides/quickstart.md diff --git a/.github/wiki/CODE_OF_CONDUCT.md b/.github/wiki/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..4148a1c --- /dev/null +++ b/.github/wiki/CODE_OF_CONDUCT.md @@ -0,0 +1,29 @@ +# Code of Conduct + +## The short version + +Be respectful. Be constructive. Focus on the work, not the person. + +## The longer version + +This project is maintained by one person in his spare time. Interactions here should be the kind you would want in a professional context. + +Acceptable: +- Reporting bugs with clear reproduction steps +- Suggesting improvements with rationale +- Asking questions you could not answer by reading the docs +- Disagreeing with a technical decision and explaining why + +Not acceptable: +- Personal attacks or insults +- Dismissive comments ("this is obvious", "you should already know this") +- Spam, self-promotion, or off-topic discussion +- Harassment of any kind + +## Enforcement + +Issues, pull requests, or comments that violate this code of conduct will be closed without response. Repeat violations result in a block. + +## Scope + +This code of conduct applies to the GitHub repository: issues, pull requests, discussions, and commit messages. diff --git a/.github/wiki/CONTRIBUTING.md b/.github/wiki/CONTRIBUTING.md new file mode 100644 index 0000000..84d692f --- /dev/null +++ b/.github/wiki/CONTRIBUTING.md @@ -0,0 +1,54 @@ +# Contributing to temporal-hijri + +Thanks for your interest in contributing. This is a small, focused library and contributions are welcome. + +## Getting started + +```bash +git clone https://github.com/acamarata/temporal-hijri.git +cd temporal-hijri +pnpm install +pnpm build +pnpm test +``` + +All tests should pass before you start. + +## What to work on + +Check the [open issues](https://github.com/acamarata/temporal-hijri/issues) for anything tagged `help wanted` or `good first issue`. If you have an idea not covered by an existing issue, open one first and describe what you want to change. That avoids duplicate work. + +## Code style + +- TypeScript strict mode. No `any` without a comment explaining why. +- Class-based implementation following the Temporal Calendar Protocol interface. +- Each class method: one purpose. If you can describe it with "and", split it. +- Run `pnpm run format` before committing. CI will fail on formatting issues. +- Run `pnpm run lint` before committing. Fix all warnings, not just errors. + +## Tests + +- Add tests for any new method or changed behavior. +- Tests live in `test.mjs` (ESM) and `test-cjs.cjs` (CommonJS). Both must pass. +- Use the native Node.js `node:test` runner. No Jest, no Vitest. +- Test known Hijri dates. The `1 Ramadan 1444 = 23 March 2023` pair is a good anchor. +- The polyfill (`@js-temporal/polyfill`) must be installed for tests to run. + +## Temporal specification + +This package implements the [TC39 Temporal proposal](https://tc39.es/proposal-temporal/) Calendar Protocol (Stage 3). Before adding or changing behavior, read the relevant section of the specification. Deviations from the spec are not accepted unless the spec itself is ambiguous. + +## Pull requests + +- Keep PRs small and focused. One concern per PR. +- Write a clear description of what changed and why. +- Reference the issue number if one exists (`Fixes #42`). +- CI must be green before merge. This includes test, lint, typecheck, and pack-check. + +## Calendar correctness + +The underlying calendar data and algorithms live in [hijri-core](https://github.com/acamarata/hijri-core), not here. If you find a date conversion error, it likely belongs there. Open an issue in hijri-core first. + +## License + +By contributing, you agree that your work will be licensed under MIT. Copyright remains with Aric Camarata. diff --git a/.github/wiki/SECURITY.md b/.github/wiki/SECURITY.md new file mode 100644 index 0000000..e2d1544 --- /dev/null +++ b/.github/wiki/SECURITY.md @@ -0,0 +1,30 @@ +# Security Policy + +## Supported versions + +| Version | Supported | +| --- | --- | +| 1.x (latest) | Yes | +| < 1.0 | No | + +## Reporting a vulnerability + +temporal-hijri is a pure calendar computation library. It implements the Temporal Calendar Protocol over Hijri calendar data. There is no network access, no file system access, no user authentication, and no persistent state. + +Security vulnerabilities are unlikely given the surface area. That said, if you find something: + +1. **Do not open a public issue.** That exposes the vulnerability before a fix is available. +2. Email **aric.camarata@gmail.com** with the subject line "Security: temporal-hijri". +3. Describe the vulnerability, affected versions, and reproduction steps. +4. You will receive a response within 7 days. + +## What counts as a security issue here + +- An input that causes the library to execute arbitrary code +- A dependency with a known CVE that affects this package's behavior +- Prototype pollution via user-provided inputs + +## What does not count + +- Incorrect Hijri date calculations (that is a bug, not a security issue) +- Missing input validation that causes incorrect output but no code execution diff --git a/.github/wiki/_Footer.md b/.github/wiki/_Footer.md new file mode 100644 index 0000000..1cead02 --- /dev/null +++ b/.github/wiki/_Footer.md @@ -0,0 +1 @@ +[temporal-hijri](https://github.com/acamarata/temporal-hijri) · MIT License · [npm](https://www.npmjs.com/package/temporal-hijri) · [Issues](https://github.com/acamarata/temporal-hijri/issues) diff --git a/.github/wiki/_Sidebar.md b/.github/wiki/_Sidebar.md new file mode 100644 index 0000000..7ad4056 --- /dev/null +++ b/.github/wiki/_Sidebar.md @@ -0,0 +1,18 @@ +**[Home](Home)** + +**Guides** +- [Quick Start](guides/quickstart) +- [Advanced Usage](guides/advanced) + +**Examples** +- [Basic Usage](examples/basic-usage) + +**Reference** +- [API Reference](API-Reference) +- [Architecture](Architecture) +- [Benchmarks](benchmarks/index) + +**Community** +- [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..e9d1c87 --- /dev/null +++ b/.github/wiki/benchmarks/index.md @@ -0,0 +1,47 @@ +# Performance Benchmarks + +## Conversion performance + +Measured on Node 22, Apple M2. Input: 1,000 random dates in range 1900-2076 CE. + +| Operation | UAQ calendar | FCNA calendar | +|---|---|---| +| `uaqCalendar.year(date)` | ~0.5 µs/call | ~14 µs/call | +| `uaqCalendar.dateFromFields(fields)` | ~0.7 µs/call | ~15 µs/call | +| `uaqCalendar.dateUntil(d1, d2)` | ~1.1 µs/call | ~16 µs/call | +| `uaqCalendar.dateAdd(date, duration)` | ~1.3 µs/call | ~17 µs/call | + +UAQ uses a precomputed lookup table (O(1) lookup). FCNA uses an arithmetic algorithm per call, which accounts for the ~26x difference. + +The Temporal polyfill itself adds overhead on top of these numbers. With native Temporal support (future Node.js versions and browsers), the overhead will be lower. + +## Bundle size + +| Module | Min+gz | +|---|---| +| temporal-hijri (wrapper only) | ~1.4 KB | +| hijri-core/uaq (peer dep, UAQ engine) | ~5.3 KB | +| hijri-core/fcna (peer dep, FCNA engine) | ~3.1 KB | +| @js-temporal/polyfill (peer dep, optional) | ~39 KB | + +When native `Temporal` is available in the runtime, the polyfill is not needed, which removes its bundle cost entirely. + +## Reproducing the benchmarks + +```javascript +import { Temporal } from '@js-temporal/polyfill'; +import { uaqCalendar } from 'temporal-hijri'; + +const dates = Array.from({ length: 1000 }, (_, i) => + Temporal.PlainDate.from('1900-01-01').add({ days: i * 26 }) +); + +const start = performance.now(); +for (const d of dates) { + uaqCalendar.year(d); +} +const elapsed = performance.now() - start; +console.log(`${(elapsed / dates.length * 1000).toFixed(1)} µs/call`); +``` + +Run with `node --version` >= 20. diff --git a/.github/wiki/examples/basic-usage.md b/.github/wiki/examples/basic-usage.md new file mode 100644 index 0000000..7789c88 --- /dev/null +++ b/.github/wiki/examples/basic-usage.md @@ -0,0 +1,76 @@ +# Basic Usage Examples + +## Setup + +```typescript +import { Temporal } from '@js-temporal/polyfill'; +import { uaqCalendar } from 'temporal-hijri'; +``` + +## Convert a Gregorian date to Hijri + +```typescript +// 23 March 2023 = 1 Ramadan 1444 AH +const isoDate = Temporal.PlainDate.from('2023-03-23'); + +console.log(uaqCalendar.year(isoDate)); // 1444 +console.log(uaqCalendar.month(isoDate)); // 9 (Ramadan is the 9th month) +console.log(uaqCalendar.day(isoDate)); // 1 +``` + +## Read today's Hijri date + +```typescript +import { Temporal } from '@js-temporal/polyfill'; +import { uaqCalendar } from 'temporal-hijri'; + +const today = Temporal.Now.plainDateISO(); +const hy = uaqCalendar.year(today); +const hm = uaqCalendar.month(today); +const hd = uaqCalendar.day(today); + +console.log(`${hd} / ${hm} / ${hy}`); +``` + +## Create a Hijri date and convert to ISO + +```typescript +const ramadan1 = uaqCalendar.dateFromFields({ year: 1444, month: 9, day: 1 }); +console.log(ramadan1.toString()); // '2023-03-23' +``` + +## Add Hijri months + +```typescript +const start = uaqCalendar.dateFromFields({ year: 1444, month: 9, day: 1 }); +const twoMonthsLater = uaqCalendar.dateAdd(start, new Temporal.Duration(0, 0, 0, 2 * 29)); +// Durations use days — calculate from expected month length + +// Or use dateUntil to measure between two dates +const end = Temporal.PlainDate.from('2023-05-20'); +const diff = uaqCalendar.dateUntil(start, end, { largestUnit: 'months' }); +console.log(diff.months, diff.days); +``` + +## Use FCNA calendar + +```typescript +import { fcnaCalendar } from 'temporal-hijri'; + +const isoDate = Temporal.PlainDate.from('2023-03-23'); + +console.log(fcnaCalendar.year(isoDate)); // 1444 +console.log(fcnaCalendar.month(isoDate)); // 9 +console.log(fcnaCalendar.day(isoDate)); // 1 +// Near month boundaries, UAQ and FCNA may differ by one day +``` + +## CJS usage + +```javascript +const { Temporal } = require('@js-temporal/polyfill'); +const { uaqCalendar } = require('temporal-hijri'); + +const d = Temporal.PlainDate.from('2023-03-23'); +console.log(uaqCalendar.year(d)); // 1444 +``` diff --git a/.github/wiki/guides/advanced.md b/.github/wiki/guides/advanced.md new file mode 100644 index 0000000..ed2df9c --- /dev/null +++ b/.github/wiki/guides/advanced.md @@ -0,0 +1,91 @@ +# Advanced Usage + +## Custom calendar engines + +Any engine registered in hijri-core can be wrapped in a Temporal calendar: + +```typescript +import { HijriCalendar } from 'temporal-hijri'; +import { registerCalendar, getCalendar } from 'hijri-core'; +import type { CalendarEngine } from 'hijri-core'; + +const myEngine: CalendarEngine = { + id: 'local-sighting', + toHijri(date) { /* ... */ return { hy, hm, hd }; }, + toGregorian(hy, hm, hd) { /* ... */ return new Date(...); }, + isValid(hy, hm, hd) { /* ... */ return true; }, + daysInMonth(hy, hm) { /* ... */ return 29; }, +}; + +registerCalendar('local-sighting', myEngine); +const cal = new HijriCalendar(getCalendar('local-sighting')); +// cal.id === 'hijri-local-sighting' +``` + +## DateUntil with different largestUnit values + +`dateUntil` respects the `largestUnit` option: + +```typescript +import { Temporal } from '@js-temporal/polyfill'; +import { uaqCalendar } from 'temporal-hijri'; + +const start = Temporal.PlainDate.from('2023-01-01'); +const end = Temporal.PlainDate.from('2023-12-31'); + +const inYears = uaqCalendar.dateUntil(start, end, { largestUnit: 'years' }); +const inMonths = uaqCalendar.dateUntil(start, end, { largestUnit: 'months' }); +const inDays = uaqCalendar.dateUntil(start, end, { largestUnit: 'days' }); + +console.log(inYears.years, inYears.months, inYears.days); +console.log(inMonths.months, inMonths.days); +console.log(inDays.days); +``` + +Note: the result measures in Hijri units. One Hijri year is 354 or 355 days, so `inYears.days` may differ from what you would expect in Gregorian. + +## PlainYearMonth and PlainMonthDay + +```typescript +import { Temporal } from '@js-temporal/polyfill'; +import { uaqCalendar } from 'temporal-hijri'; + +// Year-month in Hijri +const ym = uaqCalendar.yearMonthFromFields({ year: 1444, month: 9 }); +console.log(ym.toString()); // ISO year-month of 1 Ramadan 1444 + +// Month-day in Hijri +const md = uaqCalendar.monthDayFromFields({ month: 9, day: 1 }); +console.log(md.toString()); // ISO month-day of Ramadan 1st +``` + +## Out-of-range behavior + +UAQ covers 1318-1500 AH (1900-2076 CE). Requesting dates outside that range throws `RangeError`: + +```typescript +const earlyDate = Temporal.PlainDate.from('1800-01-01'); +try { + uaqCalendar.year(earlyDate); // throws RangeError +} catch (e) { + if (e instanceof RangeError) { + // Use FCNA for unbounded coverage + import { fcnaCalendar } from 'temporal-hijri'; + console.log(fcnaCalendar.year(earlyDate)); + } +} +``` + +## Using with native Temporal + +When native `Temporal` is available (future Node.js or browsers), you can use it directly without the polyfill: + +```typescript +// No import from @js-temporal/polyfill +import { uaqCalendar } from 'temporal-hijri'; + +const isoDate = Temporal.PlainDate.from('2023-03-23'); +console.log(uaqCalendar.year(isoDate)); // 1444 +``` + +The `uaqCalendar` and `fcnaCalendar` objects implement the `Temporal.CalendarProtocol` interface and work with any spec-conforming implementation. diff --git a/.github/wiki/guides/quickstart.md b/.github/wiki/guides/quickstart.md new file mode 100644 index 0000000..7c17895 --- /dev/null +++ b/.github/wiki/guides/quickstart.md @@ -0,0 +1,98 @@ +# Quick Start + +This guide covers the most common use cases in temporal-hijri. All examples use `uaqCalendar` (Umm al-Qura). For FCNA/ISNA output, substitute `fcnaCalendar`. + +## Installation + +```bash +pnpm add temporal-hijri hijri-core @js-temporal/polyfill +``` + +`hijri-core` is required. `@js-temporal/polyfill` is required in environments without native `Temporal` support. In environments with native Temporal (Node 22+ with the flag, or future standard support), omit the polyfill. + +## Import + +```typescript +import { Temporal } from '@js-temporal/polyfill'; // or use native Temporal +import { uaqCalendar } from 'temporal-hijri'; +``` + +## Convert an ISO date to Hijri + +```typescript +const isoDate = Temporal.PlainDate.from('2023-03-23'); + +console.log(uaqCalendar.year(isoDate)); // 1444 +console.log(uaqCalendar.month(isoDate)); // 9 +console.log(uaqCalendar.day(isoDate)); // 1 +console.log(uaqCalendar.monthCode(isoDate)); // 'M09' +``` + +## Convert Hijri coordinates to ISO + +```typescript +const ramadan = uaqCalendar.dateFromFields({ year: 1444, month: 9, day: 1 }); +console.log(ramadan.toString()); // '2023-03-23' +``` + +## Date arithmetic in Hijri space + +```typescript +const { Duration } = Temporal; + +const isoDate = Temporal.PlainDate.from('2023-03-23'); + +// Add one Hijri month +const nextMonth = uaqCalendar.dateAdd(isoDate, new Duration(0, 1)); +console.log(uaqCalendar.month(nextMonth)); // 10 (Shawwal) +console.log(nextMonth.toString()); // '2023-04-21' + +// Get the difference between two dates +const earlier = Temporal.PlainDate.from('2023-01-01'); +const later = Temporal.PlainDate.from('2023-03-23'); +const diff = uaqCalendar.dateUntil(earlier, later, { largestUnit: 'months' }); +console.log(diff.months); // 2 (in Hijri months) +``` + +## Use the FCNA calendar + +```typescript +import { fcnaCalendar } from 'temporal-hijri'; + +const isoDate = Temporal.PlainDate.from('2023-03-23'); +console.log(fcnaCalendar.year(isoDate)); // 1444 +console.log(fcnaCalendar.month(isoDate)); // 9 or may differ by 1 near month start +``` + +## Singletons vs classes + +The package exports convenience singletons for the common case: + +```typescript +import { uaqCalendar, fcnaCalendar } from 'temporal-hijri'; +``` + +If you need to construct a calendar from a custom hijri-core engine: + +```typescript +import { HijriCalendar } from 'temporal-hijri'; +import { registerCalendar, getCalendar } from 'hijri-core'; + +registerCalendar('my-engine', myEngine); +const cal = new HijriCalendar(getCalendar('my-engine')); +``` + +## CommonJS + +```js +const { Temporal } = require('@js-temporal/polyfill'); +const { uaqCalendar } = require('temporal-hijri'); + +const isoDate = Temporal.PlainDate.from('2023-03-23'); +console.log(uaqCalendar.year(isoDate)); // 1444 +``` + +## Next steps + +- [API Reference](API-Reference) for all calendar protocol methods +- [Architecture](Architecture) for how the Temporal Calendar Protocol is implemented diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d5a3527..433b934 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,11 +15,12 @@ jobs: node: [20, 22, 24] steps: - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node }} cache: pnpm + - name: Enable corepack + run: corepack enable - run: pnpm install --frozen-lockfile - run: pnpm run build - run: node --test test.mjs @@ -30,11 +31,12 @@ jobs: 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 + - name: Enable corepack + run: corepack enable - run: pnpm install --frozen-lockfile - run: pnpm run lint - run: pnpm run format:check @@ -44,11 +46,12 @@ jobs: 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 + - name: Enable corepack + run: corepack enable - run: pnpm install --frozen-lockfile - run: pnpm run typecheck @@ -57,11 +60,12 @@ jobs: 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 + - name: Enable corepack + run: corepack enable - run: pnpm install --frozen-lockfile - run: pnpm run build - name: Verify pack contents diff --git a/README.md b/README.md index c30058c..640d513 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ # temporal-hijri -Temporal Calendar Protocol implementation for the Hijri calendar. Works with the TC39 Temporal proposal and `@js-temporal/polyfill`. +Temporal Calendar Protocol implementation for the Hijri calendar. Works with the TC39 Temporal proposal (Stage 3) and `@js-temporal/polyfill`. Provides `UaqCalendar` (Umm al-Qura) and `FcnaCalendar` (FCNA/ISNA) as plug-in calendars for `Temporal.PlainDate` and related types. The underlying conversion logic comes from [hijri-core](https://github.com/acamarata/hijri-core), a zero-dependency Hijri engine with table-driven UAQ data and astronomical FCNA calculations.