Compare commits

...

18 commits
v1.0.1 ... main

Author SHA1 Message Date
Aric Camarata
e7fe51d7b3
add opt-in anonymous telemetry (#1)
Some checks failed
CI / Test (Node 20) (push) Failing after 42s
CI / Test (Node 22) (push) Failing after 31s
CI / Test (Node 24) (push) Failing after 38s
CI / Lint (push) Failing after 38s
CI / Typecheck (push) Failing after 26s
CI / Pack check (push) Failing after 28s
CI / Coverage (push) Failing after 2s
* add opt-in telemetry via @acamarata/telemetry (off by default)

* chore: update lockfile for @acamarata/telemetry devDep

* chore: fix prettier formatting on telemetry import
2026-06-30 15:56:49 -04:00
Aric Camarata
515c554763 chore: stop tracking generated coverage artifacts 2026-06-13 10:27:12 -04:00
Aric Camarata
1f201ac895 build: use prepack hook so npm pack/publish reliably emit index.d.mts 2026-06-13 10:11:20 -04:00
Aric Camarata
5bd50384f2 chore: bump to v1.0.3 2026-06-10 16:50:58 -04:00
Aric Camarata
8e5b35c3a3 chore: update hijri-core to 1.0.3 2026-06-10 16:50:22 -04:00
Aric Camarata
4dd246f27a fix: build Dates via Date.UTC for hijri-core's UTC-day contract
HijriCalendar.toHijri() previously used new Date(y, m, d) (local-time
constructor). Under hijri-core's new UTC-day contract the engine reads
the UTC calendar day, so on east-of-UTC hosts (e.g. UTC+5) the local
midnight falls on the previous UTC date, producing a one-day-off Hijri
result.

Fix: use Date.UTC(y, m, d) so PlainDate calendar fields land in the
Date's UTC components, matching what hijri-core reads.

Lock-step dependency: this fix requires the unreleased hijri-core change
on fix/utc-day-boundary (commit 3419378). Both packages will be released
together per ADR-013.

Tests: round-trip regression added (2025-03-01 = 1 Ramadan 1446 AH);
all 37 ESM + 9 CJS tests pass at TZ=UTC, TZ=America/New_York,
TZ=Pacific/Auckland.
2026-06-10 16:35:38 -04:00
Aric Camarata
70bd956179 ci: format all files with prettier to fix format:check 2026-05-31 08:50:45 -04:00
Aric Camarata
40478b6f7b ci: fix eslint - add parserOptions.project for typed linting rules 2026-05-31 08:49:46 -04:00
Aric Camarata
cae2726766 ci: fix eslint flat config - add files pattern to match TS files in src/ 2026-05-31 08:48:45 -04:00
Aric Camarata
e6fd9d14df ci: fix eslint - add @typescript-eslint/parser and eslint-plugin as explicit devDeps 2026-05-31 08:47:02 -04:00
Aric Camarata
977d4323eb chore: bump to v1.0.2 2026-05-30 19:21:29 -04:00
Aric Camarata
c4ee5efe96 chore: P1 standardization — finalize src/wiki/ci 2026-05-30 18:48:59 -04:00
Aric Camarata
bc8f70ba0b docs: refresh TypeDoc API output (T-E8-03 QA-A verify) 2026-05-30 17:48:47 -04:00
Aric Camarata
077861c7dc docs: add TypeDoc API generation (typedoc@0.28.19 + typedoc-plugin-markdown@4.11.0)
Add typedoc and typedoc-plugin-markdown as devDependencies. Add typedoc.json config
targeting src/index.ts with markdown output to .github/wiki/api. Add docs script to
package.json. Generate initial API reference pages.

Part of T-E8-03 — TypeDoc automation for all 12 JS/TS packages.
2026-05-30 16:41:59 -04:00
Aric Camarata
cdcede1c58 chore: adopt shared config packages (tsconfig, eslint, prettier) 2026-05-30 15:12:16 -04:00
Aric Camarata
1dea36eda8 ci: corepack before setup-node, scope prettier to src/, emit d.mts 2026-05-29 20:05:46 -04:00
Aric Camarata
250dda20d4 chore: E6 polish wiki content + ADR-015 CI updates (P1) 2026-05-29 07:15:50 -04:00
Aric Camarata
2289dd3718 chore: untrack AGENTS.md (AI working memory, not source code) 2026-05-29 06:36:40 -04:00
39 changed files with 3506 additions and 404 deletions

View file

@ -1,45 +0,0 @@
# temporal-hijri — PRI (Per-Repo Instructions)
**PPI:** `~/Sites/acamarata/.claude/CLAUDE.md`
## What This Is
Temporal Calendar Protocol implementation for the Hijri calendar. Works with the TC39
Temporal proposal 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.
**npm:** `temporal-hijri@1.0.0`
**Language:** TypeScript
**License:** MIT
## Key Technical Details
- Peer dependencies: `hijri-core@^1.0.0`, `@js-temporal/polyfill@^0.4.0` (optional)
- `@js-temporal/polyfill` is optional — works with native Temporal when available in the runtime
- Key exports: `uaqCalendar` (singleton), `fcnaCalendar` (singleton), `UaqCalendar` class, `FcnaCalendar` class
- Calendar protocol methods: `year()`, `month()`, `day()`, `monthCode()`, `inLeapYear()`, `dateFromFields()`, `dateAdd()`
- UAQ data covers 1318-1500 AH (Gregorian 1900-2076)
- Dual CJS/ESM build via tsup
- Zero runtime dependencies (peer deps are provided by the consumer)
## Architecture
`src/index.ts` exports calendar singletons and classes. Built to `dist/` (gitignored)
with `.cjs` and `.mjs` outputs plus dual type declarations. No format string parsing —
the Temporal API handles all date arithmetic and formatting natively.
## Commands
- `pnpm install` — install dev deps
- `pnpm build` — tsup build
- `pnpm test` — run test.mjs + test-cjs.cjs
- `pnpm run typecheck` — tsc --noEmit
## Important Notes
- This implements the Temporal Calendar Protocol — it is not a general-purpose Hijri utility
- hijri-core provides the actual calendar engine — this package is a thin Temporal protocol adapter
- Changes to hijri-core's API may require updates here
- `@js-temporal/polyfill` is optional — only needed in environments without native Temporal support
- TC39 Temporal proposal is Stage 3 — API may shift; track spec changes carefully

29
.github/wiki/CODE_OF_CONDUCT.md vendored Normal file
View file

@ -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.

54
.github/wiki/CONTRIBUTING.md vendored Normal file
View file

@ -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.

30
.github/wiki/SECURITY.md vendored Normal file
View file

@ -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

1
.github/wiki/_Footer.md vendored Normal file
View file

@ -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)

25
.github/wiki/_Sidebar.md vendored Normal file
View file

@ -0,0 +1,25 @@
**[Home](Home)**
**Guides**
- [Quick Start](guides/quickstart)
- [Advanced Usage](guides/advanced)
**Examples**
- [Basic Usage](examples/basic-usage)
- [Scheduling Display](examples/scheduling-display)
**API**
- [HijriCalendar](api/HijriCalendar)
- [UaqCalendar](api/UaqCalendar)
- [FcnaCalendar](api/FcnaCalendar)
- [Singletons](api/singletons)
- [Full API Reference](API-Reference)
**Reference**
- [Architecture](Architecture)
- [Benchmarks](benchmarks/index)
**Community**
- [Contributing](CONTRIBUTING)
- [Code of Conduct](CODE_OF_CONDUCT)
- [Security](SECURITY)

22
.github/wiki/api/README.md vendored Normal file
View file

@ -0,0 +1,22 @@
**temporal-hijri v1.0.1**
***
# temporal-hijri v1.0.1
## Classes
- [FcnaCalendar](classes/FcnaCalendar.md)
- [HijriCalendar](classes/HijriCalendar.md)
- [UaqCalendar](classes/UaqCalendar.md)
## Interfaces
- [CalendarEngine](interfaces/CalendarEngine.md)
- [ConversionOptions](interfaces/ConversionOptions.md)
- [HijriDate](interfaces/HijriDate.md)
## Variables
- [fcnaCalendar](variables/fcnaCalendar.md)
- [uaqCalendar](variables/uaqCalendar.md)

649
.github/wiki/api/classes/FcnaCalendar.md vendored Normal file
View file

@ -0,0 +1,649 @@
[**temporal-hijri v1.0.1**](../README.md)
***
[temporal-hijri](../README.md) / FcnaCalendar
# Class: FcnaCalendar
Defined in: [src/calendars/FcnaCalendar.ts:18](https://github.com/acamarata/temporal-hijri/blob/077861c7dc33e5562fb75c1defbee0f2db4e2f2a/src/calendars/FcnaCalendar.ts#L18)
Temporal calendar implementation for the FCNA/ISNA calendar.
The Fiqh Council of North America (FCNA) calendar, also used by the Islamic
Society of North America (ISNA), determines month starts through astronomical
calculation: a new month begins the day after the conjunction (new moon) if
that conjunction occurs before 12:00 noon UTC, or two days after if at or
after noon. This criterion enables global date-setting without local moon
sighting, making it popular for diaspora Muslim communities in North America
and Europe.
Calendar engine: hijri-core FCNA (Meeus Chapter 49 calculations).
Calendar ID: "hijri-fcna"
## Extends
- [`HijriCalendar`](HijriCalendar.md)
## Constructors
### Constructor
> **new FcnaCalendar**(): `FcnaCalendar`
Defined in: [src/calendars/FcnaCalendar.ts:19](https://github.com/acamarata/temporal-hijri/blob/077861c7dc33e5562fb75c1defbee0f2db4e2f2a/src/calendars/FcnaCalendar.ts#L19)
#### Returns
`FcnaCalendar`
#### Overrides
[`HijriCalendar`](HijriCalendar.md).[`constructor`](HijriCalendar.md#constructor)
## Properties
### id
> `readonly` **id**: `string`
Defined in: [src/calendars/HijriCalendar.ts:59](https://github.com/acamarata/temporal-hijri/blob/077861c7dc33e5562fb75c1defbee0f2db4e2f2a/src/calendars/HijriCalendar.ts#L59)
#### Inherited from
[`HijriCalendar`](HijriCalendar.md).[`id`](HijriCalendar.md#id)
## Methods
### dateAdd()
> **dateAdd**(`date`, `duration`, `_options?`): `PlainDate`
Defined in: [src/calendars/HijriCalendar.ts:346](https://github.com/acamarata/temporal-hijri/blob/077861c7dc33e5562fb75c1defbee0f2db4e2f2a/src/calendars/HijriCalendar.ts#L346)
Add a duration to a Hijri date.
Year and month additions are handled in Hijri space to preserve calendar
semantics (e.g., adding one month to 1 Ramadan yields 1 Shawwal, not a
fixed 30-day offset). Day and week additions are then applied in ISO space
so that they always represent exact day counts.
Month normalization uses O(1) modular arithmetic instead of iterative loops.
When the day-of-month exceeds the target month's length after a Hijri-space
adjustment, it is clamped to the last valid day of that month.
#### Parameters
##### date
`PlainDate`
##### duration
`Duration`
##### \_options?
###### overflow?
`"constrain"` \| `"reject"`
#### Returns
`PlainDate`
#### Inherited from
[`HijriCalendar`](HijriCalendar.md).[`dateAdd`](HijriCalendar.md#dateadd)
***
### dateFromFields()
> **dateFromFields**(`fields`, `options?`): `PlainDate`
Defined in: [src/calendars/HijriCalendar.ts:279](https://github.com/acamarata/temporal-hijri/blob/077861c7dc33e5562fb75c1defbee0f2db4e2f2a/src/calendars/HijriCalendar.ts#L279)
#### Parameters
##### fields
###### day
`number`
###### month
`number`
###### year
`number`
##### options?
###### overflow?
`"constrain"` \| `"reject"`
#### Returns
`PlainDate`
#### Inherited from
[`HijriCalendar`](HijriCalendar.md).[`dateFromFields`](HijriCalendar.md#datefromfields)
***
### dateUntil()
> **dateUntil**(`one`, `two`, `options?`): `Duration`
Defined in: [src/calendars/HijriCalendar.ts:384](https://github.com/acamarata/temporal-hijri/blob/077861c7dc33e5562fb75c1defbee0f2db4e2f2a/src/calendars/HijriCalendar.ts#L384)
Compute the difference between two Hijri dates.
For simplicity and correctness across variable-length Hijri months, this
delegates to the underlying ISO PlainDate difference when the largest unit
is days or weeks. Year/month differences require a Hijri-space calculation.
#### Parameters
##### one
`PlainDate`
##### two
`PlainDate`
##### options?
###### largestUnit?
`DateUnit`
#### Returns
`Duration`
#### Inherited from
[`HijriCalendar`](HijriCalendar.md).[`dateUntil`](HijriCalendar.md#dateuntil)
***
### day()
> **day**(`date`): `number`
Defined in: [src/calendars/HijriCalendar.ts:169](https://github.com/acamarata/temporal-hijri/blob/077861c7dc33e5562fb75c1defbee0f2db4e2f2a/src/calendars/HijriCalendar.ts#L169)
Returns the day of the Hijri month (1-29 or 1-30).
#### Parameters
##### date
`PlainDate`
A Temporal.PlainDate with ISO (Gregorian) coordinates.
#### Returns
`number`
Day of month within the Hijri calendar.
#### Inherited from
[`HijriCalendar`](HijriCalendar.md).[`day`](HijriCalendar.md#day)
***
### dayOfWeek()
> **dayOfWeek**(`date`): `number`
Defined in: [src/calendars/HijriCalendar.ts:234](https://github.com/acamarata/temporal-hijri/blob/077861c7dc33e5562fb75c1defbee0f2db4e2f2a/src/calendars/HijriCalendar.ts#L234)
ISO weekday: 1 = Monday, 7 = Sunday.
PlainDate.dayOfWeek on an ISO-calendar date already gives ISO weekday,
so no conversion is needed.
#### Parameters
##### date
`PlainDate`
#### Returns
`number`
#### Inherited from
[`HijriCalendar`](HijriCalendar.md).[`dayOfWeek`](HijriCalendar.md#dayofweek)
***
### dayOfYear()
> **dayOfYear**(`date`): `number`
Defined in: [src/calendars/HijriCalendar.ts:242](https://github.com/acamarata/temporal-hijri/blob/077861c7dc33e5562fb75c1defbee0f2db4e2f2a/src/calendars/HijriCalendar.ts#L242)
Day within the Hijri year. Accumulates full months before the current one,
then adds the day-of-month offset.
#### Parameters
##### date
`PlainDate`
#### Returns
`number`
#### Inherited from
[`HijriCalendar`](HijriCalendar.md).[`dayOfYear`](HijriCalendar.md#dayofyear)
***
### daysInMonth()
> **daysInMonth**(`date`): `number`
Defined in: [src/calendars/HijriCalendar.ts:184](https://github.com/acamarata/temporal-hijri/blob/077861c7dc33e5562fb75c1defbee0f2db4e2f2a/src/calendars/HijriCalendar.ts#L184)
Returns the number of days in the Hijri month containing the given date.
Hijri months alternate between 29 and 30 days, but the exact pattern
differs by calendar system (UAQ uses fixed tables; FCNA uses calculation).
#### Parameters
##### date
`PlainDate`
A Temporal.PlainDate with ISO (Gregorian) coordinates.
#### Returns
`number`
29 or 30.
#### Inherited from
[`HijriCalendar`](HijriCalendar.md).[`daysInMonth`](HijriCalendar.md#daysinmonth)
***
### daysInWeek()
> **daysInWeek**(`_date`): `number`
Defined in: [src/calendars/HijriCalendar.ts:263](https://github.com/acamarata/temporal-hijri/blob/077861c7dc33e5562fb75c1defbee0f2db4e2f2a/src/calendars/HijriCalendar.ts#L263)
Returns the number of days in a week.
Always 7. Required by the Temporal Calendar Protocol.
#### Parameters
##### \_date
`PlainDate`
#### Returns
`number`
Always 7.
#### Inherited from
[`HijriCalendar`](HijriCalendar.md).[`daysInWeek`](HijriCalendar.md#daysinweek)
***
### daysInYear()
> **daysInYear**(`date`): `number`
Defined in: [src/calendars/HijriCalendar.ts:193](https://github.com/acamarata/temporal-hijri/blob/077861c7dc33e5562fb75c1defbee0f2db4e2f2a/src/calendars/HijriCalendar.ts#L193)
Sum all 12 month lengths for the Hijri year. Standard lunar years are 354
days; leap years (with an added day in Dhul-Hijja) are 355 days.
#### Parameters
##### date
`PlainDate`
#### Returns
`number`
#### Inherited from
[`HijriCalendar`](HijriCalendar.md).[`daysInYear`](HijriCalendar.md#daysinyear)
***
### fields()
> **fields**(`fields`): `string`[]
Defined in: [src/calendars/HijriCalendar.ts:273](https://github.com/acamarata/temporal-hijri/blob/077861c7dc33e5562fb75c1defbee0f2db4e2f2a/src/calendars/HijriCalendar.ts#L273)
Return the list of fields that the calendar adds to a Temporal object.
Non-era calendars return the input array unchanged.
#### Parameters
##### fields
`string`[]
#### Returns
`string`[]
#### Inherited from
[`HijriCalendar`](HijriCalendar.md).[`fields`](HijriCalendar.md#fields)
***
### inLeapYear()
> **inLeapYear**(`date`): `boolean`
Defined in: [src/calendars/HijriCalendar.ts:223](https://github.com/acamarata/temporal-hijri/blob/077861c7dc33e5562fb75c1defbee0f2db4e2f2a/src/calendars/HijriCalendar.ts#L223)
Returns whether the Hijri year is a leap year (355 days).
Standard Hijri years have 354 days. A leap year adds one day to
Dhul-Hijja (month 12), making it 355 days total.
#### Parameters
##### date
`PlainDate`
A Temporal.PlainDate with ISO (Gregorian) coordinates.
#### Returns
`boolean`
`true` if the year has 355 days.
#### Inherited from
[`HijriCalendar`](HijriCalendar.md).[`inLeapYear`](HijriCalendar.md#inleapyear)
***
### mergeFields()
> **mergeFields**(`fields`, `additionalFields`): `Record`\<`string`, `unknown`\>
Defined in: [src/calendars/HijriCalendar.ts:414](https://github.com/acamarata/temporal-hijri/blob/077861c7dc33e5562fb75c1defbee0f2db4e2f2a/src/calendars/HijriCalendar.ts#L414)
#### Parameters
##### fields
`Record`\<`string`, `unknown`\>
##### additionalFields
`Record`\<`string`, `unknown`\>
#### Returns
`Record`\<`string`, `unknown`\>
#### Inherited from
[`HijriCalendar`](HijriCalendar.md).[`mergeFields`](HijriCalendar.md#mergefields)
***
### month()
> **month**(`date`): `number`
Defined in: [src/calendars/HijriCalendar.ts:149](https://github.com/acamarata/temporal-hijri/blob/077861c7dc33e5562fb75c1defbee0f2db4e2f2a/src/calendars/HijriCalendar.ts#L149)
Returns the Hijri month (1-12) for the given ISO date.
#### Parameters
##### date
`PlainDate`
A Temporal.PlainDate with ISO (Gregorian) coordinates.
#### Returns
`number`
Month number 1 (Muharram) through 12 (Dhul-Hijja).
#### Inherited from
[`HijriCalendar`](HijriCalendar.md).[`month`](HijriCalendar.md#month)
***
### monthCode()
> **monthCode**(`date`): `string`
Defined in: [src/calendars/HijriCalendar.ts:158](https://github.com/acamarata/temporal-hijri/blob/077861c7dc33e5562fb75c1defbee0f2db4e2f2a/src/calendars/HijriCalendar.ts#L158)
Month code per the Temporal proposal: "M01".."M12".
Hijri months are always 1-12 (no leap/intercalary month), so the code is
simply the zero-padded month number.
#### Parameters
##### date
`PlainDate`
#### Returns
`string`
#### Inherited from
[`HijriCalendar`](HijriCalendar.md).[`monthCode`](HijriCalendar.md#monthcode)
***
### monthDayFromFields()
> **monthDayFromFields**(`fields`, `options?`): `PlainMonthDay`
Defined in: [src/calendars/HijriCalendar.ts:317](https://github.com/acamarata/temporal-hijri/blob/077861c7dc33e5562fb75c1defbee0f2db4e2f2a/src/calendars/HijriCalendar.ts#L317)
ISO-anchored PlainMonthDay per the Temporal Calendar Protocol.
Reference year 1444 is intentional: it is a recent, well-covered UAQ year
used to anchor the ISO coordinates when no year is supplied.
#### Parameters
##### fields
###### day
`number`
###### month
`number`
###### year?
`number`
##### options?
###### overflow?
`"constrain"` \| `"reject"`
#### Returns
`PlainMonthDay`
#### Inherited from
[`HijriCalendar`](HijriCalendar.md).[`monthDayFromFields`](HijriCalendar.md#monthdayfromfields)
***
### monthsInYear()
> **monthsInYear**(`_date`): `number`
Defined in: [src/calendars/HijriCalendar.ts:210](https://github.com/acamarata/temporal-hijri/blob/077861c7dc33e5562fb75c1defbee0f2db4e2f2a/src/calendars/HijriCalendar.ts#L210)
Returns the number of months in the Hijri year.
Always 12. Unlike the Hebrew calendar, the Hijri lunar calendar has no
intercalary (leap) month — only a possible extra day in Dhul-Hijja.
#### Parameters
##### \_date
`PlainDate`
#### Returns
`number`
Always 12.
#### Inherited from
[`HijriCalendar`](HijriCalendar.md).[`monthsInYear`](HijriCalendar.md#monthsinyear)
***
### toString()
> **toString**(): `string`
Defined in: [src/calendars/HijriCalendar.ts:66](https://github.com/acamarata/temporal-hijri/blob/077861c7dc33e5562fb75c1defbee0f2db4e2f2a/src/calendars/HijriCalendar.ts#L66)
#### Returns
`string`
#### Inherited from
[`HijriCalendar`](HijriCalendar.md).[`toString`](HijriCalendar.md#tostring)
***
### weekOfYear()
> **weekOfYear**(`date`): `number`
Defined in: [src/calendars/HijriCalendar.ts:252](https://github.com/acamarata/temporal-hijri/blob/077861c7dc33e5562fb75c1defbee0f2db4e2f2a/src/calendars/HijriCalendar.ts#L252)
Hijri week number counted from day 1 of Muharram (day 1-7 = week 1). No ISO week alignment.
#### Parameters
##### date
`PlainDate`
#### Returns
`number`
#### Inherited from
[`HijriCalendar`](HijriCalendar.md).[`weekOfYear`](HijriCalendar.md#weekofyear)
***
### year()
> **year**(`date`): `number`
Defined in: [src/calendars/HijriCalendar.ts:139](https://github.com/acamarata/temporal-hijri/blob/077861c7dc33e5562fb75c1defbee0f2db4e2f2a/src/calendars/HijriCalendar.ts#L139)
Returns the Hijri year for the given ISO date.
#### Parameters
##### date
`PlainDate`
A Temporal.PlainDate with ISO (Gregorian) coordinates.
#### Returns
`number`
The Hijri year, e.g. 1444.
#### Inherited from
[`HijriCalendar`](HijriCalendar.md).[`year`](HijriCalendar.md#year)
***
### yearMonthFromFields()
> **yearMonthFromFields**(`fields`, `options?`): `PlainYearMonth`
Defined in: [src/calendars/HijriCalendar.ts:294](https://github.com/acamarata/temporal-hijri/blob/077861c7dc33e5562fb75c1defbee0f2db4e2f2a/src/calendars/HijriCalendar.ts#L294)
ISO-anchored PlainYearMonth per the Temporal Calendar Protocol.
The resulting PlainYearMonth stores ISO coordinates internally, representing
the Hijri month that starts on that ISO year/month.
#### Parameters
##### fields
###### month
`number`
###### year
`number`
##### options?
###### overflow?
`"constrain"` \| `"reject"`
#### Returns
`PlainYearMonth`
#### Inherited from
[`HijriCalendar`](HijriCalendar.md).[`yearMonthFromFields`](HijriCalendar.md#yearmonthfromfields)

View file

@ -0,0 +1,568 @@
[**temporal-hijri v1.0.1**](../README.md)
***
[temporal-hijri](../README.md) / HijriCalendar
# Class: HijriCalendar
Defined in: [src/calendars/HijriCalendar.ts:57](https://github.com/acamarata/temporal-hijri/blob/077861c7dc33e5562fb75c1defbee0f2db4e2f2a/src/calendars/HijriCalendar.ts#L57)
Base class implementing the TC39 Temporal Calendar Protocol for Hijri calendars.
Coordinate bridging: Temporal.PlainDate operates in the ISO (Gregorian) calendar.
Every calendar method receives a PlainDate with ISO year/month/day, and must
return results in the Hijri calendar's coordinate system. The bridge is
toHijri() and fromHijri(), which delegate to the injected CalendarEngine.
Arithmetic strategy for dateAdd():
- Year and month deltas are applied in Hijri space (correct handling of
variable month lengths).
- Day and week deltas are applied in ISO space after the Hijri addition,
so that "add 30 days" always means exactly 30 days.
## Extended by
- [`UaqCalendar`](UaqCalendar.md)
- [`FcnaCalendar`](FcnaCalendar.md)
## Constructors
### Constructor
> **new HijriCalendar**(`engine`): `HijriCalendar`
Defined in: [src/calendars/HijriCalendar.ts:61](https://github.com/acamarata/temporal-hijri/blob/077861c7dc33e5562fb75c1defbee0f2db4e2f2a/src/calendars/HijriCalendar.ts#L61)
#### Parameters
##### engine
[`CalendarEngine`](../interfaces/CalendarEngine.md)
#### Returns
`HijriCalendar`
## Properties
### id
> `readonly` **id**: `string`
Defined in: [src/calendars/HijriCalendar.ts:59](https://github.com/acamarata/temporal-hijri/blob/077861c7dc33e5562fb75c1defbee0f2db4e2f2a/src/calendars/HijriCalendar.ts#L59)
## Methods
### dateAdd()
> **dateAdd**(`date`, `duration`, `_options?`): `PlainDate`
Defined in: [src/calendars/HijriCalendar.ts:346](https://github.com/acamarata/temporal-hijri/blob/077861c7dc33e5562fb75c1defbee0f2db4e2f2a/src/calendars/HijriCalendar.ts#L346)
Add a duration to a Hijri date.
Year and month additions are handled in Hijri space to preserve calendar
semantics (e.g., adding one month to 1 Ramadan yields 1 Shawwal, not a
fixed 30-day offset). Day and week additions are then applied in ISO space
so that they always represent exact day counts.
Month normalization uses O(1) modular arithmetic instead of iterative loops.
When the day-of-month exceeds the target month's length after a Hijri-space
adjustment, it is clamped to the last valid day of that month.
#### Parameters
##### date
`PlainDate`
##### duration
`Duration`
##### \_options?
###### overflow?
`"constrain"` \| `"reject"`
#### Returns
`PlainDate`
***
### dateFromFields()
> **dateFromFields**(`fields`, `options?`): `PlainDate`
Defined in: [src/calendars/HijriCalendar.ts:279](https://github.com/acamarata/temporal-hijri/blob/077861c7dc33e5562fb75c1defbee0f2db4e2f2a/src/calendars/HijriCalendar.ts#L279)
#### Parameters
##### fields
###### day
`number`
###### month
`number`
###### year
`number`
##### options?
###### overflow?
`"constrain"` \| `"reject"`
#### Returns
`PlainDate`
***
### dateUntil()
> **dateUntil**(`one`, `two`, `options?`): `Duration`
Defined in: [src/calendars/HijriCalendar.ts:384](https://github.com/acamarata/temporal-hijri/blob/077861c7dc33e5562fb75c1defbee0f2db4e2f2a/src/calendars/HijriCalendar.ts#L384)
Compute the difference between two Hijri dates.
For simplicity and correctness across variable-length Hijri months, this
delegates to the underlying ISO PlainDate difference when the largest unit
is days or weeks. Year/month differences require a Hijri-space calculation.
#### Parameters
##### one
`PlainDate`
##### two
`PlainDate`
##### options?
###### largestUnit?
`DateUnit`
#### Returns
`Duration`
***
### day()
> **day**(`date`): `number`
Defined in: [src/calendars/HijriCalendar.ts:169](https://github.com/acamarata/temporal-hijri/blob/077861c7dc33e5562fb75c1defbee0f2db4e2f2a/src/calendars/HijriCalendar.ts#L169)
Returns the day of the Hijri month (1-29 or 1-30).
#### Parameters
##### date
`PlainDate`
A Temporal.PlainDate with ISO (Gregorian) coordinates.
#### Returns
`number`
Day of month within the Hijri calendar.
***
### dayOfWeek()
> **dayOfWeek**(`date`): `number`
Defined in: [src/calendars/HijriCalendar.ts:234](https://github.com/acamarata/temporal-hijri/blob/077861c7dc33e5562fb75c1defbee0f2db4e2f2a/src/calendars/HijriCalendar.ts#L234)
ISO weekday: 1 = Monday, 7 = Sunday.
PlainDate.dayOfWeek on an ISO-calendar date already gives ISO weekday,
so no conversion is needed.
#### Parameters
##### date
`PlainDate`
#### Returns
`number`
***
### dayOfYear()
> **dayOfYear**(`date`): `number`
Defined in: [src/calendars/HijriCalendar.ts:242](https://github.com/acamarata/temporal-hijri/blob/077861c7dc33e5562fb75c1defbee0f2db4e2f2a/src/calendars/HijriCalendar.ts#L242)
Day within the Hijri year. Accumulates full months before the current one,
then adds the day-of-month offset.
#### Parameters
##### date
`PlainDate`
#### Returns
`number`
***
### daysInMonth()
> **daysInMonth**(`date`): `number`
Defined in: [src/calendars/HijriCalendar.ts:184](https://github.com/acamarata/temporal-hijri/blob/077861c7dc33e5562fb75c1defbee0f2db4e2f2a/src/calendars/HijriCalendar.ts#L184)
Returns the number of days in the Hijri month containing the given date.
Hijri months alternate between 29 and 30 days, but the exact pattern
differs by calendar system (UAQ uses fixed tables; FCNA uses calculation).
#### Parameters
##### date
`PlainDate`
A Temporal.PlainDate with ISO (Gregorian) coordinates.
#### Returns
`number`
29 or 30.
***
### daysInWeek()
> **daysInWeek**(`_date`): `number`
Defined in: [src/calendars/HijriCalendar.ts:263](https://github.com/acamarata/temporal-hijri/blob/077861c7dc33e5562fb75c1defbee0f2db4e2f2a/src/calendars/HijriCalendar.ts#L263)
Returns the number of days in a week.
Always 7. Required by the Temporal Calendar Protocol.
#### Parameters
##### \_date
`PlainDate`
#### Returns
`number`
Always 7.
***
### daysInYear()
> **daysInYear**(`date`): `number`
Defined in: [src/calendars/HijriCalendar.ts:193](https://github.com/acamarata/temporal-hijri/blob/077861c7dc33e5562fb75c1defbee0f2db4e2f2a/src/calendars/HijriCalendar.ts#L193)
Sum all 12 month lengths for the Hijri year. Standard lunar years are 354
days; leap years (with an added day in Dhul-Hijja) are 355 days.
#### Parameters
##### date
`PlainDate`
#### Returns
`number`
***
### fields()
> **fields**(`fields`): `string`[]
Defined in: [src/calendars/HijriCalendar.ts:273](https://github.com/acamarata/temporal-hijri/blob/077861c7dc33e5562fb75c1defbee0f2db4e2f2a/src/calendars/HijriCalendar.ts#L273)
Return the list of fields that the calendar adds to a Temporal object.
Non-era calendars return the input array unchanged.
#### Parameters
##### fields
`string`[]
#### Returns
`string`[]
***
### inLeapYear()
> **inLeapYear**(`date`): `boolean`
Defined in: [src/calendars/HijriCalendar.ts:223](https://github.com/acamarata/temporal-hijri/blob/077861c7dc33e5562fb75c1defbee0f2db4e2f2a/src/calendars/HijriCalendar.ts#L223)
Returns whether the Hijri year is a leap year (355 days).
Standard Hijri years have 354 days. A leap year adds one day to
Dhul-Hijja (month 12), making it 355 days total.
#### Parameters
##### date
`PlainDate`
A Temporal.PlainDate with ISO (Gregorian) coordinates.
#### Returns
`boolean`
`true` if the year has 355 days.
***
### mergeFields()
> **mergeFields**(`fields`, `additionalFields`): `Record`\<`string`, `unknown`\>
Defined in: [src/calendars/HijriCalendar.ts:414](https://github.com/acamarata/temporal-hijri/blob/077861c7dc33e5562fb75c1defbee0f2db4e2f2a/src/calendars/HijriCalendar.ts#L414)
#### Parameters
##### fields
`Record`\<`string`, `unknown`\>
##### additionalFields
`Record`\<`string`, `unknown`\>
#### Returns
`Record`\<`string`, `unknown`\>
***
### month()
> **month**(`date`): `number`
Defined in: [src/calendars/HijriCalendar.ts:149](https://github.com/acamarata/temporal-hijri/blob/077861c7dc33e5562fb75c1defbee0f2db4e2f2a/src/calendars/HijriCalendar.ts#L149)
Returns the Hijri month (1-12) for the given ISO date.
#### Parameters
##### date
`PlainDate`
A Temporal.PlainDate with ISO (Gregorian) coordinates.
#### Returns
`number`
Month number 1 (Muharram) through 12 (Dhul-Hijja).
***
### monthCode()
> **monthCode**(`date`): `string`
Defined in: [src/calendars/HijriCalendar.ts:158](https://github.com/acamarata/temporal-hijri/blob/077861c7dc33e5562fb75c1defbee0f2db4e2f2a/src/calendars/HijriCalendar.ts#L158)
Month code per the Temporal proposal: "M01".."M12".
Hijri months are always 1-12 (no leap/intercalary month), so the code is
simply the zero-padded month number.
#### Parameters
##### date
`PlainDate`
#### Returns
`string`
***
### monthDayFromFields()
> **monthDayFromFields**(`fields`, `options?`): `PlainMonthDay`
Defined in: [src/calendars/HijriCalendar.ts:317](https://github.com/acamarata/temporal-hijri/blob/077861c7dc33e5562fb75c1defbee0f2db4e2f2a/src/calendars/HijriCalendar.ts#L317)
ISO-anchored PlainMonthDay per the Temporal Calendar Protocol.
Reference year 1444 is intentional: it is a recent, well-covered UAQ year
used to anchor the ISO coordinates when no year is supplied.
#### Parameters
##### fields
###### day
`number`
###### month
`number`
###### year?
`number`
##### options?
###### overflow?
`"constrain"` \| `"reject"`
#### Returns
`PlainMonthDay`
***
### monthsInYear()
> **monthsInYear**(`_date`): `number`
Defined in: [src/calendars/HijriCalendar.ts:210](https://github.com/acamarata/temporal-hijri/blob/077861c7dc33e5562fb75c1defbee0f2db4e2f2a/src/calendars/HijriCalendar.ts#L210)
Returns the number of months in the Hijri year.
Always 12. Unlike the Hebrew calendar, the Hijri lunar calendar has no
intercalary (leap) month — only a possible extra day in Dhul-Hijja.
#### Parameters
##### \_date
`PlainDate`
#### Returns
`number`
Always 12.
***
### toString()
> **toString**(): `string`
Defined in: [src/calendars/HijriCalendar.ts:66](https://github.com/acamarata/temporal-hijri/blob/077861c7dc33e5562fb75c1defbee0f2db4e2f2a/src/calendars/HijriCalendar.ts#L66)
#### Returns
`string`
***
### weekOfYear()
> **weekOfYear**(`date`): `number`
Defined in: [src/calendars/HijriCalendar.ts:252](https://github.com/acamarata/temporal-hijri/blob/077861c7dc33e5562fb75c1defbee0f2db4e2f2a/src/calendars/HijriCalendar.ts#L252)
Hijri week number counted from day 1 of Muharram (day 1-7 = week 1). No ISO week alignment.
#### Parameters
##### date
`PlainDate`
#### Returns
`number`
***
### year()
> **year**(`date`): `number`
Defined in: [src/calendars/HijriCalendar.ts:139](https://github.com/acamarata/temporal-hijri/blob/077861c7dc33e5562fb75c1defbee0f2db4e2f2a/src/calendars/HijriCalendar.ts#L139)
Returns the Hijri year for the given ISO date.
#### Parameters
##### date
`PlainDate`
A Temporal.PlainDate with ISO (Gregorian) coordinates.
#### Returns
`number`
The Hijri year, e.g. 1444.
***
### yearMonthFromFields()
> **yearMonthFromFields**(`fields`, `options?`): `PlainYearMonth`
Defined in: [src/calendars/HijriCalendar.ts:294](https://github.com/acamarata/temporal-hijri/blob/077861c7dc33e5562fb75c1defbee0f2db4e2f2a/src/calendars/HijriCalendar.ts#L294)
ISO-anchored PlainYearMonth per the Temporal Calendar Protocol.
The resulting PlainYearMonth stores ISO coordinates internally, representing
the Hijri month that starts on that ISO year/month.
#### Parameters
##### fields
###### month
`number`
###### year
`number`
##### options?
###### overflow?
`"constrain"` \| `"reject"`
#### Returns
`PlainYearMonth`

647
.github/wiki/api/classes/UaqCalendar.md vendored Normal file
View file

@ -0,0 +1,647 @@
[**temporal-hijri v1.0.1**](../README.md)
***
[temporal-hijri](../README.md) / UaqCalendar
# Class: UaqCalendar
Defined in: [src/calendars/UaqCalendar.ts:16](https://github.com/acamarata/temporal-hijri/blob/077861c7dc33e5562fb75c1defbee0f2db4e2f2a/src/calendars/UaqCalendar.ts#L16)
Temporal calendar implementation for the Umm al-Qura calendar.
Umm al-Qura is the official calendar of Saudi Arabia, maintained by the
King Abdulaziz City for Science and Technology (KACST). It is the most
widely used Hijri calendar standard for civil and religious purposes across
the Muslim world. Month boundaries are determined by pre-calculated tables
rather than real-time moon sighting.
Calendar engine: hijri-core UAQ (table-driven, covers 1318-1500 AH).
Calendar ID: "hijri-uaq"
## Extends
- [`HijriCalendar`](HijriCalendar.md)
## Constructors
### Constructor
> **new UaqCalendar**(): `UaqCalendar`
Defined in: [src/calendars/UaqCalendar.ts:17](https://github.com/acamarata/temporal-hijri/blob/077861c7dc33e5562fb75c1defbee0f2db4e2f2a/src/calendars/UaqCalendar.ts#L17)
#### Returns
`UaqCalendar`
#### Overrides
[`HijriCalendar`](HijriCalendar.md).[`constructor`](HijriCalendar.md#constructor)
## Properties
### id
> `readonly` **id**: `string`
Defined in: [src/calendars/HijriCalendar.ts:59](https://github.com/acamarata/temporal-hijri/blob/077861c7dc33e5562fb75c1defbee0f2db4e2f2a/src/calendars/HijriCalendar.ts#L59)
#### Inherited from
[`HijriCalendar`](HijriCalendar.md).[`id`](HijriCalendar.md#id)
## Methods
### dateAdd()
> **dateAdd**(`date`, `duration`, `_options?`): `PlainDate`
Defined in: [src/calendars/HijriCalendar.ts:346](https://github.com/acamarata/temporal-hijri/blob/077861c7dc33e5562fb75c1defbee0f2db4e2f2a/src/calendars/HijriCalendar.ts#L346)
Add a duration to a Hijri date.
Year and month additions are handled in Hijri space to preserve calendar
semantics (e.g., adding one month to 1 Ramadan yields 1 Shawwal, not a
fixed 30-day offset). Day and week additions are then applied in ISO space
so that they always represent exact day counts.
Month normalization uses O(1) modular arithmetic instead of iterative loops.
When the day-of-month exceeds the target month's length after a Hijri-space
adjustment, it is clamped to the last valid day of that month.
#### Parameters
##### date
`PlainDate`
##### duration
`Duration`
##### \_options?
###### overflow?
`"constrain"` \| `"reject"`
#### Returns
`PlainDate`
#### Inherited from
[`HijriCalendar`](HijriCalendar.md).[`dateAdd`](HijriCalendar.md#dateadd)
***
### dateFromFields()
> **dateFromFields**(`fields`, `options?`): `PlainDate`
Defined in: [src/calendars/HijriCalendar.ts:279](https://github.com/acamarata/temporal-hijri/blob/077861c7dc33e5562fb75c1defbee0f2db4e2f2a/src/calendars/HijriCalendar.ts#L279)
#### Parameters
##### fields
###### day
`number`
###### month
`number`
###### year
`number`
##### options?
###### overflow?
`"constrain"` \| `"reject"`
#### Returns
`PlainDate`
#### Inherited from
[`HijriCalendar`](HijriCalendar.md).[`dateFromFields`](HijriCalendar.md#datefromfields)
***
### dateUntil()
> **dateUntil**(`one`, `two`, `options?`): `Duration`
Defined in: [src/calendars/HijriCalendar.ts:384](https://github.com/acamarata/temporal-hijri/blob/077861c7dc33e5562fb75c1defbee0f2db4e2f2a/src/calendars/HijriCalendar.ts#L384)
Compute the difference between two Hijri dates.
For simplicity and correctness across variable-length Hijri months, this
delegates to the underlying ISO PlainDate difference when the largest unit
is days or weeks. Year/month differences require a Hijri-space calculation.
#### Parameters
##### one
`PlainDate`
##### two
`PlainDate`
##### options?
###### largestUnit?
`DateUnit`
#### Returns
`Duration`
#### Inherited from
[`HijriCalendar`](HijriCalendar.md).[`dateUntil`](HijriCalendar.md#dateuntil)
***
### day()
> **day**(`date`): `number`
Defined in: [src/calendars/HijriCalendar.ts:169](https://github.com/acamarata/temporal-hijri/blob/077861c7dc33e5562fb75c1defbee0f2db4e2f2a/src/calendars/HijriCalendar.ts#L169)
Returns the day of the Hijri month (1-29 or 1-30).
#### Parameters
##### date
`PlainDate`
A Temporal.PlainDate with ISO (Gregorian) coordinates.
#### Returns
`number`
Day of month within the Hijri calendar.
#### Inherited from
[`HijriCalendar`](HijriCalendar.md).[`day`](HijriCalendar.md#day)
***
### dayOfWeek()
> **dayOfWeek**(`date`): `number`
Defined in: [src/calendars/HijriCalendar.ts:234](https://github.com/acamarata/temporal-hijri/blob/077861c7dc33e5562fb75c1defbee0f2db4e2f2a/src/calendars/HijriCalendar.ts#L234)
ISO weekday: 1 = Monday, 7 = Sunday.
PlainDate.dayOfWeek on an ISO-calendar date already gives ISO weekday,
so no conversion is needed.
#### Parameters
##### date
`PlainDate`
#### Returns
`number`
#### Inherited from
[`HijriCalendar`](HijriCalendar.md).[`dayOfWeek`](HijriCalendar.md#dayofweek)
***
### dayOfYear()
> **dayOfYear**(`date`): `number`
Defined in: [src/calendars/HijriCalendar.ts:242](https://github.com/acamarata/temporal-hijri/blob/077861c7dc33e5562fb75c1defbee0f2db4e2f2a/src/calendars/HijriCalendar.ts#L242)
Day within the Hijri year. Accumulates full months before the current one,
then adds the day-of-month offset.
#### Parameters
##### date
`PlainDate`
#### Returns
`number`
#### Inherited from
[`HijriCalendar`](HijriCalendar.md).[`dayOfYear`](HijriCalendar.md#dayofyear)
***
### daysInMonth()
> **daysInMonth**(`date`): `number`
Defined in: [src/calendars/HijriCalendar.ts:184](https://github.com/acamarata/temporal-hijri/blob/077861c7dc33e5562fb75c1defbee0f2db4e2f2a/src/calendars/HijriCalendar.ts#L184)
Returns the number of days in the Hijri month containing the given date.
Hijri months alternate between 29 and 30 days, but the exact pattern
differs by calendar system (UAQ uses fixed tables; FCNA uses calculation).
#### Parameters
##### date
`PlainDate`
A Temporal.PlainDate with ISO (Gregorian) coordinates.
#### Returns
`number`
29 or 30.
#### Inherited from
[`HijriCalendar`](HijriCalendar.md).[`daysInMonth`](HijriCalendar.md#daysinmonth)
***
### daysInWeek()
> **daysInWeek**(`_date`): `number`
Defined in: [src/calendars/HijriCalendar.ts:263](https://github.com/acamarata/temporal-hijri/blob/077861c7dc33e5562fb75c1defbee0f2db4e2f2a/src/calendars/HijriCalendar.ts#L263)
Returns the number of days in a week.
Always 7. Required by the Temporal Calendar Protocol.
#### Parameters
##### \_date
`PlainDate`
#### Returns
`number`
Always 7.
#### Inherited from
[`HijriCalendar`](HijriCalendar.md).[`daysInWeek`](HijriCalendar.md#daysinweek)
***
### daysInYear()
> **daysInYear**(`date`): `number`
Defined in: [src/calendars/HijriCalendar.ts:193](https://github.com/acamarata/temporal-hijri/blob/077861c7dc33e5562fb75c1defbee0f2db4e2f2a/src/calendars/HijriCalendar.ts#L193)
Sum all 12 month lengths for the Hijri year. Standard lunar years are 354
days; leap years (with an added day in Dhul-Hijja) are 355 days.
#### Parameters
##### date
`PlainDate`
#### Returns
`number`
#### Inherited from
[`HijriCalendar`](HijriCalendar.md).[`daysInYear`](HijriCalendar.md#daysinyear)
***
### fields()
> **fields**(`fields`): `string`[]
Defined in: [src/calendars/HijriCalendar.ts:273](https://github.com/acamarata/temporal-hijri/blob/077861c7dc33e5562fb75c1defbee0f2db4e2f2a/src/calendars/HijriCalendar.ts#L273)
Return the list of fields that the calendar adds to a Temporal object.
Non-era calendars return the input array unchanged.
#### Parameters
##### fields
`string`[]
#### Returns
`string`[]
#### Inherited from
[`HijriCalendar`](HijriCalendar.md).[`fields`](HijriCalendar.md#fields)
***
### inLeapYear()
> **inLeapYear**(`date`): `boolean`
Defined in: [src/calendars/HijriCalendar.ts:223](https://github.com/acamarata/temporal-hijri/blob/077861c7dc33e5562fb75c1defbee0f2db4e2f2a/src/calendars/HijriCalendar.ts#L223)
Returns whether the Hijri year is a leap year (355 days).
Standard Hijri years have 354 days. A leap year adds one day to
Dhul-Hijja (month 12), making it 355 days total.
#### Parameters
##### date
`PlainDate`
A Temporal.PlainDate with ISO (Gregorian) coordinates.
#### Returns
`boolean`
`true` if the year has 355 days.
#### Inherited from
[`HijriCalendar`](HijriCalendar.md).[`inLeapYear`](HijriCalendar.md#inleapyear)
***
### mergeFields()
> **mergeFields**(`fields`, `additionalFields`): `Record`\<`string`, `unknown`\>
Defined in: [src/calendars/HijriCalendar.ts:414](https://github.com/acamarata/temporal-hijri/blob/077861c7dc33e5562fb75c1defbee0f2db4e2f2a/src/calendars/HijriCalendar.ts#L414)
#### Parameters
##### fields
`Record`\<`string`, `unknown`\>
##### additionalFields
`Record`\<`string`, `unknown`\>
#### Returns
`Record`\<`string`, `unknown`\>
#### Inherited from
[`HijriCalendar`](HijriCalendar.md).[`mergeFields`](HijriCalendar.md#mergefields)
***
### month()
> **month**(`date`): `number`
Defined in: [src/calendars/HijriCalendar.ts:149](https://github.com/acamarata/temporal-hijri/blob/077861c7dc33e5562fb75c1defbee0f2db4e2f2a/src/calendars/HijriCalendar.ts#L149)
Returns the Hijri month (1-12) for the given ISO date.
#### Parameters
##### date
`PlainDate`
A Temporal.PlainDate with ISO (Gregorian) coordinates.
#### Returns
`number`
Month number 1 (Muharram) through 12 (Dhul-Hijja).
#### Inherited from
[`HijriCalendar`](HijriCalendar.md).[`month`](HijriCalendar.md#month)
***
### monthCode()
> **monthCode**(`date`): `string`
Defined in: [src/calendars/HijriCalendar.ts:158](https://github.com/acamarata/temporal-hijri/blob/077861c7dc33e5562fb75c1defbee0f2db4e2f2a/src/calendars/HijriCalendar.ts#L158)
Month code per the Temporal proposal: "M01".."M12".
Hijri months are always 1-12 (no leap/intercalary month), so the code is
simply the zero-padded month number.
#### Parameters
##### date
`PlainDate`
#### Returns
`string`
#### Inherited from
[`HijriCalendar`](HijriCalendar.md).[`monthCode`](HijriCalendar.md#monthcode)
***
### monthDayFromFields()
> **monthDayFromFields**(`fields`, `options?`): `PlainMonthDay`
Defined in: [src/calendars/HijriCalendar.ts:317](https://github.com/acamarata/temporal-hijri/blob/077861c7dc33e5562fb75c1defbee0f2db4e2f2a/src/calendars/HijriCalendar.ts#L317)
ISO-anchored PlainMonthDay per the Temporal Calendar Protocol.
Reference year 1444 is intentional: it is a recent, well-covered UAQ year
used to anchor the ISO coordinates when no year is supplied.
#### Parameters
##### fields
###### day
`number`
###### month
`number`
###### year?
`number`
##### options?
###### overflow?
`"constrain"` \| `"reject"`
#### Returns
`PlainMonthDay`
#### Inherited from
[`HijriCalendar`](HijriCalendar.md).[`monthDayFromFields`](HijriCalendar.md#monthdayfromfields)
***
### monthsInYear()
> **monthsInYear**(`_date`): `number`
Defined in: [src/calendars/HijriCalendar.ts:210](https://github.com/acamarata/temporal-hijri/blob/077861c7dc33e5562fb75c1defbee0f2db4e2f2a/src/calendars/HijriCalendar.ts#L210)
Returns the number of months in the Hijri year.
Always 12. Unlike the Hebrew calendar, the Hijri lunar calendar has no
intercalary (leap) month — only a possible extra day in Dhul-Hijja.
#### Parameters
##### \_date
`PlainDate`
#### Returns
`number`
Always 12.
#### Inherited from
[`HijriCalendar`](HijriCalendar.md).[`monthsInYear`](HijriCalendar.md#monthsinyear)
***
### toString()
> **toString**(): `string`
Defined in: [src/calendars/HijriCalendar.ts:66](https://github.com/acamarata/temporal-hijri/blob/077861c7dc33e5562fb75c1defbee0f2db4e2f2a/src/calendars/HijriCalendar.ts#L66)
#### Returns
`string`
#### Inherited from
[`HijriCalendar`](HijriCalendar.md).[`toString`](HijriCalendar.md#tostring)
***
### weekOfYear()
> **weekOfYear**(`date`): `number`
Defined in: [src/calendars/HijriCalendar.ts:252](https://github.com/acamarata/temporal-hijri/blob/077861c7dc33e5562fb75c1defbee0f2db4e2f2a/src/calendars/HijriCalendar.ts#L252)
Hijri week number counted from day 1 of Muharram (day 1-7 = week 1). No ISO week alignment.
#### Parameters
##### date
`PlainDate`
#### Returns
`number`
#### Inherited from
[`HijriCalendar`](HijriCalendar.md).[`weekOfYear`](HijriCalendar.md#weekofyear)
***
### year()
> **year**(`date`): `number`
Defined in: [src/calendars/HijriCalendar.ts:139](https://github.com/acamarata/temporal-hijri/blob/077861c7dc33e5562fb75c1defbee0f2db4e2f2a/src/calendars/HijriCalendar.ts#L139)
Returns the Hijri year for the given ISO date.
#### Parameters
##### date
`PlainDate`
A Temporal.PlainDate with ISO (Gregorian) coordinates.
#### Returns
`number`
The Hijri year, e.g. 1444.
#### Inherited from
[`HijriCalendar`](HijriCalendar.md).[`year`](HijriCalendar.md#year)
***
### yearMonthFromFields()
> **yearMonthFromFields**(`fields`, `options?`): `PlainYearMonth`
Defined in: [src/calendars/HijriCalendar.ts:294](https://github.com/acamarata/temporal-hijri/blob/077861c7dc33e5562fb75c1defbee0f2db4e2f2a/src/calendars/HijriCalendar.ts#L294)
ISO-anchored PlainYearMonth per the Temporal Calendar Protocol.
The resulting PlainYearMonth stores ISO coordinates internally, representing
the Hijri month that starts on that ISO year/month.
#### Parameters
##### fields
###### month
`number`
###### year
`number`
##### options?
###### overflow?
`"constrain"` \| `"reject"`
#### Returns
`PlainYearMonth`
#### Inherited from
[`HijriCalendar`](HijriCalendar.md).[`yearMonthFromFields`](HijriCalendar.md#yearmonthfromfields)

View file

@ -0,0 +1,111 @@
[**temporal-hijri v1.0.1**](../README.md)
***
[temporal-hijri](../README.md) / CalendarEngine
# Interface: CalendarEngine
Defined in: node\_modules/.pnpm/hijri-core@1.0.0/node\_modules/hijri-core/dist/index.d.mts:13
## Properties
### id
> `readonly` **id**: `string`
Defined in: node\_modules/.pnpm/hijri-core@1.0.0/node\_modules/hijri-core/dist/index.d.mts:14
## Methods
### daysInMonth()
> **daysInMonth**(`hy`, `hm`): `number`
Defined in: node\_modules/.pnpm/hijri-core@1.0.0/node\_modules/hijri-core/dist/index.d.mts:19
#### Parameters
##### hy
`number`
##### hm
`number`
#### Returns
`number`
***
### isValid()
> **isValid**(`hy`, `hm`, `hd`): `boolean`
Defined in: node\_modules/.pnpm/hijri-core@1.0.0/node\_modules/hijri-core/dist/index.d.mts:18
#### Parameters
##### hy
`number`
##### hm
`number`
##### hd
`number`
#### Returns
`boolean`
***
### toGregorian()
> **toGregorian**(`hy`, `hm`, `hd`): `Date` \| `null`
Defined in: node\_modules/.pnpm/hijri-core@1.0.0/node\_modules/hijri-core/dist/index.d.mts:17
Returns null for invalid or out-of-range input. Never throws.
#### Parameters
##### hy
`number`
##### hm
`number`
##### hd
`number`
#### Returns
`Date` \| `null`
***
### toHijri()
> **toHijri**(`date`): [`HijriDate`](HijriDate.md) \| `null`
Defined in: node\_modules/.pnpm/hijri-core@1.0.0/node\_modules/hijri-core/dist/index.d.mts:15
#### Parameters
##### date
`Date`
#### Returns
[`HijriDate`](HijriDate.md) \| `null`

View file

@ -0,0 +1,17 @@
[**temporal-hijri v1.0.1**](../README.md)
***
[temporal-hijri](../README.md) / ConversionOptions
# Interface: ConversionOptions
Defined in: node\_modules/.pnpm/hijri-core@1.0.0/node\_modules/hijri-core/dist/index.d.mts:21
## Properties
### calendar?
> `optional` **calendar?**: `string`
Defined in: node\_modules/.pnpm/hijri-core@1.0.0/node\_modules/hijri-core/dist/index.d.mts:22

View file

@ -0,0 +1,33 @@
[**temporal-hijri v1.0.1**](../README.md)
***
[temporal-hijri](../README.md) / HijriDate
# Interface: HijriDate
Defined in: node\_modules/.pnpm/hijri-core@1.0.0/node\_modules/hijri-core/dist/index.d.mts:1
## Properties
### hd
> **hd**: `number`
Defined in: node\_modules/.pnpm/hijri-core@1.0.0/node\_modules/hijri-core/dist/index.d.mts:4
***
### hm
> **hm**: `number`
Defined in: node\_modules/.pnpm/hijri-core@1.0.0/node\_modules/hijri-core/dist/index.d.mts:3
***
### hy
> **hy**: `number`
Defined in: node\_modules/.pnpm/hijri-core@1.0.0/node\_modules/hijri-core/dist/index.d.mts:2

View file

@ -0,0 +1,11 @@
[**temporal-hijri v1.0.1**](../README.md)
***
[temporal-hijri](../README.md) / fcnaCalendar
# Variable: fcnaCalendar
> `const` **fcnaCalendar**: [`FcnaCalendar`](../classes/FcnaCalendar.md)
Defined in: [src/index.ts:12](https://github.com/acamarata/temporal-hijri/blob/077861c7dc33e5562fb75c1defbee0f2db4e2f2a/src/index.ts#L12)

View file

@ -0,0 +1,11 @@
[**temporal-hijri v1.0.1**](../README.md)
***
[temporal-hijri](../README.md) / uaqCalendar
# Variable: uaqCalendar
> `const` **uaqCalendar**: [`UaqCalendar`](../classes/UaqCalendar.md)
Defined in: [src/index.ts:11](https://github.com/acamarata/temporal-hijri/blob/077861c7dc33e5562fb75c1defbee0f2db4e2f2a/src/index.ts#L11)

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

@ -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.

76
.github/wiki/examples/basic-usage.md vendored Normal file
View file

@ -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
```

View file

@ -0,0 +1,99 @@
# Example: Scheduling Display with Hijri Dates
A common need in calendaring apps for Muslim communities is displaying both the
Gregorian and Hijri dates for an event. This example shows how to take a list of
event dates, annotate each with its Hijri date, and display it in a human-readable
format.
## Setup
```typescript
import { Temporal } from '@js-temporal/polyfill';
import { uaqCalendar } from 'temporal-hijri';
```
## Month name lookup
The calendar returns numeric months (1-12). Map them to names:
```typescript
const HIJRI_MONTHS = [
'Muharram', 'Safar', 'Rabi al-Awwal', 'Rabi al-Thani',
'Jumada al-Ula', 'Jumada al-Akhira', 'Rajab', 'Shaban',
'Ramadan', 'Shawwal', 'Dhul-Qadah', 'Dhul-Hijja',
];
function hijriMonthName(month: number): string {
return HIJRI_MONTHS[month - 1] ?? 'Unknown';
}
```
## Format a single date
```typescript
function formatWithHijri(isoDateStr: string): string {
const isoDate = Temporal.PlainDate.from(isoDateStr);
const hy = uaqCalendar.year(isoDate);
const hm = uaqCalendar.month(isoDate);
const hd = uaqCalendar.day(isoDate);
const monthName = hijriMonthName(hm);
return `${isoDateStr} (${hd} ${monthName} ${hy} AH)`;
}
```
## Annotate a schedule
```typescript
const events = [
{ title: 'Project kickoff', date: '2025-01-01' },
{ title: 'Mid-year review', date: '2025-06-15' },
{ title: 'Year-end summary', date: '2025-12-31' },
];
for (const event of events) {
console.log(`${event.title}: ${formatWithHijri(event.date)}`);
}
```
Output:
```
Project kickoff: 2025-01-01 (2 Rajab 1446 AH)
Mid-year review: 2025-06-15 (19 Dhul-Hijja 1446 AH)
Year-end summary: 2025-12-31 (11 Jumada al-Akhira 1447 AH)
```
## Find the start of Ramadan for a given Hijri year
```typescript
function ramadanStart(hijriYear: number): Temporal.PlainDate {
// 1 Ramadan = month 9, day 1
return uaqCalendar.dateFromFields({ year: hijriYear, month: 9, day: 1 });
}
const ramadan1447 = ramadanStart(1447);
console.log(ramadan1447.toString()); // 2026-02-18 (approximate)
```
## Count days until an event in Hijri months
```typescript
const today = Temporal.Now.plainDateISO();
const eid = uaqCalendar.dateFromFields({ year: 1447, month: 10, day: 1 });
const diff = uaqCalendar.dateUntil(today, eid, { largestUnit: 'months' });
console.log(`Eid al-Fitr 1447 is in ${diff.months} month(s) and ${diff.days} day(s)`);
```
## Notes
- Month names are transliterated from Arabic. Adapt the spelling to your style guide.
- UAQ covers 1318-1500 AH. For dates outside that range, substitute `fcnaCalendar`.
- `Temporal.Now.plainDateISO()` returns the current date in the host's local calendar.
It does not return a Hijri date directly; pass the result to the calendar methods
to get Hijri coordinates.
---
[Home](../Home) · [Basic Usage](basic-usage) · [API Reference](../API-Reference)

91
.github/wiki/guides/advanced.md vendored Normal file
View file

@ -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.

98
.github/wiki/guides/quickstart.md vendored Normal file
View file

@ -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

View file

@ -15,7 +15,8 @@ jobs:
node: [20, 22, 24]
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- name: Enable corepack
run: corepack enable
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
@ -30,7 +31,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- name: Enable corepack
run: corepack enable
- uses: actions/setup-node@v4
with:
node-version: 24
@ -44,7 +46,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- name: Enable corepack
run: corepack enable
- uses: actions/setup-node@v4
with:
node-version: 24
@ -57,7 +60,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- name: Enable corepack
run: corepack enable
- uses: actions/setup-node@v4
with:
node-version: 24
@ -74,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

1
.gitignore vendored
View file

@ -1,5 +1,6 @@
node_modules/
dist/
coverage/
*.tgz
*.log
.DS_Store

View file

@ -1,6 +0,0 @@
{
"singleQuote": true,
"trailingComma": "all",
"printWidth": 100,
"semi": true
}

View file

@ -5,7 +5,22 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [1.0.3] - 2026-06-10
### Fixed
- Date handed to hijri-core is now built via `Date.UTC()` to match hijri-core's UTC-day
contract; fixes previous-day results on east-of-UTC hosts (e.g. UTC+5, UTC+8).
Requires hijri-core 1.0.3.
## [1.0.2] - 2026-05-30
### Added
- TSDoc comments on all public `HijriCalendar` methods
### Changed
- README condensed; quickstart trimmed to essential examples
- CI: corepack before setup-node, prettier scoped to src/, d.mts emitted via postbuild
- Adopt shared config packages (@acamarata/eslint-config, @acamarata/prettier-config, @acamarata/tsconfig)
## [1.0.1] - 2026-05-28

198
README.md
View file

@ -4,207 +4,79 @@
# 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.
---
Provides `UaqCalendar` (Umm al-Qura) and `FcnaCalendar` (FCNA/ISNA) as plug-in
calendars for `Temporal.PlainDate`. The underlying conversion logic comes from
[hijri-core](https://github.com/acamarata/hijri-core).
## Installation
```bash
pnpm add temporal-hijri hijri-core
# Add the polyfill if native Temporal is unavailable:
pnpm add @js-temporal/polyfill
```
If you are using the polyfill instead of the native `Temporal` API:
```bash
pnpm add temporal-hijri hijri-core @js-temporal/polyfill
```
---
## Quick Start
```typescript
import { Temporal } from '@js-temporal/polyfill'; // or use native Temporal
import { Temporal } from '@js-temporal/polyfill';
import { uaqCalendar } from 'temporal-hijri';
// Convert an ISO date to Hijri coordinates
const isoDate = Temporal.PlainDate.from('2023-03-23');
console.log(uaqCalendar.year(isoDate)); // 1444
console.log(uaqCalendar.month(isoDate)); // 9 (Ramadan)
console.log(uaqCalendar.day(isoDate)); // 1
console.log(uaqCalendar.monthCode(isoDate)); // "M09"
console.log(uaqCalendar.inLeapYear(isoDate)); // false (1444 is 354 days)
// Convert Hijri coordinates back to ISO
// Convert Hijri coordinates to ISO
const ramadan = uaqCalendar.dateFromFields({ year: 1444, month: 9, day: 1 });
console.log(ramadan.toString()); // "2023-03-23"
// Arithmetic in Hijri space
// Date arithmetic in Hijri space
const { Duration } = Temporal;
const nextMonth = uaqCalendar.dateAdd(isoDate, new Duration(0, 1));
console.log(uaqCalendar.month(nextMonth)); // 10 (Shawwal)
console.log(nextMonth.toString()); // "2023-04-21"
```
---
## Calendars
## Calendar Classes
### `UaqCalendar`
Implements the Umm al-Qura calendar, the official calendar of Saudi Arabia. Month boundaries come from pre-calculated tables covering 1318-1500 AH (Gregorian 1900-2076). The most widely used Hijri calendar standard for civil and religious purposes.
```typescript
import { UaqCalendar } from 'temporal-hijri';
const cal = new UaqCalendar(); // cal.id === 'hijri-uaq'
```
### `FcnaCalendar`
Implements the FCNA/ISNA calendar used by the Fiqh Council of North America and the Islamic Society of North America. Month starts are determined by astronomical new moon calculation (Meeus Chapter 49): if conjunction occurs before 12:00 UTC, the month begins the next day; if at or after noon, it begins the day after that.
```typescript
import { FcnaCalendar } from 'temporal-hijri';
const cal = new FcnaCalendar(); // cal.id === 'hijri-fcna'
```
### `HijriCalendar` (base class)
The base implementation. Accepts any `CalendarEngine` from hijri-core. Use this to build a Temporal calendar from a custom engine registered via `hijri-core`'s `registerCalendar()`.
```typescript
import { HijriCalendar } from 'temporal-hijri';
import { getCalendar, registerCalendar } from 'hijri-core';
// Register a custom engine first
registerCalendar('my-calendar', myEngine);
const cal = new HijriCalendar(getCalendar('my-calendar'));
// cal.id === 'hijri-my-calendar'
```
### Convenience singletons
`uaqCalendar` and `fcnaCalendar` are pre-constructed instances. They are shared objects and safe to reuse across calls.
```typescript
import { uaqCalendar, fcnaCalendar } from 'temporal-hijri';
```
---
## API
All methods receive a `Temporal.PlainDate` with an ISO (Gregorian) calendar. The PlainDate carries the ISO year/month/day; the calendar object interprets those coordinates.
| Method | Returns | Description |
|---|---|---|
| `year(date)` | `number` | Hijri year |
| `month(date)` | `number` | Hijri month (1-12) |
| `monthCode(date)` | `string` | Month code: `"M01"` through `"M12"` |
| `day(date)` | `number` | Day of the Hijri month (1-29 or 1-30) |
| `daysInMonth(date)` | `number` | Length of the Hijri month (29 or 30) |
| `daysInYear(date)` | `number` | Days in the Hijri year (354 or 355) |
| `monthsInYear(date)` | `number` | Always `12` |
| `inLeapYear(date)` | `boolean` | `true` if the year has 355 days |
| `dayOfWeek(date)` | `number` | ISO weekday: 1=Monday, 7=Sunday |
| `dayOfYear(date)` | `number` | Day position within the Hijri year |
| `weekOfYear(date)` | `number` | Week position within the Hijri year |
| `daysInWeek(date)` | `number` | Always `7` |
| `dateFromFields(fields)` | `Temporal.PlainDate` | Construct ISO PlainDate from `{year, month, day}` in Hijri |
| `yearMonthFromFields(fields)` | `Temporal.PlainYearMonth` | Construct from `{year, month}` in Hijri |
| `monthDayFromFields(fields)` | `Temporal.PlainMonthDay` | Construct from `{month, day}` in Hijri |
| `dateAdd(date, duration)` | `Temporal.PlainDate` | Add a duration; years/months applied in Hijri space, days in ISO space |
| `dateUntil(one, two, options)` | `Temporal.Duration` | Difference between two dates; supports `largestUnit: 'years'|'months'|'days'|'weeks'` |
| `mergeFields(fields, additional)` | `Record` | Merge field objects (Temporal protocol requirement) |
| `toString()` | `string` | Calendar identifier (`"hijri-uaq"` or `"hijri-fcna"`) |
---
## Calendar Systems
| System | ID | Authority | Method | Coverage |
|---|---|---|---|---|
| Umm al-Qura | `hijri-uaq` | KACST / Saudi Arabia | Pre-calculated tables | 1318-1500 AH (1900-2076 CE) |
| FCNA/ISNA | `hijri-fcna` | Fiqh Council of North America | Astronomical new moon (Meeus) | Unlimited (calculated) |
UAQ dates outside 1318-1500 AH throw `RangeError`. FCNA is unbounded but loses precision for very early dates.
---
## Custom Calendars
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'
```
---
## TypeScript
All types are exported:
```typescript
import type { HijriDate, ConversionOptions, HijriCalendarOptions } from 'temporal-hijri';
```
The package ships dual CJS/ESM builds with full `.d.ts` and `.d.mts` declarations.
---
## Architecture
A thin adapter over [hijri-core](https://github.com/acamarata/hijri-core) that maps the Temporal Calendar Protocol to Hijri calendar engine calls. The `UaqCalendar` and `FcnaCalendar` classes implement `Temporal.CalendarProtocol`: the package itself adds no calendar math.
For more detail see the [Architecture wiki page](https://github.com/acamarata/temporal-hijri/wiki/Architecture).
## Compatibility
- Node.js 20, 22, 24
- Any bundler supporting `exports` field (`Vite`, `Webpack 5`, `Rollup`, `esbuild`)
- ESM (`import`) and CommonJS (`require`): both provided
- No native `Temporal` required: works entirely with `@js-temporal/polyfill`
---
| Calendar | ID | Authority | Method | Coverage |
|-------------|--------------|--------------------|--------------------------|------------------|
| Umm al-Qura | `hijri-uaq` | KACST, Saudi Arabia | Pre-calculated tables | 1318-1500 AH |
| FCNA/ISNA | `hijri-fcna` | Fiqh Council of NA | Astronomical new moon | Unbounded |
## Documentation
Full reference, architecture notes, and algorithmic detail in the [wiki](https://github.com/acamarata/temporal-hijri/wiki).
Full reference in the [wiki](https://github.com/acamarata/temporal-hijri/wiki).
---
- [API Reference](https://github.com/acamarata/temporal-hijri/wiki/API-Reference)
- [Architecture](https://github.com/acamarata/temporal-hijri/wiki/Architecture)
- [Examples](https://github.com/acamarata/temporal-hijri/wiki/examples/basic-usage)
## Conversion behavior
Conversions between ISO and Hijri dates are pure calendar-date mappings: the same
ISO date always maps to the same Hijri date on every machine, regardless of the host's
timezone. `Temporal.PlainDate` carries no time-of-day information, and the underlying
hijri-core engine operates on UTC calendar days, so there is no timezone dependency.
Note: the Islamic calendar begins a new day at sunset, not midnight. This library
follows the civil-calendar convention (midnight boundary) used by most software. Sunset
day-start determination is out of scope.
## Related
- [hijri-core](https://github.com/acamarata/hijri-core): zero-dependency Hijri engine powering this package
- [luxon-hijri](https://github.com/acamarata/luxon-hijri): Hijri/Gregorian conversion for Luxon
- [hijri-core](https://github.com/acamarata/hijri-core): the underlying calendar engine
- [luxon-hijri](https://github.com/acamarata/luxon-hijri): Hijri support for Luxon
- [pray-calc](https://github.com/acamarata/pray-calc): Islamic prayer times
---
## Telemetry
## Acknowledgments
Calendar data and algorithms provided by [hijri-core](https://github.com/acamarata/hijri-core). The Umm al-Qura table is derived from data published by the King Abdulaziz City for Science and Technology (KACST). FCNA new moon calculations follow Jean Meeus, "Astronomical Algorithms," 2nd ed., Chapter 49.
---
This package supports opt-in anonymous usage telemetry — off by default.
Enable: `ACAMARATA_TELEMETRY=1`. See [TELEMETRY.md](./TELEMETRY.md) for what is sent and how to disable.
## License

8
TELEMETRY.md Normal file
View file

@ -0,0 +1,8 @@
# Telemetry Disclosure
This package supports opt-in anonymous usage telemetry via [`@acamarata/telemetry`](https://github.com/acamarata/telemetry).
Telemetry is **off by default**. No data is sent unless you set `ACAMARATA_TELEMETRY=1`.
Full disclosure (what is sent, where it goes, how to disable):
[github.com/acamarata/telemetry/blob/main/TELEMETRY.md](https://github.com/acamarata/telemetry/blob/main/TELEMETRY.md)

View file

@ -1,20 +1,20 @@
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
import eslintConfigPrettier from 'eslint-config-prettier';
import tsParser from "@typescript-eslint/parser";
import tsPlugin from "@typescript-eslint/eslint-plugin";
import eslintConfigPrettier from "eslint-config-prettier";
import { typescript } from "@acamarata/eslint-config";
export default tseslint.config(
eslint.configs.recommended,
...tseslint.configs.recommended,
eslintConfigPrettier,
export default [
{
ignores: ['dist/', 'node_modules/', 'test.mjs', 'test-cjs.cjs'],
},
{
rules: {
'@typescript-eslint/no-unused-vars': [
'error',
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
],
files: ["**/*.ts"],
plugins: { "@typescript-eslint": tsPlugin },
languageOptions: {
parser: tsParser,
parserOptions: { project: true, tsconfigRootDir: import.meta.dirname },
},
},
);
...typescript.map((config) => ({ files: ["**/*.ts"], ...config })),
eslintConfigPrettier,
{
ignores: ["dist/", "node_modules/", "test.mjs", "test-cjs.cjs"],
},
];

View file

@ -1,6 +1,6 @@
{
"name": "temporal-hijri",
"version": "1.0.1",
"version": "1.0.3",
"description": "Temporal Calendar Protocol implementation for the Hijri calendar system. Supports Umm al-Qura and FCNA calendars via hijri-core.",
"author": "Aric Camarata",
"license": "MIT",
@ -37,8 +37,10 @@
"format:check": "prettier --check src/ test.mjs test-cjs.cjs eslint.config.mjs tsup.config.ts",
"pretest": "tsup",
"test": "node --test test.mjs && node --test test-cjs.cjs",
"prepublishOnly": "tsup",
"coverage": "c8 --reporter=lcov --reporter=text node --test"
"prepack": "pnpm run build",
"coverage": "c8 --reporter=lcov --reporter=text node test.mjs",
"docs": "typedoc --out .github/wiki/api src/index.ts",
"postbuild": "cp dist/index.d.ts dist/index.d.mts"
},
"keywords": [
"temporal",
@ -62,16 +64,25 @@
}
},
"devDependencies": {
"@acamarata/eslint-config": "^0.1.0",
"@acamarata/prettier-config": "^0.1.0",
"@acamarata/tsconfig": "^0.1.0",
"@eslint/js": "^10.0.1",
"@js-temporal/polyfill": "^0.4.4",
"@types/node": "^22.0.0",
"@typescript-eslint/eslint-plugin": "^8.56.1",
"@typescript-eslint/parser": "^8.56.1",
"c8": "^11.0.0",
"eslint": "^10.0.3",
"eslint-config-prettier": "^10.1.8",
"hijri-core": "^1.0.0",
"hijri-core": "^1.0.3",
"prettier": "^3.8.1",
"tsup": "^8.0.0",
"typedoc": "^0.28.19",
"typedoc-plugin-markdown": "^4.11.0",
"typescript": "^5.5.0",
"typescript-eslint": "^8.56.1"
"typescript-eslint": "^8.56.1",
"@acamarata/telemetry": "^0.1.0"
},
"publishConfig": {
"access": "public",
@ -85,5 +96,6 @@
"bugs": {
"url": "https://github.com/acamarata/temporal-hijri/issues"
},
"type": "module"
"type": "module",
"prettier": "@acamarata/prettier-config"
}

View file

@ -8,6 +8,18 @@ importers:
.:
devDependencies:
'@acamarata/eslint-config':
specifier: ^0.1.0
version: 0.1.0(@typescript-eslint/eslint-plugin@8.56.1(@typescript-eslint/parser@8.56.1(eslint@10.0.3)(typescript@5.9.3))(eslint@10.0.3)(typescript@5.9.3))(@typescript-eslint/parser@8.56.1(eslint@10.0.3)(typescript@5.9.3))(eslint-config-prettier@10.1.8(eslint@10.0.3))(eslint@10.0.3)
'@acamarata/prettier-config':
specifier: ^0.1.0
version: 0.1.0(prettier@3.8.1)
'@acamarata/telemetry':
specifier: ^0.1.0
version: 0.1.0
'@acamarata/tsconfig':
specifier: ^0.1.0
version: 0.1.0
'@eslint/js':
specifier: ^10.0.1
version: 10.0.1(eslint@10.0.3)
@ -17,6 +29,15 @@ importers:
'@types/node':
specifier: ^22.0.0
version: 22.19.11
'@typescript-eslint/eslint-plugin':
specifier: ^8.56.1
version: 8.56.1(@typescript-eslint/parser@8.56.1(eslint@10.0.3)(typescript@5.9.3))(eslint@10.0.3)(typescript@5.9.3)
'@typescript-eslint/parser':
specifier: ^8.56.1
version: 8.56.1(eslint@10.0.3)(typescript@5.9.3)
c8:
specifier: ^11.0.0
version: 11.0.0
eslint:
specifier: ^10.0.3
version: 10.0.3
@ -24,14 +45,20 @@ importers:
specifier: ^10.1.8
version: 10.1.8(eslint@10.0.3)
hijri-core:
specifier: ^1.0.0
version: 1.0.0
specifier: ^1.0.3
version: 1.0.3
prettier:
specifier: ^3.8.1
version: 3.8.1
tsup:
specifier: ^8.0.0
version: 8.5.1(typescript@5.9.3)
version: 8.5.1(typescript@5.9.3)(yaml@2.9.0)
typedoc:
specifier: ^0.28.19
version: 0.28.19(typescript@5.9.3)
typedoc-plugin-markdown:
specifier: ^4.11.0
version: 4.11.0(typedoc@0.28.19(typescript@5.9.3))
typescript:
specifier: ^5.5.0
version: 5.9.3
@ -41,6 +68,40 @@ importers:
packages:
'@acamarata/eslint-config@0.1.0':
resolution: {integrity: sha512-St2TObpHKXBLBy1GPrXWlHzXCujJ7jaor6BuWRdLMTOjM56LPBDJx+898R7yFyI8Wi52VWNZdSTPxzHuyRAq+A==}
engines: {node: '>=20'}
peerDependencies:
'@typescript-eslint/eslint-plugin': '>=8.0.0'
'@typescript-eslint/parser': '>=8.0.0'
eslint: '>=9.0.0'
eslint-config-prettier: '>=9.0.0'
eslint-plugin-react: '>=7.0.0'
eslint-plugin-react-hooks: '>=5.0.0'
peerDependenciesMeta:
eslint-plugin-react:
optional: true
eslint-plugin-react-hooks:
optional: true
'@acamarata/prettier-config@0.1.0':
resolution: {integrity: sha512-ImMnz/653ettR4gJVd1f7Pz61DQSeDmUtguLrnCl8RdYncqfghT+QGEO5Znml0KvgX3vk4c4roPAWYdXaZgcPA==}
engines: {node: '>=20'}
peerDependencies:
prettier: '>=3.0.0'
'@acamarata/telemetry@0.1.0':
resolution: {integrity: sha512-iP09ZD0bHencHLbv6kQZDgwN9crLCWGKxmiMrfJjhBCoWTgv4koSgg0Li/LFKwCCFluua6orj9fVeQ8eqcJXSQ==}
engines: {node: '>=20'}
'@acamarata/tsconfig@0.1.0':
resolution: {integrity: sha512-bgzyBak43mE+0HhduZX3cvaPjKcggtGGZZMjr35qtYWolsIWgZ9nx7OOswbVYoU35qoUv6rZ0mTK6GbZ8QTYjw==}
engines: {node: '>=20'}
'@bcoe/v8-coverage@1.0.2':
resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==}
engines: {node: '>=18'}
'@esbuild/aix-ppc64@0.27.3':
resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==}
engines: {node: '>=18'}
@ -236,6 +297,9 @@ packages:
resolution: {integrity: sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
'@gerrit0/mini-shiki@3.23.0':
resolution: {integrity: sha512-bEMORlG0cqdjVyCEuU0cDQbORWX+kYCeo0kV1lbxF5bt4r7SID2l9bqsxJEM0zndaxpOUT7riCyIVEuqq/Ynxg==}
'@humanfs/core@0.19.1':
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
engines: {node: '>=18.18.0'}
@ -252,6 +316,10 @@ packages:
resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
engines: {node: '>=18.18'}
'@istanbuljs/schema@0.1.6':
resolution: {integrity: sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==}
engines: {node: '>=8'}
'@jridgewell/gen-mapping@0.3.13':
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
@ -407,18 +475,42 @@ packages:
cpu: [x64]
os: [win32]
'@shikijs/engine-oniguruma@3.23.0':
resolution: {integrity: sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g==}
'@shikijs/langs@3.23.0':
resolution: {integrity: sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg==}
'@shikijs/themes@3.23.0':
resolution: {integrity: sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA==}
'@shikijs/types@3.23.0':
resolution: {integrity: sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ==}
'@shikijs/vscode-textmate@10.0.2':
resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==}
'@types/esrecurse@4.3.1':
resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==}
'@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
'@types/hast@3.0.4':
resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
'@types/istanbul-lib-coverage@2.0.6':
resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==}
'@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
'@types/node@22.19.11':
resolution: {integrity: sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==}
'@types/unist@3.0.3':
resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
'@typescript-eslint/eslint-plugin@8.56.1':
resolution: {integrity: sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@ -491,9 +583,20 @@ packages:
ajv@6.14.0:
resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==}
ansi-regex@5.0.1:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
engines: {node: '>=8'}
ansi-styles@4.3.0:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
engines: {node: '>=8'}
any-promise@1.3.0:
resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==}
argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
balanced-match@4.0.4:
resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
engines: {node: 18 || 20 || >=22}
@ -502,12 +605,26 @@ packages:
resolution: {integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==}
engines: {node: 18 || 20 || >=22}
brace-expansion@5.0.6:
resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==}
engines: {node: 18 || 20 || >=22}
bundle-require@5.1.0:
resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
peerDependencies:
esbuild: '>=0.18'
c8@11.0.0:
resolution: {integrity: sha512-e/uRViGHSVIJv7zsaDKM7VRn2390TgHXqUSvYwPHBQaU6L7E9L0n9JbdkwdYPvshDT0KymBmmlwSpms3yBaMNg==}
engines: {node: 20 || >=22}
hasBin: true
peerDependencies:
monocart-coverage-reports: ^2
peerDependenciesMeta:
monocart-coverage-reports:
optional: true
cac@6.7.14:
resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
engines: {node: '>=8'}
@ -516,6 +633,17 @@ packages:
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
engines: {node: '>= 14.16.0'}
cliui@8.0.1:
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
engines: {node: '>=12'}
color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
commander@4.1.1:
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
engines: {node: '>= 6'}
@ -527,6 +655,9 @@ packages:
resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==}
engines: {node: ^14.18.0 || >=16.10.0}
convert-source-map@2.0.0:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
@ -543,11 +674,22 @@ packages:
deep-is@0.1.4:
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
entities@4.5.0:
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
engines: {node: '>=0.12'}
esbuild@0.27.3:
resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==}
engines: {node: '>=18'}
hasBin: true
escalade@3.2.0:
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
engines: {node: '>=6'}
escape-string-regexp@4.0.0:
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
engines: {node: '>=10'}
@ -636,19 +778,38 @@ packages:
flatted@3.3.4:
resolution: {integrity: sha512-3+mMldrTAPdta5kjX2G2J7iX4zxtnwpdA8Tr2ZSjkyPSanvbZAcy6flmtnXbEybHrDcU9641lxrMfFuUxVz9vA==}
foreground-child@3.3.1:
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
engines: {node: '>=14'}
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
get-caller-file@2.0.5:
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
engines: {node: 6.* || 8.* || >= 10.*}
glob-parent@6.0.2:
resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
engines: {node: '>=10.13.0'}
hijri-core@1.0.0:
resolution: {integrity: sha512-wImBZLBKbEWEEUE1nrc1CFY/uvx4XjGNWYChImJZlswXIVhrBCzSVaj6DP1AU2gUMJ6KDh2ygXo/u/Qx232CXA==}
glob@13.0.6:
resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==}
engines: {node: 18 || 20 || >=22}
has-flag@4.0.0:
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
engines: {node: '>=8'}
hijri-core@1.0.3:
resolution: {integrity: sha512-ONT5gp+Z/lzT/BbWKS0F8lcKK9T/H6jc95NLQE4Angdt7Uimo4KkmSt/qn9odaQRp1pX0RjA65QRcR4miF7XxA==}
engines: {node: '>=20'}
html-escaper@2.0.2:
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
ignore@5.3.2:
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
engines: {node: '>= 4'}
@ -665,6 +826,10 @@ packages:
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
engines: {node: '>=0.10.0'}
is-fullwidth-code-point@3.0.0:
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
engines: {node: '>=8'}
is-glob@4.0.3:
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
engines: {node: '>=0.10.0'}
@ -672,6 +837,18 @@ packages:
isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
istanbul-lib-coverage@3.2.2:
resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==}
engines: {node: '>=8'}
istanbul-lib-report@3.0.1:
resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==}
engines: {node: '>=10'}
istanbul-reports@3.2.0:
resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==}
engines: {node: '>=8'}
joycon@3.1.1:
resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==}
engines: {node: '>=10'}
@ -702,6 +879,9 @@ packages:
lines-and-columns@1.2.4:
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
linkify-it@5.0.1:
resolution: {integrity: sha512-wVoTjP4Q6R0NW5hiZkVJaFZPWgtXfoGF+6LucL3/FtiNjmcHhYjEr5f1Kqjirc1nBW07J/ZuRFumqr2oqccEWg==}
load-tsconfig@0.2.5:
resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
@ -710,13 +890,39 @@ packages:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
engines: {node: '>=10'}
lru-cache@11.5.1:
resolution: {integrity: sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==}
engines: {node: 20 || >=22}
lunr@2.3.9:
resolution: {integrity: sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==}
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
make-dir@4.0.0:
resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==}
engines: {node: '>=10'}
markdown-it@14.2.0:
resolution: {integrity: sha512-1TGiQiJVRQ3NPmZH6sx5Cfnmg6GQm9jvC1ch4TK511NjSJvjzKLzn5pPfZRNZkRPZP0HqCioSndqH8v2nRaWVQ==}
hasBin: true
mdurl@2.0.0:
resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==}
minimatch@10.2.4:
resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==}
engines: {node: 18 || 20 || >=22}
minimatch@10.2.5:
resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==}
engines: {node: 18 || 20 || >=22}
minipass@7.1.3:
resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==}
engines: {node: '>=16 || 14 >=14.17'}
mlly@1.8.0:
resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==}
@ -753,6 +959,10 @@ packages:
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
engines: {node: '>=8'}
path-scurry@2.0.2:
resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==}
engines: {node: 18 || 20 || >=22}
pathe@2.0.3:
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
@ -797,6 +1007,10 @@ packages:
engines: {node: '>=14'}
hasBin: true
punycode.js@2.3.1:
resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==}
engines: {node: '>=6'}
punycode@2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
@ -805,6 +1019,10 @@ packages:
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
engines: {node: '>= 14.18.0'}
require-directory@2.1.1:
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
engines: {node: '>=0.10.0'}
resolve-from@5.0.0:
resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==}
engines: {node: '>=8'}
@ -827,15 +1045,35 @@ packages:
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
engines: {node: '>=8'}
signal-exit@4.1.0:
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
engines: {node: '>=14'}
source-map@0.7.6:
resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==}
engines: {node: '>= 12'}
string-width@4.2.3:
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
engines: {node: '>=8'}
strip-ansi@6.0.1:
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
engines: {node: '>=8'}
sucrase@3.35.1:
resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==}
engines: {node: '>=16 || 14 >=14.17'}
hasBin: true
supports-color@7.2.0:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
engines: {node: '>=8'}
test-exclude@8.0.0:
resolution: {integrity: sha512-ZOffsNrXYggvU1mDGHk54I96r26P8SyMjO5slMKSc7+IWmtB/MQKnEC2fP51imB3/pT6YK5cT5E8f+Dd9KdyOQ==}
engines: {node: 20 || >=22}
thenify-all@1.6.0:
resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
engines: {node: '>=0.8'}
@ -889,6 +1127,19 @@ packages:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'}
typedoc-plugin-markdown@4.11.0:
resolution: {integrity: sha512-2iunh2ALyfyh204OF7h2u0kuQ84xB3jFZtFyUr01nThJkLvR8oGGSSDlyt2gyO4kXhvUxDcVbO0y43+qX+wFbw==}
engines: {node: '>= 18'}
peerDependencies:
typedoc: 0.28.x
typedoc@0.28.19:
resolution: {integrity: sha512-wKh+lhdmMFivMlc6vRRcMGXeGEHGU2g8a2CkPTJjJlwRf1iXbimWIPcFolCqe4E0d/FRtGszpIrsp3WLpDB8Pw==}
engines: {node: '>= 18', pnpm: '>= 10'}
hasBin: true
peerDependencies:
typescript: 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x || 5.9.x || 6.0.x
typescript-eslint@8.56.1:
resolution: {integrity: sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@ -901,6 +1152,9 @@ packages:
engines: {node: '>=14.17'}
hasBin: true
uc.micro@2.1.0:
resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==}
ufo@1.6.3:
resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==}
@ -910,6 +1164,10 @@ packages:
uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
v8-to-istanbul@9.3.0:
resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==}
engines: {node: '>=10.12.0'}
which@2.0.2:
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
engines: {node: '>= 8'}
@ -919,12 +1177,50 @@ packages:
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
engines: {node: '>=0.10.0'}
wrap-ansi@7.0.0:
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
engines: {node: '>=10'}
y18n@5.0.8:
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
engines: {node: '>=10'}
yaml@2.9.0:
resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==}
engines: {node: '>= 14.6'}
hasBin: true
yargs-parser@21.1.1:
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
engines: {node: '>=12'}
yargs@17.7.2:
resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
engines: {node: '>=12'}
yocto-queue@0.1.0:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
snapshots:
'@acamarata/eslint-config@0.1.0(@typescript-eslint/eslint-plugin@8.56.1(@typescript-eslint/parser@8.56.1(eslint@10.0.3)(typescript@5.9.3))(eslint@10.0.3)(typescript@5.9.3))(@typescript-eslint/parser@8.56.1(eslint@10.0.3)(typescript@5.9.3))(eslint-config-prettier@10.1.8(eslint@10.0.3))(eslint@10.0.3)':
dependencies:
'@typescript-eslint/eslint-plugin': 8.56.1(@typescript-eslint/parser@8.56.1(eslint@10.0.3)(typescript@5.9.3))(eslint@10.0.3)(typescript@5.9.3)
'@typescript-eslint/parser': 8.56.1(eslint@10.0.3)(typescript@5.9.3)
eslint: 10.0.3
eslint-config-prettier: 10.1.8(eslint@10.0.3)
'@acamarata/prettier-config@0.1.0(prettier@3.8.1)':
dependencies:
prettier: 3.8.1
'@acamarata/telemetry@0.1.0': {}
'@acamarata/tsconfig@0.1.0': {}
'@bcoe/v8-coverage@1.0.2': {}
'@esbuild/aix-ppc64@0.27.3':
optional: true
@ -1037,6 +1333,14 @@ snapshots:
'@eslint/core': 1.1.1
levn: 0.4.1
'@gerrit0/mini-shiki@3.23.0':
dependencies:
'@shikijs/engine-oniguruma': 3.23.0
'@shikijs/langs': 3.23.0
'@shikijs/themes': 3.23.0
'@shikijs/types': 3.23.0
'@shikijs/vscode-textmate': 10.0.2
'@humanfs/core@0.19.1': {}
'@humanfs/node@0.16.7':
@ -1048,6 +1352,8 @@ snapshots:
'@humanwhocodes/retry@0.4.3': {}
'@istanbuljs/schema@0.1.6': {}
'@jridgewell/gen-mapping@0.3.13':
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
@ -1142,16 +1448,44 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.59.0':
optional: true
'@shikijs/engine-oniguruma@3.23.0':
dependencies:
'@shikijs/types': 3.23.0
'@shikijs/vscode-textmate': 10.0.2
'@shikijs/langs@3.23.0':
dependencies:
'@shikijs/types': 3.23.0
'@shikijs/themes@3.23.0':
dependencies:
'@shikijs/types': 3.23.0
'@shikijs/types@3.23.0':
dependencies:
'@shikijs/vscode-textmate': 10.0.2
'@types/hast': 3.0.4
'@shikijs/vscode-textmate@10.0.2': {}
'@types/esrecurse@4.3.1': {}
'@types/estree@1.0.8': {}
'@types/hast@3.0.4':
dependencies:
'@types/unist': 3.0.3
'@types/istanbul-lib-coverage@2.0.6': {}
'@types/json-schema@7.0.15': {}
'@types/node@22.19.11':
dependencies:
undici-types: 6.21.0
'@types/unist@3.0.3': {}
'@typescript-eslint/eslint-plugin@8.56.1(@typescript-eslint/parser@8.56.1(eslint@10.0.3)(typescript@5.9.3))(eslint@10.0.3)(typescript@5.9.3)':
dependencies:
'@eslint-community/regexpp': 4.12.2
@ -1256,31 +1590,71 @@ snapshots:
json-schema-traverse: 0.4.1
uri-js: 4.4.1
ansi-regex@5.0.1: {}
ansi-styles@4.3.0:
dependencies:
color-convert: 2.0.1
any-promise@1.3.0: {}
argparse@2.0.1: {}
balanced-match@4.0.4: {}
brace-expansion@5.0.4:
dependencies:
balanced-match: 4.0.4
brace-expansion@5.0.6:
dependencies:
balanced-match: 4.0.4
bundle-require@5.1.0(esbuild@0.27.3):
dependencies:
esbuild: 0.27.3
load-tsconfig: 0.2.5
c8@11.0.0:
dependencies:
'@bcoe/v8-coverage': 1.0.2
'@istanbuljs/schema': 0.1.6
find-up: 5.0.0
foreground-child: 3.3.1
istanbul-lib-coverage: 3.2.2
istanbul-lib-report: 3.0.1
istanbul-reports: 3.2.0
test-exclude: 8.0.0
v8-to-istanbul: 9.3.0
yargs: 17.7.2
yargs-parser: 21.1.1
cac@6.7.14: {}
chokidar@4.0.3:
dependencies:
readdirp: 4.1.2
cliui@8.0.1:
dependencies:
string-width: 4.2.3
strip-ansi: 6.0.1
wrap-ansi: 7.0.0
color-convert@2.0.1:
dependencies:
color-name: 1.1.4
color-name@1.1.4: {}
commander@4.1.1: {}
confbox@0.1.8: {}
consola@3.4.2: {}
convert-source-map@2.0.0: {}
cross-spawn@7.0.6:
dependencies:
path-key: 3.1.1
@ -1293,6 +1667,10 @@ snapshots:
deep-is@0.1.4: {}
emoji-regex@8.0.0: {}
entities@4.5.0: {}
esbuild@0.27.3:
optionalDependencies:
'@esbuild/aix-ppc64': 0.27.3
@ -1322,6 +1700,8 @@ snapshots:
'@esbuild/win32-ia32': 0.27.3
'@esbuild/win32-x64': 0.27.3
escalade@3.2.0: {}
escape-string-regexp@4.0.0: {}
eslint-config-prettier@10.1.8(eslint@10.0.3):
@ -1424,14 +1804,31 @@ snapshots:
flatted@3.3.4: {}
foreground-child@3.3.1:
dependencies:
cross-spawn: 7.0.6
signal-exit: 4.1.0
fsevents@2.3.3:
optional: true
get-caller-file@2.0.5: {}
glob-parent@6.0.2:
dependencies:
is-glob: 4.0.3
hijri-core@1.0.0: {}
glob@13.0.6:
dependencies:
minimatch: 10.2.4
minipass: 7.1.3
path-scurry: 2.0.2
has-flag@4.0.0: {}
hijri-core@1.0.3: {}
html-escaper@2.0.2: {}
ignore@5.3.2: {}
@ -1441,12 +1838,27 @@ snapshots:
is-extglob@2.1.1: {}
is-fullwidth-code-point@3.0.0: {}
is-glob@4.0.3:
dependencies:
is-extglob: 2.1.1
isexe@2.0.0: {}
istanbul-lib-coverage@3.2.2: {}
istanbul-lib-report@3.0.1:
dependencies:
istanbul-lib-coverage: 3.2.2
make-dir: 4.0.0
supports-color: 7.2.0
istanbul-reports@3.2.0:
dependencies:
html-escaper: 2.0.2
istanbul-lib-report: 3.0.1
joycon@3.1.1: {}
jsbi@4.3.2: {}
@ -1470,20 +1882,49 @@ snapshots:
lines-and-columns@1.2.4: {}
linkify-it@5.0.1:
dependencies:
uc.micro: 2.1.0
load-tsconfig@0.2.5: {}
locate-path@6.0.0:
dependencies:
p-locate: 5.0.0
lru-cache@11.5.1: {}
lunr@2.3.9: {}
magic-string@0.30.21:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
make-dir@4.0.0:
dependencies:
semver: 7.7.4
markdown-it@14.2.0:
dependencies:
argparse: 2.0.1
entities: 4.5.0
linkify-it: 5.0.1
mdurl: 2.0.0
punycode.js: 2.3.1
uc.micro: 2.1.0
mdurl@2.0.0: {}
minimatch@10.2.4:
dependencies:
brace-expansion: 5.0.4
minimatch@10.2.5:
dependencies:
brace-expansion: 5.0.6
minipass@7.1.3: {}
mlly@1.8.0:
dependencies:
acorn: 8.16.0
@ -1524,6 +1965,11 @@ snapshots:
path-key@3.1.1: {}
path-scurry@2.0.2:
dependencies:
lru-cache: 11.5.1
minipass: 7.1.3
pathe@2.0.3: {}
picocolors@1.1.1: {}
@ -1538,18 +1984,24 @@ snapshots:
mlly: 1.8.0
pathe: 2.0.3
postcss-load-config@6.0.1:
postcss-load-config@6.0.1(yaml@2.9.0):
dependencies:
lilconfig: 3.1.3
optionalDependencies:
yaml: 2.9.0
prelude-ls@1.2.1: {}
prettier@3.8.1: {}
punycode.js@2.3.1: {}
punycode@2.3.1: {}
readdirp@4.1.2: {}
require-directory@2.1.1: {}
resolve-from@5.0.0: {}
rollup@4.59.0:
@ -1591,8 +2043,20 @@ snapshots:
shebang-regex@3.0.0: {}
signal-exit@4.1.0: {}
source-map@0.7.6: {}
string-width@4.2.3:
dependencies:
emoji-regex: 8.0.0
is-fullwidth-code-point: 3.0.0
strip-ansi: 6.0.1
strip-ansi@6.0.1:
dependencies:
ansi-regex: 5.0.1
sucrase@3.35.1:
dependencies:
'@jridgewell/gen-mapping': 0.3.13
@ -1603,6 +2067,16 @@ snapshots:
tinyglobby: 0.2.15
ts-interface-checker: 0.1.13
supports-color@7.2.0:
dependencies:
has-flag: 4.0.0
test-exclude@8.0.0:
dependencies:
'@istanbuljs/schema': 0.1.6
glob: 13.0.6
minimatch: 10.2.4
thenify-all@1.6.0:
dependencies:
thenify: 3.3.1
@ -1628,7 +2102,7 @@ snapshots:
tslib@2.8.1: {}
tsup@8.5.1(typescript@5.9.3):
tsup@8.5.1(typescript@5.9.3)(yaml@2.9.0):
dependencies:
bundle-require: 5.1.0(esbuild@0.27.3)
cac: 6.7.14
@ -1639,7 +2113,7 @@ snapshots:
fix-dts-default-cjs-exports: 1.0.1
joycon: 3.1.1
picocolors: 1.1.1
postcss-load-config: 6.0.1
postcss-load-config: 6.0.1(yaml@2.9.0)
resolve-from: 5.0.0
rollup: 4.59.0
source-map: 0.7.6
@ -1659,6 +2133,19 @@ snapshots:
dependencies:
prelude-ls: 1.2.1
typedoc-plugin-markdown@4.11.0(typedoc@0.28.19(typescript@5.9.3)):
dependencies:
typedoc: 0.28.19(typescript@5.9.3)
typedoc@0.28.19(typescript@5.9.3):
dependencies:
'@gerrit0/mini-shiki': 3.23.0
lunr: 2.3.9
markdown-it: 14.2.0
minimatch: 10.2.5
typescript: 5.9.3
yaml: 2.9.0
typescript-eslint@8.56.1(eslint@10.0.3)(typescript@5.9.3):
dependencies:
'@typescript-eslint/eslint-plugin': 8.56.1(@typescript-eslint/parser@8.56.1(eslint@10.0.3)(typescript@5.9.3))(eslint@10.0.3)(typescript@5.9.3)
@ -1672,6 +2159,8 @@ snapshots:
typescript@5.9.3: {}
uc.micro@2.1.0: {}
ufo@1.6.3: {}
undici-types@6.21.0: {}
@ -1680,10 +2169,38 @@ snapshots:
dependencies:
punycode: 2.3.1
v8-to-istanbul@9.3.0:
dependencies:
'@jridgewell/trace-mapping': 0.3.31
'@types/istanbul-lib-coverage': 2.0.6
convert-source-map: 2.0.0
which@2.0.2:
dependencies:
isexe: 2.0.0
word-wrap@1.2.5: {}
wrap-ansi@7.0.0:
dependencies:
ansi-styles: 4.3.0
string-width: 4.2.3
strip-ansi: 6.0.1
y18n@5.0.8: {}
yaml@2.9.0: {}
yargs-parser@21.1.1: {}
yargs@17.7.2:
dependencies:
cliui: 8.0.1
escalade: 3.2.0
get-caller-file: 2.0.5
require-directory: 2.1.1
string-width: 4.2.3
y18n: 5.0.8
yargs-parser: 21.1.1
yocto-queue@0.1.0: {}

View file

@ -1,5 +1,5 @@
import { getCalendar } from 'hijri-core';
import { HijriCalendar } from './HijriCalendar';
import { getCalendar } from "hijri-core";
import { HijriCalendar } from "./HijriCalendar";
/**
* Temporal calendar implementation for the FCNA/ISNA calendar.
@ -17,6 +17,6 @@ import { HijriCalendar } from './HijriCalendar';
*/
export class FcnaCalendar extends HijriCalendar {
constructor() {
super(getCalendar('fcna'));
super(getCalendar("fcna"));
}
}

View file

@ -1,7 +1,7 @@
import { Temporal } from '@js-temporal/polyfill';
import type { CalendarEngine } from 'hijri-core';
import { Temporal } from "@js-temporal/polyfill";
import type { CalendarEngine } from "hijri-core";
type DateUnit = 'year' | 'years' | 'month' | 'months' | 'week' | 'weeks' | 'day' | 'days';
type DateUnit = "year" | "years" | "month" | "months" | "week" | "weeks" | "day" | "days";
/** Reference year for monthDay construction when no year is specified. */
const REFERENCE_YEAR = 1444;
@ -70,14 +70,14 @@ export class HijriCalendar {
/**
* Convert a Temporal.PlainDate (ISO calendar) to Hijri coordinates.
*
* Uses the local-time Date constructor so that the date components passed to
* the engine match the calendar date exactly, regardless of host timezone.
* The UAQ engine reads local components; the FCNA engine reads UTC components.
* Because we construct with new Date(y, m, d) the local date always matches
* the intended calendar date.
* PlainDate calendar fields are placed in the Date's UTC components via
* Date.UTC() because hijri-core reads the UTC calendar day. This ensures
* the conversion returns the correct Hijri date on every host timezone:
* without Date.UTC, on east-of-UTC hosts (e.g. UTC+5) the local midnight
* falls on the previous UTC day, causing a one-day-off result.
*/
protected toHijri(date: Temporal.PlainDate): { hy: number; hm: number; hd: number } {
const jsDate = new Date(date.year, date.month - 1, date.day);
const jsDate = new Date(Date.UTC(date.year, date.month - 1, date.day));
const hijri = this.engine.toHijri(jsDate);
if (!hijri) {
throw new RangeError(`Date ${date.toString()} is out of range for the ${this.id} calendar`);
@ -109,8 +109,8 @@ export class HijriCalendar {
* Resolve the overflow option from a Temporal options bag.
* Returns 'constrain' (the default) or 'reject'.
*/
private resolveOverflow(options?: { overflow?: 'constrain' | 'reject' }): 'constrain' | 'reject' {
return options?.overflow ?? 'constrain';
private resolveOverflow(options?: { overflow?: "constrain" | "reject" }): "constrain" | "reject" {
return options?.overflow ?? "constrain";
}
/**
@ -120,9 +120,9 @@ export class HijriCalendar {
day: number,
maxDay: number,
month: number,
overflow: 'constrain' | 'reject',
overflow: "constrain" | "reject",
): number {
if (overflow === 'reject' && day > maxDay) {
if (overflow === "reject" && day > maxDay) {
throw new RangeError(`Day ${day} exceeds ${maxDay} days in month ${month}`);
}
return Math.min(day, maxDay);
@ -130,10 +130,22 @@ export class HijriCalendar {
// ── Field accessors ───────────────────────────────────────────────────────
/**
* Returns the Hijri year for the given ISO date.
*
* @param date - A Temporal.PlainDate with ISO (Gregorian) coordinates.
* @returns The Hijri year, e.g. 1444.
*/
year(date: Temporal.PlainDate): number {
return this.toHijri(date).hy;
}
/**
* Returns the Hijri month (1-12) for the given ISO date.
*
* @param date - A Temporal.PlainDate with ISO (Gregorian) coordinates.
* @returns Month number 1 (Muharram) through 12 (Dhul-Hijja).
*/
month(date: Temporal.PlainDate): number {
return this.toHijri(date).hm;
}
@ -145,15 +157,30 @@ export class HijriCalendar {
*/
monthCode(date: Temporal.PlainDate): string {
const { hm } = this.toHijri(date);
return `M${String(hm).padStart(2, '0')}`;
return `M${String(hm).padStart(2, "0")}`;
}
/**
* Returns the day of the Hijri month (1-29 or 1-30).
*
* @param date - A Temporal.PlainDate with ISO (Gregorian) coordinates.
* @returns Day of month within the Hijri calendar.
*/
day(date: Temporal.PlainDate): number {
return this.toHijri(date).hd;
}
// ── Month and year metrics ─────────────────────────────────────────────────
/**
* Returns the number of days in the Hijri month containing the given date.
*
* Hijri months alternate between 29 and 30 days, but the exact pattern
* differs by calendar system (UAQ uses fixed tables; FCNA uses calculation).
*
* @param date - A Temporal.PlainDate with ISO (Gregorian) coordinates.
* @returns 29 or 30.
*/
daysInMonth(date: Temporal.PlainDate): number {
const { hy, hm } = this.toHijri(date);
return this.engine.daysInMonth(hy, hm);
@ -172,10 +199,27 @@ export class HijriCalendar {
return total;
}
/**
* Returns the number of months in the Hijri year.
*
* Always 12. Unlike the Hebrew calendar, the Hijri lunar calendar has no
* intercalary (leap) month only a possible extra day in Dhul-Hijja.
*
* @returns Always 12.
*/
monthsInYear(_date: Temporal.PlainDate): number {
return 12;
}
/**
* Returns whether the Hijri year is a leap year (355 days).
*
* Standard Hijri years have 354 days. A leap year adds one day to
* Dhul-Hijja (month 12), making it 355 days total.
*
* @param date - A Temporal.PlainDate with ISO (Gregorian) coordinates.
* @returns `true` if the year has 355 days.
*/
inLeapYear(date: Temporal.PlainDate): boolean {
return this.daysInYear(date) === 355;
}
@ -209,6 +253,13 @@ export class HijriCalendar {
return Math.ceil(this.dayOfYear(date) / 7);
}
/**
* Returns the number of days in a week.
*
* Always 7. Required by the Temporal Calendar Protocol.
*
* @returns Always 7.
*/
daysInWeek(_date: Temporal.PlainDate): number {
return 7;
}
@ -227,7 +278,7 @@ export class HijriCalendar {
dateFromFields(
fields: { year: number; month: number; day: number },
options?: { overflow?: 'constrain' | 'reject' },
options?: { overflow?: "constrain" | "reject" },
): Temporal.PlainDate {
const overflow = this.resolveOverflow(options);
const maxDay = this.engine.daysInMonth(fields.year, fields.month);
@ -242,12 +293,12 @@ export class HijriCalendar {
*/
yearMonthFromFields(
fields: { year: number; month: number },
options?: { overflow?: 'constrain' | 'reject' },
options?: { overflow?: "constrain" | "reject" },
): Temporal.PlainYearMonth {
const overflow = this.resolveOverflow(options);
// Clamp month to 1-12 or reject.
const maxMonth = 12;
if (overflow === 'reject' && (fields.month < 1 || fields.month > maxMonth)) {
if (overflow === "reject" && (fields.month < 1 || fields.month > maxMonth)) {
throw new RangeError(`Month ${fields.month} is out of range 1-${maxMonth}`);
}
const month = Math.max(1, Math.min(fields.month, maxMonth));
@ -265,7 +316,7 @@ export class HijriCalendar {
*/
monthDayFromFields(
fields: { month: number; day: number; year?: number },
options?: { overflow?: 'constrain' | 'reject' },
options?: { overflow?: "constrain" | "reject" },
): Temporal.PlainMonthDay {
const overflow = this.resolveOverflow(options);
const year = fields.year ?? REFERENCE_YEAR;
@ -295,7 +346,7 @@ export class HijriCalendar {
dateAdd(
date: Temporal.PlainDate,
duration: Temporal.Duration,
_options?: { overflow?: 'constrain' | 'reject' },
_options?: { overflow?: "constrain" | "reject" },
): Temporal.PlainDate {
const { hy, hm, hd } = this.toHijri(date);
@ -335,16 +386,16 @@ export class HijriCalendar {
two: Temporal.PlainDate,
options?: { largestUnit?: DateUnit },
): Temporal.Duration {
const largestUnit: DateUnit = options?.largestUnit ?? 'days';
const largestUnit: DateUnit = options?.largestUnit ?? "days";
if (largestUnit === 'years' || largestUnit === 'year') {
if (largestUnit === "years" || largestUnit === "year") {
const h1 = this.toHijri(one);
const h2 = this.toHijri(two);
const diff = borrowHijriDiff(this.engine, h2.hy - h1.hy, h2.hm - h1.hm, h2.hd - h1.hd, h2);
return new Temporal.Duration(diff.years, diff.months, 0, diff.days);
}
if (largestUnit === 'months' || largestUnit === 'month') {
if (largestUnit === "months" || largestUnit === "month") {
const h1 = this.toHijri(one);
const h2 = this.toHijri(two);
const diff = borrowHijriDiff(this.engine, h2.hy - h1.hy, h2.hm - h1.hm, h2.hd - h1.hd, h2);
@ -353,11 +404,11 @@ export class HijriCalendar {
}
// For weeks and days, delegate to ISO arithmetic which is exact.
if (largestUnit === 'weeks' || largestUnit === 'week') {
return one.until(two, { largestUnit: 'weeks' });
if (largestUnit === "weeks" || largestUnit === "week") {
return one.until(two, { largestUnit: "weeks" });
}
return one.until(two, { largestUnit: 'days' });
return one.until(two, { largestUnit: "days" });
}
mergeFields(

View file

@ -1,5 +1,5 @@
import { getCalendar } from 'hijri-core';
import { HijriCalendar } from './HijriCalendar';
import { getCalendar } from "hijri-core";
import { HijriCalendar } from "./HijriCalendar";
/**
* Temporal calendar implementation for the Umm al-Qura calendar.
@ -15,6 +15,6 @@ import { HijriCalendar } from './HijriCalendar';
*/
export class UaqCalendar extends HijriCalendar {
constructor() {
super(getCalendar('uaq'));
super(getCalendar("uaq"));
}
}

View file

@ -1,12 +1,21 @@
export { HijriCalendar } from './calendars/HijriCalendar';
export { UaqCalendar } from './calendars/UaqCalendar';
export { FcnaCalendar } from './calendars/FcnaCalendar';
export { HijriCalendar } from "./calendars/HijriCalendar";
export { UaqCalendar } from "./calendars/UaqCalendar";
export { FcnaCalendar } from "./calendars/FcnaCalendar";
export type { HijriDate, CalendarEngine, ConversionOptions } from 'hijri-core';
export type { HijriDate, CalendarEngine, ConversionOptions } from "hijri-core";
// Pre-built singletons. Import and use directly; no need to instantiate.
import { UaqCalendar } from './calendars/UaqCalendar';
import { FcnaCalendar } from './calendars/FcnaCalendar';
import { UaqCalendar } from "./calendars/UaqCalendar";
import { FcnaCalendar } from "./calendars/FcnaCalendar";
export const uaqCalendar = new UaqCalendar();
export const fcnaCalendar = new FcnaCalendar();
// ── Opt-in anonymous telemetry ────────────────────────────────────────────────
// Off by default. Enable: ACAMARATA_TELEMETRY=1
// What is sent + how to disable: https://github.com/acamarata/telemetry/blob/main/TELEMETRY.md
import("@acamarata/telemetry")
.then(({ track }) => track("load", { package: "temporal-hijri", version: "1.0.3" }))
.catch(() => {
// telemetry not installed or disabled — that is fine
});

View file

@ -1 +1 @@
export type { HijriDate, CalendarEngine, ConversionOptions } from 'hijri-core';
export type { HijriDate, CalendarEngine, ConversionOptions } from "hijri-core";

View file

@ -1,4 +1,4 @@
'use strict';
"use strict";
/**
* CJS test suite for temporal-hijri.
@ -6,66 +6,66 @@
* Verifies that the CommonJS build loads and functions correctly via require().
*/
const { describe, it } = require('node:test');
const assert = require('node:assert/strict');
const { Temporal } = require('@js-temporal/polyfill');
const { UaqCalendar, FcnaCalendar, uaqCalendar, fcnaCalendar } = require('./dist/index.cjs');
const { describe, it } = require("node:test");
const assert = require("node:assert/strict");
const { Temporal } = require("@js-temporal/polyfill");
const { UaqCalendar, FcnaCalendar, uaqCalendar, fcnaCalendar } = require("./dist/index.cjs");
const isoRamadan = Temporal.PlainDate.from('2023-03-23');
const isoRamadan = Temporal.PlainDate.from("2023-03-23");
// ── Class and singleton exports ───────────────────────────────────────────────
describe('CJS class exports', () => {
it('UaqCalendar class loads via require', () => {
assert(typeof UaqCalendar === 'function', 'UaqCalendar should be a constructor');
describe("CJS class exports", () => {
it("UaqCalendar class loads via require", () => {
assert(typeof UaqCalendar === "function", "UaqCalendar should be a constructor");
const cal = new UaqCalendar();
assert.equal(cal.id, 'hijri-uaq');
assert.equal(cal.id, "hijri-uaq");
});
it('FcnaCalendar class loads via require', () => {
assert(typeof FcnaCalendar === 'function', 'FcnaCalendar should be a constructor');
it("FcnaCalendar class loads via require", () => {
assert(typeof FcnaCalendar === "function", "FcnaCalendar should be a constructor");
const cal = new FcnaCalendar();
assert.equal(cal.id, 'hijri-fcna');
assert.equal(cal.id, "hijri-fcna");
});
it('uaqCalendar singleton id', () => {
assert.equal(uaqCalendar.id, 'hijri-uaq');
it("uaqCalendar singleton id", () => {
assert.equal(uaqCalendar.id, "hijri-uaq");
});
it('fcnaCalendar singleton id', () => {
assert.equal(fcnaCalendar.id, 'hijri-fcna');
it("fcnaCalendar singleton id", () => {
assert.equal(fcnaCalendar.id, "hijri-fcna");
});
});
// ── Field accessors ───────────────────────────────────────────────────────────
describe('CJS field accessors', () => {
it('uaqCalendar.year(2023-03-23) = 1444', () => {
describe("CJS field accessors", () => {
it("uaqCalendar.year(2023-03-23) = 1444", () => {
assert.equal(uaqCalendar.year(isoRamadan), 1444);
});
it('uaqCalendar.month(2023-03-23) = 9', () => {
it("uaqCalendar.month(2023-03-23) = 9", () => {
assert.equal(uaqCalendar.month(isoRamadan), 9);
});
it('uaqCalendar.day(2023-03-23) = 1', () => {
it("uaqCalendar.day(2023-03-23) = 1", () => {
assert.equal(uaqCalendar.day(isoRamadan), 1);
});
});
// ── dateFromFields ─────────────────────────────────────────────────────────────
describe('CJS dateFromFields', () => {
it('uaqCalendar.dateFromFields({year:1444, month:9, day:1}) = 2023-03-23', () => {
describe("CJS dateFromFields", () => {
it("uaqCalendar.dateFromFields({year:1444, month:9, day:1}) = 2023-03-23", () => {
const result = uaqCalendar.dateFromFields({ year: 1444, month: 9, day: 1 });
assert.equal(result.toString(), '2023-03-23');
assert.equal(result.toString(), "2023-03-23");
});
});
// ── fields() ──────────────────────────────────────────────────────────────────
describe('CJS fields()', () => {
it('returns the input array unchanged', () => {
assert.deepEqual(uaqCalendar.fields(['year', 'month', 'day']), ['year', 'month', 'day']);
describe("CJS fields()", () => {
it("returns the input array unchanged", () => {
assert.deepEqual(uaqCalendar.fields(["year", "month", "day"]), ["year", "month", "day"]);
});
});

162
test.mjs
View file

@ -5,127 +5,127 @@
* Reference point: 2023-03-23 = 1 Ramadan 1444 AH (both UAQ and FCNA agree).
*/
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { Temporal } from '@js-temporal/polyfill';
import { UaqCalendar, FcnaCalendar, uaqCalendar, fcnaCalendar } from './dist/index.mjs';
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { Temporal } from "@js-temporal/polyfill";
import { UaqCalendar, FcnaCalendar, uaqCalendar, fcnaCalendar } from "./dist/index.mjs";
// Reference date: 2023-03-23 = 1 Ramadan 1444 AH
const isoRamadan = Temporal.PlainDate.from('2023-03-23');
const isoRamadan = Temporal.PlainDate.from("2023-03-23");
// 2023-04-21 = 1 Shawwal 1444 AH (first day after Ramadan)
const isoShawwal = Temporal.PlainDate.from('2023-04-21');
const isoShawwal = Temporal.PlainDate.from("2023-04-21");
// 2021-08-09 = 1 Muharram 1443 AH (a 355-day / leap year)
const isoLeapYear = Temporal.PlainDate.from('2021-08-09');
const isoLeapYear = Temporal.PlainDate.from("2021-08-09");
// ── 1. Class exports ──────────────────────────────────────────────────────────
describe('Class exports', () => {
it('UaqCalendar class export', () => {
assert(UaqCalendar, 'UaqCalendar should be exported');
describe("Class exports", () => {
it("UaqCalendar class export", () => {
assert(UaqCalendar, "UaqCalendar should be exported");
const cal = new UaqCalendar();
assert(cal instanceof UaqCalendar, 'UaqCalendar should be instantiable');
assert(cal instanceof UaqCalendar, "UaqCalendar should be instantiable");
});
it('FcnaCalendar class export', () => {
assert(FcnaCalendar, 'FcnaCalendar should be exported');
it("FcnaCalendar class export", () => {
assert(FcnaCalendar, "FcnaCalendar should be exported");
const cal = new FcnaCalendar();
assert(cal instanceof FcnaCalendar, 'FcnaCalendar should be instantiable');
assert(cal instanceof FcnaCalendar, "FcnaCalendar should be instantiable");
});
});
// ── 2. Calendar IDs ───────────────────────────────────────────────────────────
describe('Calendar IDs', () => {
it('uaqCalendar.id', () => {
assert.equal(uaqCalendar.id, 'hijri-uaq');
describe("Calendar IDs", () => {
it("uaqCalendar.id", () => {
assert.equal(uaqCalendar.id, "hijri-uaq");
});
it('fcnaCalendar.id', () => {
assert.equal(fcnaCalendar.id, 'hijri-fcna');
it("fcnaCalendar.id", () => {
assert.equal(fcnaCalendar.id, "hijri-fcna");
});
});
// ── 3. Field accessors on 1 Ramadan 1444 (2023-03-23) ────────────────────────
describe('Field accessors (UAQ, 1 Ramadan 1444)', () => {
it('year = 1444', () => {
describe("Field accessors (UAQ, 1 Ramadan 1444)", () => {
it("year = 1444", () => {
assert.equal(uaqCalendar.year(isoRamadan), 1444);
});
it('month = 9 (Ramadan)', () => {
it("month = 9 (Ramadan)", () => {
assert.equal(uaqCalendar.month(isoRamadan), 9);
});
it('day = 1', () => {
it("day = 1", () => {
assert.equal(uaqCalendar.day(isoRamadan), 1);
});
it('monthCode = "M09"', () => {
assert.equal(uaqCalendar.monthCode(isoRamadan), 'M09');
assert.equal(uaqCalendar.monthCode(isoRamadan), "M09");
});
it('daysInMonth = 29 (Ramadan 1444)', () => {
it("daysInMonth = 29 (Ramadan 1444)", () => {
assert.equal(uaqCalendar.daysInMonth(isoRamadan), 29);
});
it('monthsInYear = 12', () => {
it("monthsInYear = 12", () => {
assert.equal(uaqCalendar.monthsInYear(isoRamadan), 12);
});
it('daysInWeek = 7', () => {
it("daysInWeek = 7", () => {
assert.equal(uaqCalendar.daysInWeek(isoRamadan), 7);
});
it('dayOfWeek = 4 (Thursday)', () => {
it("dayOfWeek = 4 (Thursday)", () => {
assert.equal(uaqCalendar.dayOfWeek(isoRamadan), 4);
});
it('dayOfYear = 237', () => {
it("dayOfYear = 237", () => {
assert.equal(uaqCalendar.dayOfYear(isoRamadan), 237);
});
});
// ── 4. dateFromFields ─────────────────────────────────────────────────────────
describe('dateFromFields', () => {
it('dateFromFields({year:1444, month:9, day:1}) = 2023-03-23', () => {
describe("dateFromFields", () => {
it("dateFromFields({year:1444, month:9, day:1}) = 2023-03-23", () => {
const result = uaqCalendar.dateFromFields({ year: 1444, month: 9, day: 1 });
assert.equal(result.toString(), '2023-03-23');
assert.equal(result.toString(), "2023-03-23");
});
});
// ── 5. dateAdd ────────────────────────────────────────────────────────────────
describe('dateAdd', () => {
it('adding 1 month from 1 Ramadan 1444 lands on 1 Shawwal 1444', () => {
describe("dateAdd", () => {
it("adding 1 month from 1 Ramadan 1444 lands on 1 Shawwal 1444", () => {
const oneMonth = new Temporal.Duration(0, 1, 0, 0);
const result = uaqCalendar.dateAdd(isoRamadan, oneMonth);
assert.equal(result.toString(), isoShawwal.toString());
assert.equal(uaqCalendar.month(result), 10);
});
it('adding 7 days from 1 Ramadan 1444', () => {
it("adding 7 days from 1 Ramadan 1444", () => {
const sevenDays = new Temporal.Duration(0, 0, 0, 7);
const result = uaqCalendar.dateAdd(isoRamadan, sevenDays);
assert.equal(uaqCalendar.day(result), 8);
assert.equal(uaqCalendar.month(result), 9);
});
it('adding 1 week from 1 Ramadan 1444', () => {
it("adding 1 week from 1 Ramadan 1444", () => {
const oneWeek = new Temporal.Duration(0, 0, 1, 0);
const result = uaqCalendar.dateAdd(isoRamadan, oneWeek);
assert.equal(uaqCalendar.day(result), 8);
assert.equal(uaqCalendar.month(result), 9);
});
it('adding 12 months rolls the year forward', () => {
it("adding 12 months rolls the year forward", () => {
const twelveMonths = new Temporal.Duration(0, 12, 0, 0);
const result = uaqCalendar.dateAdd(isoRamadan, twelveMonths);
assert.equal(uaqCalendar.year(result), 1445);
assert.equal(uaqCalendar.month(result), 9);
});
it('subtracting months via negative duration', () => {
it("subtracting months via negative duration", () => {
const negMonth = new Temporal.Duration(0, -1, 0, 0);
const result = uaqCalendar.dateAdd(isoShawwal, negMonth);
assert.equal(uaqCalendar.month(result), 9);
@ -135,29 +135,29 @@ describe('dateAdd', () => {
// ── 6. dateUntil ──────────────────────────────────────────────────────────────
describe('dateUntil', () => {
it('days between 1 Ramadan and 1 Shawwal 1444', () => {
const dur = uaqCalendar.dateUntil(isoRamadan, isoShawwal, { largestUnit: 'days' });
describe("dateUntil", () => {
it("days between 1 Ramadan and 1 Shawwal 1444", () => {
const dur = uaqCalendar.dateUntil(isoRamadan, isoShawwal, { largestUnit: "days" });
assert.equal(dur.days, 29);
});
it('months between 1 Ramadan and 1 Shawwal 1444', () => {
const dur = uaqCalendar.dateUntil(isoRamadan, isoShawwal, { largestUnit: 'months' });
it("months between 1 Ramadan and 1 Shawwal 1444", () => {
const dur = uaqCalendar.dateUntil(isoRamadan, isoShawwal, { largestUnit: "months" });
assert.equal(dur.months, 1);
assert.equal(dur.days, 0);
});
it('years between dates spanning one Hijri year', () => {
it("years between dates spanning one Hijri year", () => {
const iso1443 = uaqCalendar.dateFromFields({ year: 1443, month: 1, day: 1 });
const iso1444 = uaqCalendar.dateFromFields({ year: 1444, month: 1, day: 1 });
const dur = uaqCalendar.dateUntil(iso1443, iso1444, { largestUnit: 'years' });
const dur = uaqCalendar.dateUntil(iso1443, iso1444, { largestUnit: "years" });
assert.equal(dur.years, 1);
assert.equal(dur.months, 0);
assert.equal(dur.days, 0);
});
it('weeks between dates', () => {
const dur = uaqCalendar.dateUntil(isoRamadan, isoShawwal, { largestUnit: 'weeks' });
it("weeks between dates", () => {
const dur = uaqCalendar.dateUntil(isoRamadan, isoShawwal, { largestUnit: "weeks" });
assert.equal(dur.weeks, 4);
assert.equal(dur.days, 1);
});
@ -165,8 +165,8 @@ describe('dateUntil', () => {
// ── 7. inLeapYear ─────────────────────────────────────────────────────────────
describe('inLeapYear', () => {
it('1443 AH (355 days) is a leap year, 1444 AH (354) is not', () => {
describe("inLeapYear", () => {
it("1443 AH (355 days) is a leap year, 1444 AH (354) is not", () => {
assert.equal(uaqCalendar.inLeapYear(isoLeapYear), true);
assert.equal(uaqCalendar.inLeapYear(isoRamadan), false);
});
@ -174,18 +174,18 @@ describe('inLeapYear', () => {
// ── 8. FCNA calendar ──────────────────────────────────────────────────────────
describe('FCNA calendar', () => {
it('fcnaCalendar.year(2023-03-23) returns a valid Hijri year', () => {
describe("FCNA calendar", () => {
it("fcnaCalendar.year(2023-03-23) returns a valid Hijri year", () => {
const year = fcnaCalendar.year(isoRamadan);
assert(typeof year === 'number' && year > 1400, `Expected a Hijri year > 1400, got ${year}`);
assert(typeof year === "number" && year > 1400, `Expected a Hijri year > 1400, got ${year}`);
});
});
// ── 9. Out-of-range error ─────────────────────────────────────────────────────
describe('Out-of-range error', () => {
it('uaqCalendar.year throws RangeError for out-of-range date (1800-01-01)', () => {
const outOfRange = Temporal.PlainDate.from('1800-01-01');
describe("Out-of-range error", () => {
it("uaqCalendar.year throws RangeError for out-of-range date (1800-01-01)", () => {
const outOfRange = Temporal.PlainDate.from("1800-01-01");
assert.throws(
() => uaqCalendar.year(outOfRange),
(err) => err instanceof RangeError,
@ -195,11 +195,11 @@ describe('Out-of-range error', () => {
// ── 10. overflow option ───────────────────────────────────────────────────────
describe('overflow option', () => {
describe("overflow option", () => {
it('dateFromFields with overflow: "constrain" clamps day', () => {
const result = uaqCalendar.dateFromFields(
{ year: 1444, month: 9, day: 31 },
{ overflow: 'constrain' },
{ overflow: "constrain" },
);
assert.equal(uaqCalendar.day(result), 29);
assert.equal(uaqCalendar.month(result), 9);
@ -207,19 +207,19 @@ describe('overflow option', () => {
it('dateFromFields with overflow: "reject" throws RangeError', () => {
assert.throws(
() => uaqCalendar.dateFromFields({ year: 1444, month: 9, day: 31 }, { overflow: 'reject' }),
() => uaqCalendar.dateFromFields({ year: 1444, month: 9, day: 31 }, { overflow: "reject" }),
(err) => err instanceof RangeError,
);
});
it('monthDayFromFields with overflow: "constrain" clamps day', () => {
const result = uaqCalendar.monthDayFromFields({ month: 9, day: 31 }, { overflow: 'constrain' });
const result = uaqCalendar.monthDayFromFields({ month: 9, day: 31 }, { overflow: "constrain" });
assert.ok(result);
});
it('monthDayFromFields with overflow: "reject" throws RangeError', () => {
assert.throws(
() => uaqCalendar.monthDayFromFields({ month: 9, day: 31 }, { overflow: 'reject' }),
() => uaqCalendar.monthDayFromFields({ month: 9, day: 31 }, { overflow: "reject" }),
(err) => err instanceof RangeError,
);
});
@ -227,22 +227,22 @@ describe('overflow option', () => {
// ── 11. fields() ──────────────────────────────────────────────────────────────
describe('fields()', () => {
it('returns the input array unchanged', () => {
const input = ['year', 'month', 'day'];
describe("fields()", () => {
it("returns the input array unchanged", () => {
const input = ["year", "month", "day"];
const result = uaqCalendar.fields(input);
assert.deepEqual(result, ['year', 'month', 'day']);
assert.deepEqual(result, ["year", "month", "day"]);
});
it('returns an empty array for empty input', () => {
it("returns an empty array for empty input", () => {
assert.deepEqual(uaqCalendar.fields([]), []);
});
});
// ── 12. yearMonthFromFields ─────────────────────────────────────────────────
describe('yearMonthFromFields', () => {
it('creates a PlainYearMonth for Ramadan 1444', () => {
describe("yearMonthFromFields", () => {
it("creates a PlainYearMonth for Ramadan 1444", () => {
const result = uaqCalendar.yearMonthFromFields({ year: 1444, month: 9 });
assert.ok(result);
assert.equal(result.month, 3);
@ -252,14 +252,36 @@ describe('yearMonthFromFields', () => {
// ── 13. monthDayFromFields ──────────────────────────────────────────────────
describe('monthDayFromFields', () => {
it('creates a PlainMonthDay for 15 Ramadan (default reference year)', () => {
describe("monthDayFromFields", () => {
it("creates a PlainMonthDay for 15 Ramadan (default reference year)", () => {
const result = uaqCalendar.monthDayFromFields({ month: 9, day: 15 });
assert.ok(result);
});
it('creates a PlainMonthDay with explicit year', () => {
it("creates a PlainMonthDay with explicit year", () => {
const result = uaqCalendar.monthDayFromFields({ month: 9, day: 1, year: 1445 });
assert.ok(result);
});
});
// ── 14. UTC-day round-trip regression ─────────────────────────────────────────
// Verifies that ISO→Hijri→ISO is exact regardless of host timezone.
// 2025-03-01 = 1 Ramadan 1446 AH (UAQ).
describe("UTC-day round-trip regression (ISO → Hijri → ISO)", () => {
const isoRamadan1446 = Temporal.PlainDate.from("2025-03-01");
it("2025-03-01 maps to 1 Ramadan 1446 AH", () => {
assert.equal(uaqCalendar.year(isoRamadan1446), 1446);
assert.equal(uaqCalendar.month(isoRamadan1446), 9);
assert.equal(uaqCalendar.day(isoRamadan1446), 1);
});
it("round-trip: 1446-09-01 → ISO → Hijri returns 1446-09-01", () => {
const iso = uaqCalendar.dateFromFields({ year: 1446, month: 9, day: 1 });
assert.equal(iso.toString(), "2025-03-01");
assert.equal(uaqCalendar.year(iso), 1446);
assert.equal(uaqCalendar.month(iso), 9);
assert.equal(uaqCalendar.day(iso), 1);
});
});

View file

@ -1,20 +1,11 @@
{
"extends": "@acamarata/tsconfig/tsconfig.library.json",
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"forceConsistentCasingInFileNames": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"esModuleInterop": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "dist",
"rootDir": "src",
"types": ["node"],
"skipLibCheck": true
"types": ["node"]
},
"include": ["src"]
}

View file

@ -1,17 +1,17 @@
import { defineConfig } from 'tsup';
import { defineConfig } from "tsup";
export default defineConfig({
entry: ['src/index.ts'],
format: ['cjs', 'esm'],
entry: ["src/index.ts"],
format: ["cjs", "esm"],
dts: true,
clean: true,
outDir: 'dist',
outDir: "dist",
splitting: false,
sourcemap: true,
target: 'es2020',
platform: 'node',
external: ['hijri-core', '@js-temporal/polyfill'],
target: "es2020",
platform: "node",
external: ["hijri-core", "@js-temporal/polyfill"],
outExtension({ format }) {
return { js: format === 'esm' ? '.mjs' : '.cjs' };
return { js: format === "esm" ? ".mjs" : ".cjs" };
},
});

10
typedoc.json Normal file
View file

@ -0,0 +1,10 @@
{
"entryPoints": ["src/index.ts"],
"out": ".github/wiki/api",
"plugin": ["typedoc-plugin-markdown"],
"readme": "none",
"skipErrorChecking": false,
"excludePrivate": true,
"excludeProtected": true,
"includeVersion": true
}