mirror of
https://github.com/acamarata/hijri-core.git
synced 2026-07-02 19:50:40 +00:00
Compare commits
20 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c762bd694 | ||
|
|
9367a45ac8 | ||
|
|
d9ce016e4b | ||
|
|
fb803e4068 | ||
|
|
34193780f3 | ||
|
|
6caa9eed2c | ||
|
|
680bc72c19 | ||
|
|
db575a3b41 | ||
|
|
e1b761db7c | ||
|
|
27b89b03a8 | ||
|
|
235ffb8851 | ||
|
|
94bc28ebc3 | ||
|
|
f5cb959dcc | ||
|
|
69da87f639 | ||
|
|
26a43b5829 | ||
|
|
4c37382667 | ||
|
|
71116954da | ||
|
|
193bd5527f | ||
|
|
b6ee3b08d0 | ||
|
|
4d231b8aec |
56 changed files with 3249 additions and 1034 deletions
|
|
@ -1,41 +0,0 @@
|
||||||
# hijri-core — PRI (Per-Repo Instructions)
|
|
||||||
|
|
||||||
**PPI:** `~/Sites/acamarata/.claude/CLAUDE.md`
|
|
||||||
|
|
||||||
## What This Is
|
|
||||||
|
|
||||||
Core Hijri calendar engine for JavaScript/TypeScript. Provides Hijri/Gregorian date conversion with a pluggable calendar registry supporting multiple calculation methods.
|
|
||||||
|
|
||||||
**npm:** `hijri-core@1.0.0`
|
|
||||||
**Language:** TypeScript
|
|
||||||
**License:** MIT
|
|
||||||
|
|
||||||
## Key Technical Details
|
|
||||||
|
|
||||||
- Zero runtime dependencies
|
|
||||||
- Pluggable engine architecture: UAQ (Umm al-Qura, 1318-1500 AH) and FCNA engines built in
|
|
||||||
- Foundation package: used by luxon-hijri, date-fns-hijri, dayjs-hijri-plus, moment-hijri-plus, temporal-hijri
|
|
||||||
- Dart counterpart: hijri-core-dart (hijri_core@1.0.0 on pub.dev)
|
|
||||||
|
|
||||||
## API Surface
|
|
||||||
|
|
||||||
- `toHijri(gregorianDate, engine?)` — Gregorian to Hijri
|
|
||||||
- `toGregorian(hijriYear, hijriMonth, hijriDay, engine?)` — Hijri to Gregorian
|
|
||||||
- `registerCalendar(name, engine)` — add custom calendar engine
|
|
||||||
- `getCalendar(name)` — retrieve registered engine
|
|
||||||
- `isValidHijriDate(year, month, day, engine?)` — validation
|
|
||||||
- `daysInHijriMonth(year, month, engine?)` — month length
|
|
||||||
- Month name arrays: long/medium/short (Arabic + English)
|
|
||||||
- Weekday name arrays
|
|
||||||
|
|
||||||
## Important Notes
|
|
||||||
|
|
||||||
- This is a FOUNDATION package. Breaking changes here affect 5 downstream plugin packages.
|
|
||||||
- When making API changes: update all 5 plugin packages (luxon-hijri, date-fns-hijri, dayjs-hijri-plus, moment-hijri-plus, temporal-hijri) and bump their versions too.
|
|
||||||
- Test all 5 plugins against any hijri-core change before publishing.
|
|
||||||
|
|
||||||
## Commands
|
|
||||||
|
|
||||||
- `pnpm test` — run test.mjs + test-cjs.cjs
|
|
||||||
- `pnpm run typecheck` — tsc --noEmit
|
|
||||||
- `pnpm build` — tsup build
|
|
||||||
34
.github/wiki/CODE_OF_CONDUCT.md
vendored
Normal file
34
.github/wiki/CODE_OF_CONDUCT.md
vendored
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
# Code of Conduct
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Be direct, be respectful, and focus on the work.
|
||||||
|
|
||||||
|
## Standards
|
||||||
|
|
||||||
|
Constructive behavior:
|
||||||
|
|
||||||
|
- Technical criticism aimed at code and ideas, not people
|
||||||
|
- Clear and specific feedback with examples where possible
|
||||||
|
- Acknowledging when you are wrong or do not know something
|
||||||
|
- Staying on topic in issues and pull requests
|
||||||
|
|
||||||
|
Unacceptable behavior:
|
||||||
|
|
||||||
|
- Personal attacks, insults, or harassment
|
||||||
|
- Sustained off-topic disruption
|
||||||
|
- Publishing private information without consent
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This applies to all project spaces: GitHub issues, pull requests, discussions, and any other venue where project work happens.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
The project maintainer handles violations. Contact: aric.camarata@gmail.com.
|
||||||
|
|
||||||
|
Reports are reviewed promptly. Responses range from a private note to a permanent ban, depending on severity and history.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This code of conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 2.1.
|
||||||
49
.github/wiki/Contributing.md
vendored
Normal file
49
.github/wiki/Contributing.md
vendored
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
# Contributing
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Node.js 20 or later
|
||||||
|
- pnpm (enabled via corepack: `corepack enable`)
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git clone https://github.com/acamarata/hijri-core.git
|
||||||
|
cd hijri-core
|
||||||
|
pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pnpm build # compile TypeScript
|
||||||
|
pnpm test # build + run test suite
|
||||||
|
pnpm run typecheck # type-check without emitting
|
||||||
|
pnpm run lint # ESLint
|
||||||
|
pnpm run format # Prettier format
|
||||||
|
```
|
||||||
|
|
||||||
|
## Conversion Engines
|
||||||
|
|
||||||
|
The package ships two engines:
|
||||||
|
|
||||||
|
- **UAQ** (Umm al-Qura): table-based, identical to the official Saudi calendar
|
||||||
|
- **FCNA**: an algorithmic engine for Fiqh Council of North America calculations
|
||||||
|
|
||||||
|
When modifying either engine, cross-validate against the reference tables in the test suite. The UAQ table covers 1318-1500 AH; the FCNA engine is unbounded but less authoritative outside that range.
|
||||||
|
|
||||||
|
## Downstream Packages
|
||||||
|
|
||||||
|
hijri-core is the foundation for several thin wrapper packages:
|
||||||
|
|
||||||
|
- `luxon-hijri`, `date-fns-hijri`, `dayjs-hijri-plus`, `moment-hijri-plus`, `temporal-hijri` (JS)
|
||||||
|
- `hijri_core` (Dart)
|
||||||
|
|
||||||
|
Breaking changes here require coordinated updates across all downstream packages. When changing the public API, note which downstream packages are affected in your PR description.
|
||||||
|
|
||||||
|
## Pull Requests
|
||||||
|
|
||||||
|
- One logical change per PR
|
||||||
|
- Include tests with cross-validation against known dates
|
||||||
|
- Update `CHANGELOG.md` under `[Unreleased]`
|
||||||
|
- Do not bump the version number
|
||||||
26
.github/wiki/SECURITY.md
vendored
Normal file
26
.github/wiki/SECURITY.md
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Supported Versions
|
||||||
|
|
||||||
|
| Version | Supported |
|
||||||
|
| --- | --- |
|
||||||
|
| 1.x | Yes |
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
Do not open a public GitHub issue for security vulnerabilities.
|
||||||
|
|
||||||
|
Email: aric.camarata@gmail.com
|
||||||
|
|
||||||
|
Include:
|
||||||
|
|
||||||
|
- A description of the vulnerability
|
||||||
|
- Steps to reproduce
|
||||||
|
- Potential impact
|
||||||
|
- Any suggested fix, if you have one
|
||||||
|
|
||||||
|
You will receive an acknowledgment within 48 hours and a resolution timeline within 7 days.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This package is a pure computation library. It performs no network requests, reads no files, and holds no credentials. All calendar conversion logic is deterministic arithmetic. The primary security concern would be a supply-chain compromise of the npm package.
|
||||||
1
.github/wiki/_Footer.md
vendored
Normal file
1
.github/wiki/_Footer.md
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
[npm](https://www.npmjs.com/package/hijri-core) · [GitHub](https://github.com/acamarata/hijri-core) · [Changelog](https://github.com/acamarata/hijri-core/blob/main/CHANGELOG.md) · MIT License
|
||||||
38
.github/wiki/_Sidebar.md
vendored
Normal file
38
.github/wiki/_Sidebar.md
vendored
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
## hijri-core
|
||||||
|
|
||||||
|
**[Home](Home)**
|
||||||
|
|
||||||
|
**Reference**
|
||||||
|
- [API Reference](API-Reference)
|
||||||
|
- [Architecture](Architecture)
|
||||||
|
- [Benchmarks](benchmarks/index)
|
||||||
|
|
||||||
|
**API**
|
||||||
|
- [toHijri](api/toHijri)
|
||||||
|
- [toGregorian](api/toGregorian)
|
||||||
|
- [isValidHijriDate](api/isValidHijriDate)
|
||||||
|
- [daysInHijriMonth](api/daysInHijriMonth)
|
||||||
|
- [registerCalendar](api/registerCalendar)
|
||||||
|
- [getCalendar](api/getCalendar)
|
||||||
|
- [listCalendars](api/listCalendars)
|
||||||
|
- [hDatesTable](api/hDatesTable)
|
||||||
|
- [hmLong / hmMedium / hmShort](api/hmLong)
|
||||||
|
- [hwLong / hwShort / hwNumeric](api/hwLong)
|
||||||
|
|
||||||
|
**Guides**
|
||||||
|
- [Quick Start](guides/quickstart)
|
||||||
|
- [Advanced Usage](guides/advanced)
|
||||||
|
|
||||||
|
**Examples**
|
||||||
|
- [Gregorian to Hijri](examples/gregorian-to-hijri)
|
||||||
|
- [Ramadan Calendar](examples/ramadan-calendar)
|
||||||
|
|
||||||
|
**Contributing**
|
||||||
|
- [Contributing](CONTRIBUTING)
|
||||||
|
- [Code of Conduct](CODE_OF_CONDUCT)
|
||||||
|
- [Security](SECURITY)
|
||||||
|
|
||||||
|
**Links**
|
||||||
|
- [npm](https://www.npmjs.com/package/hijri-core)
|
||||||
|
- [GitHub](https://github.com/acamarata/hijri-core)
|
||||||
|
- [Changelog](https://github.com/acamarata/hijri-core/blob/main/CHANGELOG.md)
|
||||||
34
.github/wiki/api/README.md
vendored
Normal file
34
.github/wiki/api/README.md
vendored
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
**hijri-core v1.0.1**
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
# hijri-core v1.0.1
|
||||||
|
|
||||||
|
## Interfaces
|
||||||
|
|
||||||
|
- [CalendarEngine](interfaces/CalendarEngine.md)
|
||||||
|
- [ConversionOptions](interfaces/ConversionOptions.md)
|
||||||
|
- [HijriDate](interfaces/HijriDate.md)
|
||||||
|
- [HijriYearRecord](interfaces/HijriYearRecord.md)
|
||||||
|
|
||||||
|
## Variables
|
||||||
|
|
||||||
|
- [hDatesTable](variables/hDatesTable.md)
|
||||||
|
- [hmLong](variables/hmLong.md)
|
||||||
|
- [hmMedium](variables/hmMedium.md)
|
||||||
|
- [hmShort](variables/hmShort.md)
|
||||||
|
- [hwLong](variables/hwLong.md)
|
||||||
|
- [hwNumeric](variables/hwNumeric.md)
|
||||||
|
- [hwShort](variables/hwShort.md)
|
||||||
|
- [MONTHS\_PER\_YEAR](variables/MONTHS_PER_YEAR.md)
|
||||||
|
- [MS\_PER\_DAY](variables/MS_PER_DAY.md)
|
||||||
|
|
||||||
|
## Functions
|
||||||
|
|
||||||
|
- [daysInHijriMonth](functions/daysInHijriMonth.md)
|
||||||
|
- [getCalendar](functions/getCalendar.md)
|
||||||
|
- [isValidHijriDate](functions/isValidHijriDate.md)
|
||||||
|
- [listCalendars](functions/listCalendars.md)
|
||||||
|
- [registerCalendar](functions/registerCalendar.md)
|
||||||
|
- [toGregorian](functions/toGregorian.md)
|
||||||
|
- [toHijri](functions/toHijri.md)
|
||||||
43
.github/wiki/api/functions/daysInHijriMonth.md
vendored
Normal file
43
.github/wiki/api/functions/daysInHijriMonth.md
vendored
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
[**hijri-core v1.0.1**](../README.md)
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
[hijri-core](../README.md) / daysInHijriMonth
|
||||||
|
|
||||||
|
# Function: daysInHijriMonth()
|
||||||
|
|
||||||
|
> **daysInHijriMonth**(`hy`, `hm`, `options?`): `number`
|
||||||
|
|
||||||
|
Defined in: [index.ts:96](https://github.com/acamarata/hijri-core/blob/235ffb8851dac2e67cab33f1fc76cd8c00bbbe7c/src/index.ts#L96)
|
||||||
|
|
||||||
|
Return the number of days in a given Hijri month.
|
||||||
|
|
||||||
|
## Parameters
|
||||||
|
|
||||||
|
### hy
|
||||||
|
|
||||||
|
`number`
|
||||||
|
|
||||||
|
Hijri year
|
||||||
|
|
||||||
|
### hm
|
||||||
|
|
||||||
|
`number`
|
||||||
|
|
||||||
|
Hijri month (1-12)
|
||||||
|
|
||||||
|
### options?
|
||||||
|
|
||||||
|
[`ConversionOptions`](../interfaces/ConversionOptions.md)
|
||||||
|
|
||||||
|
conversion options (calendar engine selection)
|
||||||
|
|
||||||
|
## Returns
|
||||||
|
|
||||||
|
`number`
|
||||||
|
|
||||||
|
29 or 30
|
||||||
|
|
||||||
|
## Throws
|
||||||
|
|
||||||
|
if the month or year is out of range
|
||||||
31
.github/wiki/api/functions/getCalendar.md
vendored
Normal file
31
.github/wiki/api/functions/getCalendar.md
vendored
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
[**hijri-core v1.0.1**](../README.md)
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
[hijri-core](../README.md) / getCalendar
|
||||||
|
|
||||||
|
# Function: getCalendar()
|
||||||
|
|
||||||
|
> **getCalendar**(`name`): [`CalendarEngine`](../interfaces/CalendarEngine.md)
|
||||||
|
|
||||||
|
Defined in: [registry.ts:25](https://github.com/acamarata/hijri-core/blob/235ffb8851dac2e67cab33f1fc76cd8c00bbbe7c/src/registry.ts#L25)
|
||||||
|
|
||||||
|
Retrieve a registered calendar engine by name.
|
||||||
|
|
||||||
|
## Parameters
|
||||||
|
|
||||||
|
### name
|
||||||
|
|
||||||
|
`string`
|
||||||
|
|
||||||
|
the calendar identifier passed to [registerCalendar](registerCalendar.md)
|
||||||
|
|
||||||
|
## Returns
|
||||||
|
|
||||||
|
[`CalendarEngine`](../interfaces/CalendarEngine.md)
|
||||||
|
|
||||||
|
the matching engine
|
||||||
|
|
||||||
|
## Throws
|
||||||
|
|
||||||
|
if no engine is registered under that name
|
||||||
45
.github/wiki/api/functions/isValidHijriDate.md
vendored
Normal file
45
.github/wiki/api/functions/isValidHijriDate.md
vendored
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
[**hijri-core v1.0.1**](../README.md)
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
[hijri-core](../README.md) / isValidHijriDate
|
||||||
|
|
||||||
|
# Function: isValidHijriDate()
|
||||||
|
|
||||||
|
> **isValidHijriDate**(`hy`, `hm`, `hd`, `options?`): `boolean`
|
||||||
|
|
||||||
|
Defined in: [index.ts:78](https://github.com/acamarata/hijri-core/blob/235ffb8851dac2e67cab33f1fc76cd8c00bbbe7c/src/index.ts#L78)
|
||||||
|
|
||||||
|
Check whether a Hijri date is valid for the given calendar engine.
|
||||||
|
|
||||||
|
## Parameters
|
||||||
|
|
||||||
|
### hy
|
||||||
|
|
||||||
|
`number`
|
||||||
|
|
||||||
|
Hijri year
|
||||||
|
|
||||||
|
### hm
|
||||||
|
|
||||||
|
`number`
|
||||||
|
|
||||||
|
Hijri month (1-12)
|
||||||
|
|
||||||
|
### hd
|
||||||
|
|
||||||
|
`number`
|
||||||
|
|
||||||
|
Hijri day (1-30)
|
||||||
|
|
||||||
|
### options?
|
||||||
|
|
||||||
|
[`ConversionOptions`](../interfaces/ConversionOptions.md)
|
||||||
|
|
||||||
|
conversion options (calendar engine selection)
|
||||||
|
|
||||||
|
## Returns
|
||||||
|
|
||||||
|
`boolean`
|
||||||
|
|
||||||
|
true if the date is valid
|
||||||
19
.github/wiki/api/functions/listCalendars.md
vendored
Normal file
19
.github/wiki/api/functions/listCalendars.md
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
[**hijri-core v1.0.1**](../README.md)
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
[hijri-core](../README.md) / listCalendars
|
||||||
|
|
||||||
|
# Function: listCalendars()
|
||||||
|
|
||||||
|
> **listCalendars**(): `string`[]
|
||||||
|
|
||||||
|
Defined in: [registry.ts:41](https://github.com/acamarata/hijri-core/blob/235ffb8851dac2e67cab33f1fc76cd8c00bbbe7c/src/registry.ts#L41)
|
||||||
|
|
||||||
|
List the names of all registered calendar engines.
|
||||||
|
|
||||||
|
## Returns
|
||||||
|
|
||||||
|
`string`[]
|
||||||
|
|
||||||
|
an array of calendar names (e.g. ['uaq', 'fcna'])
|
||||||
34
.github/wiki/api/functions/registerCalendar.md
vendored
Normal file
34
.github/wiki/api/functions/registerCalendar.md
vendored
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
[**hijri-core v1.0.1**](../README.md)
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
[hijri-core](../README.md) / registerCalendar
|
||||||
|
|
||||||
|
# Function: registerCalendar()
|
||||||
|
|
||||||
|
> **registerCalendar**(`name`, `engine`): `void`
|
||||||
|
|
||||||
|
Defined in: [registry.ts:14](https://github.com/acamarata/hijri-core/blob/235ffb8851dac2e67cab33f1fc76cd8c00bbbe7c/src/registry.ts#L14)
|
||||||
|
|
||||||
|
Register a calendar engine under the given name.
|
||||||
|
|
||||||
|
Once registered, the engine can be selected via `{ calendar: name }` in any
|
||||||
|
conversion function or retrieved directly with [getCalendar](getCalendar.md).
|
||||||
|
|
||||||
|
## Parameters
|
||||||
|
|
||||||
|
### name
|
||||||
|
|
||||||
|
`string`
|
||||||
|
|
||||||
|
unique identifier for the calendar (e.g. 'uaq', 'fcna')
|
||||||
|
|
||||||
|
### engine
|
||||||
|
|
||||||
|
[`CalendarEngine`](../interfaces/CalendarEngine.md)
|
||||||
|
|
||||||
|
an object implementing the [CalendarEngine](../interfaces/CalendarEngine.md) interface
|
||||||
|
|
||||||
|
## Returns
|
||||||
|
|
||||||
|
`void`
|
||||||
47
.github/wiki/api/functions/toGregorian.md
vendored
Normal file
47
.github/wiki/api/functions/toGregorian.md
vendored
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
[**hijri-core v1.0.1**](../README.md)
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
[hijri-core](../README.md) / toGregorian
|
||||||
|
|
||||||
|
# Function: toGregorian()
|
||||||
|
|
||||||
|
> **toGregorian**(`hy`, `hm`, `hd`, `options?`): `Date` \| `null`
|
||||||
|
|
||||||
|
Defined in: [index.ts:60](https://github.com/acamarata/hijri-core/blob/235ffb8851dac2e67cab33f1fc76cd8c00bbbe7c/src/index.ts#L60)
|
||||||
|
|
||||||
|
Convert a Hijri date to a Gregorian date.
|
||||||
|
|
||||||
|
Uses the UAQ calendar by default.
|
||||||
|
|
||||||
|
## Parameters
|
||||||
|
|
||||||
|
### hy
|
||||||
|
|
||||||
|
`number`
|
||||||
|
|
||||||
|
Hijri year
|
||||||
|
|
||||||
|
### hm
|
||||||
|
|
||||||
|
`number`
|
||||||
|
|
||||||
|
Hijri month (1-12)
|
||||||
|
|
||||||
|
### hd
|
||||||
|
|
||||||
|
`number`
|
||||||
|
|
||||||
|
Hijri day (1-30)
|
||||||
|
|
||||||
|
### options?
|
||||||
|
|
||||||
|
[`ConversionOptions`](../interfaces/ConversionOptions.md)
|
||||||
|
|
||||||
|
conversion options (calendar engine selection)
|
||||||
|
|
||||||
|
## Returns
|
||||||
|
|
||||||
|
`Date` \| `null`
|
||||||
|
|
||||||
|
a Date in UTC, or null if the input is invalid or out of range
|
||||||
40
.github/wiki/api/functions/toHijri.md
vendored
Normal file
40
.github/wiki/api/functions/toHijri.md
vendored
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
[**hijri-core v1.0.1**](../README.md)
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
[hijri-core](../README.md) / toHijri
|
||||||
|
|
||||||
|
# Function: toHijri()
|
||||||
|
|
||||||
|
> **toHijri**(`date`, `options?`): [`HijriDate`](../interfaces/HijriDate.md) \| `null`
|
||||||
|
|
||||||
|
Defined in: [index.ts:42](https://github.com/acamarata/hijri-core/blob/235ffb8851dac2e67cab33f1fc76cd8c00bbbe7c/src/index.ts#L42)
|
||||||
|
|
||||||
|
Convert a Gregorian date to a Hijri date.
|
||||||
|
|
||||||
|
Uses the UAQ (Umm al-Qura) calendar by default. Pass `{ calendar: 'fcna' }`
|
||||||
|
or any registered calendar name via options to use a different engine.
|
||||||
|
|
||||||
|
## Parameters
|
||||||
|
|
||||||
|
### date
|
||||||
|
|
||||||
|
`Date`
|
||||||
|
|
||||||
|
a valid JavaScript Date object
|
||||||
|
|
||||||
|
### options?
|
||||||
|
|
||||||
|
[`ConversionOptions`](../interfaces/ConversionOptions.md)
|
||||||
|
|
||||||
|
conversion options (calendar engine selection)
|
||||||
|
|
||||||
|
## Returns
|
||||||
|
|
||||||
|
[`HijriDate`](../interfaces/HijriDate.md) \| `null`
|
||||||
|
|
||||||
|
the corresponding Hijri date, or null if the date is out of range
|
||||||
|
|
||||||
|
## Throws
|
||||||
|
|
||||||
|
if `date` is not a valid Date instance
|
||||||
118
.github/wiki/api/interfaces/CalendarEngine.md
vendored
Normal file
118
.github/wiki/api/interfaces/CalendarEngine.md
vendored
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
[**hijri-core v1.0.1**](../README.md)
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
[hijri-core](../README.md) / CalendarEngine
|
||||||
|
|
||||||
|
# Interface: CalendarEngine
|
||||||
|
|
||||||
|
Defined in: [types.ts:42](https://github.com/acamarata/hijri-core/blob/235ffb8851dac2e67cab33f1fc76cd8c00bbbe7c/src/types.ts#L42)
|
||||||
|
|
||||||
|
Interface every calendar engine must implement.
|
||||||
|
|
||||||
|
Return `null` when a date is outside the engine's supported range.
|
||||||
|
Throw `Error` for structurally invalid input (malformed Date, month outside 1-12, etc.).
|
||||||
|
Never throw for out-of-range inputs — return `null` instead so callers can handle
|
||||||
|
the boundary gracefully without try/catch.
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
|
||||||
|
### id
|
||||||
|
|
||||||
|
> `readonly` **id**: `string`
|
||||||
|
|
||||||
|
Defined in: [types.ts:43](https://github.com/acamarata/hijri-core/blob/235ffb8851dac2e67cab33f1fc76cd8c00bbbe7c/src/types.ts#L43)
|
||||||
|
|
||||||
|
## Methods
|
||||||
|
|
||||||
|
### daysInMonth()
|
||||||
|
|
||||||
|
> **daysInMonth**(`hy`, `hm`): `number`
|
||||||
|
|
||||||
|
Defined in: [types.ts:48](https://github.com/acamarata/hijri-core/blob/235ffb8851dac2e67cab33f1fc76cd8c00bbbe7c/src/types.ts#L48)
|
||||||
|
|
||||||
|
#### Parameters
|
||||||
|
|
||||||
|
##### hy
|
||||||
|
|
||||||
|
`number`
|
||||||
|
|
||||||
|
##### hm
|
||||||
|
|
||||||
|
`number`
|
||||||
|
|
||||||
|
#### Returns
|
||||||
|
|
||||||
|
`number`
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
### isValid()
|
||||||
|
|
||||||
|
> **isValid**(`hy`, `hm`, `hd`): `boolean`
|
||||||
|
|
||||||
|
Defined in: [types.ts:47](https://github.com/acamarata/hijri-core/blob/235ffb8851dac2e67cab33f1fc76cd8c00bbbe7c/src/types.ts#L47)
|
||||||
|
|
||||||
|
#### Parameters
|
||||||
|
|
||||||
|
##### hy
|
||||||
|
|
||||||
|
`number`
|
||||||
|
|
||||||
|
##### hm
|
||||||
|
|
||||||
|
`number`
|
||||||
|
|
||||||
|
##### hd
|
||||||
|
|
||||||
|
`number`
|
||||||
|
|
||||||
|
#### Returns
|
||||||
|
|
||||||
|
`boolean`
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
### toGregorian()
|
||||||
|
|
||||||
|
> **toGregorian**(`hy`, `hm`, `hd`): `Date` \| `null`
|
||||||
|
|
||||||
|
Defined in: [types.ts:46](https://github.com/acamarata/hijri-core/blob/235ffb8851dac2e67cab33f1fc76cd8c00bbbe7c/src/types.ts#L46)
|
||||||
|
|
||||||
|
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: [types.ts:44](https://github.com/acamarata/hijri-core/blob/235ffb8851dac2e67cab33f1fc76cd8c00bbbe7c/src/types.ts#L44)
|
||||||
|
|
||||||
|
#### Parameters
|
||||||
|
|
||||||
|
##### date
|
||||||
|
|
||||||
|
`Date`
|
||||||
|
|
||||||
|
#### Returns
|
||||||
|
|
||||||
|
[`HijriDate`](HijriDate.md) \| `null`
|
||||||
22
.github/wiki/api/interfaces/ConversionOptions.md
vendored
Normal file
22
.github/wiki/api/interfaces/ConversionOptions.md
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
[**hijri-core v1.0.1**](../README.md)
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
[hijri-core](../README.md) / ConversionOptions
|
||||||
|
|
||||||
|
# Interface: ConversionOptions
|
||||||
|
|
||||||
|
Defined in: [types.ts:57](https://github.com/acamarata/hijri-core/blob/235ffb8851dac2e67cab33f1fc76cd8c00bbbe7c/src/types.ts#L57)
|
||||||
|
|
||||||
|
Options accepted by the convenience conversion functions.
|
||||||
|
|
||||||
|
Omitting `calendar` defaults to `'uaq'` (Umm al-Qura).
|
||||||
|
Pass any name previously given to [registerCalendar](../functions/registerCalendar.md) to use a custom engine.
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
|
||||||
|
### calendar?
|
||||||
|
|
||||||
|
> `optional` **calendar?**: `string`
|
||||||
|
|
||||||
|
Defined in: [types.ts:58](https://github.com/acamarata/hijri-core/blob/235ffb8851dac2e67cab33f1fc76cd8c00bbbe7c/src/types.ts#L58)
|
||||||
44
.github/wiki/api/interfaces/HijriDate.md
vendored
Normal file
44
.github/wiki/api/interfaces/HijriDate.md
vendored
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
[**hijri-core v1.0.1**](../README.md)
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
[hijri-core](../README.md) / HijriDate
|
||||||
|
|
||||||
|
# Interface: HijriDate
|
||||||
|
|
||||||
|
Defined in: [types.ts:10](https://github.com/acamarata/hijri-core/blob/235ffb8851dac2e67cab33f1fc76cd8c00bbbe7c/src/types.ts#L10)
|
||||||
|
|
||||||
|
A Hijri date triple.
|
||||||
|
|
||||||
|
All three fields are required. Month and day are 1-based.
|
||||||
|
The year is a Hijri (AH) year number, e.g. 1446.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const d: HijriDate = { hy: 1446, hm: 9, hd: 1 }; // 1 Ramadan 1446 AH
|
||||||
|
```
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
|
||||||
|
### hd
|
||||||
|
|
||||||
|
> **hd**: `number`
|
||||||
|
|
||||||
|
Defined in: [types.ts:13](https://github.com/acamarata/hijri-core/blob/235ffb8851dac2e67cab33f1fc76cd8c00bbbe7c/src/types.ts#L13)
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
### hm
|
||||||
|
|
||||||
|
> **hm**: `number`
|
||||||
|
|
||||||
|
Defined in: [types.ts:12](https://github.com/acamarata/hijri-core/blob/235ffb8851dac2e67cab33f1fc76cd8c00bbbe7c/src/types.ts#L12)
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
### hy
|
||||||
|
|
||||||
|
> **hy**: `number`
|
||||||
|
|
||||||
|
Defined in: [types.ts:11](https://github.com/acamarata/hijri-core/blob/235ffb8851dac2e67cab33f1fc76cd8c00bbbe7c/src/types.ts#L11)
|
||||||
58
.github/wiki/api/interfaces/HijriYearRecord.md
vendored
Normal file
58
.github/wiki/api/interfaces/HijriYearRecord.md
vendored
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
[**hijri-core v1.0.1**](../README.md)
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
[hijri-core](../README.md) / HijriYearRecord
|
||||||
|
|
||||||
|
# Interface: HijriYearRecord
|
||||||
|
|
||||||
|
Defined in: [types.ts:26](https://github.com/acamarata/hijri-core/blob/235ffb8851dac2e67cab33f1fc76cd8c00bbbe7c/src/types.ts#L26)
|
||||||
|
|
||||||
|
One row in the Umm al-Qura reference table.
|
||||||
|
|
||||||
|
The table covers Hijri years 1318-1500 (Gregorian 1900-2076). A sentinel row
|
||||||
|
at hy=1501 with dpm=0 marks the upper boundary and is used to detect
|
||||||
|
out-of-range inputs without a separate bounds check.
|
||||||
|
|
||||||
|
The `dpm` bitmask encodes month lengths for all 12 months:
|
||||||
|
bit i (0-indexed from bit 0) = month i+1; 1 = 30 days, 0 = 29 days.
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
|
||||||
|
### dpm
|
||||||
|
|
||||||
|
> **dpm**: `number`
|
||||||
|
|
||||||
|
Defined in: [types.ts:28](https://github.com/acamarata/hijri-core/blob/235ffb8851dac2e67cab33f1fc76cd8c00bbbe7c/src/types.ts#L28)
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
### gd
|
||||||
|
|
||||||
|
> **gd**: `number`
|
||||||
|
|
||||||
|
Defined in: [types.ts:31](https://github.com/acamarata/hijri-core/blob/235ffb8851dac2e67cab33f1fc76cd8c00bbbe7c/src/types.ts#L31)
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
### gm
|
||||||
|
|
||||||
|
> **gm**: `number`
|
||||||
|
|
||||||
|
Defined in: [types.ts:30](https://github.com/acamarata/hijri-core/blob/235ffb8851dac2e67cab33f1fc76cd8c00bbbe7c/src/types.ts#L30)
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
### gy
|
||||||
|
|
||||||
|
> **gy**: `number`
|
||||||
|
|
||||||
|
Defined in: [types.ts:29](https://github.com/acamarata/hijri-core/blob/235ffb8851dac2e67cab33f1fc76cd8c00bbbe7c/src/types.ts#L29)
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
### hy
|
||||||
|
|
||||||
|
> **hy**: `number`
|
||||||
|
|
||||||
|
Defined in: [types.ts:27](https://github.com/acamarata/hijri-core/blob/235ffb8851dac2e67cab33f1fc76cd8c00bbbe7c/src/types.ts#L27)
|
||||||
16
.github/wiki/api/variables/MONTHS_PER_YEAR.md
vendored
Normal file
16
.github/wiki/api/variables/MONTHS_PER_YEAR.md
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
[**hijri-core v1.0.1**](../README.md)
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
[hijri-core](../README.md) / MONTHS\_PER\_YEAR
|
||||||
|
|
||||||
|
# Variable: MONTHS\_PER\_YEAR
|
||||||
|
|
||||||
|
> `const` **MONTHS\_PER\_YEAR**: `12` = `12`
|
||||||
|
|
||||||
|
Defined in: [constants.ts:16](https://github.com/acamarata/hijri-core/blob/235ffb8851dac2e67cab33f1fc76cd8c00bbbe7c/src/constants.ts#L16)
|
||||||
|
|
||||||
|
Number of months in a Hijri year.
|
||||||
|
|
||||||
|
The Islamic calendar is purely lunar: 12 months of 29 or 30 days each,
|
||||||
|
totalling 354 or 355 days per year. This constant is 12.
|
||||||
17
.github/wiki/api/variables/MS_PER_DAY.md
vendored
Normal file
17
.github/wiki/api/variables/MS_PER_DAY.md
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
[**hijri-core v1.0.1**](../README.md)
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
[hijri-core](../README.md) / MS\_PER\_DAY
|
||||||
|
|
||||||
|
# Variable: MS\_PER\_DAY
|
||||||
|
|
||||||
|
> `const` **MS\_PER\_DAY**: `86400000` = `86_400_000`
|
||||||
|
|
||||||
|
Defined in: [constants.ts:8](https://github.com/acamarata/hijri-core/blob/235ffb8851dac2e67cab33f1fc76cd8c00bbbe7c/src/constants.ts#L8)
|
||||||
|
|
||||||
|
Milliseconds in one day (24 * 60 * 60 * 1000).
|
||||||
|
|
||||||
|
Used internally for day-offset arithmetic when converting between Gregorian
|
||||||
|
timestamps and Hijri dates. Exposed as a public constant so custom engine
|
||||||
|
authors can share the same value without redefining it.
|
||||||
11
.github/wiki/api/variables/hDatesTable.md
vendored
Normal file
11
.github/wiki/api/variables/hDatesTable.md
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
[**hijri-core v1.0.1**](../README.md)
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
[hijri-core](../README.md) / hDatesTable
|
||||||
|
|
||||||
|
# Variable: hDatesTable
|
||||||
|
|
||||||
|
> `const` **hDatesTable**: [`HijriYearRecord`](../interfaces/HijriYearRecord.md)[]
|
||||||
|
|
||||||
|
Defined in: [data/hDates.ts:7](https://github.com/acamarata/hijri-core/blob/235ffb8851dac2e67cab33f1fc76cd8c00bbbe7c/src/data/hDates.ts#L7)
|
||||||
22
.github/wiki/api/variables/hmLong.md
vendored
Normal file
22
.github/wiki/api/variables/hmLong.md
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
[**hijri-core v1.0.1**](../README.md)
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
[hijri-core](../README.md) / hmLong
|
||||||
|
|
||||||
|
# Variable: hmLong
|
||||||
|
|
||||||
|
> `const` **hmLong**: `string`[]
|
||||||
|
|
||||||
|
Defined in: [names/months.ts:13](https://github.com/acamarata/hijri-core/blob/235ffb8851dac2e67cab33f1fc76cd8c00bbbe7c/src/names/months.ts#L13)
|
||||||
|
|
||||||
|
Full English transliterations of the 12 Hijri month names.
|
||||||
|
|
||||||
|
Index 0 corresponds to Muharram (month 1); index 11 to Dhul Hijjah (month 12).
|
||||||
|
Suitable for display in contexts where the full name aids readability.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const month = hmLong[hijriDate.hm - 1]; // "Ramadan"
|
||||||
|
```
|
||||||
22
.github/wiki/api/variables/hmMedium.md
vendored
Normal file
22
.github/wiki/api/variables/hmMedium.md
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
[**hijri-core v1.0.1**](../README.md)
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
[hijri-core](../README.md) / hmMedium
|
||||||
|
|
||||||
|
# Variable: hmMedium
|
||||||
|
|
||||||
|
> `const` **hmMedium**: `string`[]
|
||||||
|
|
||||||
|
Defined in: [names/months.ts:37](https://github.com/acamarata/hijri-core/blob/235ffb8851dac2e67cab33f1fc76cd8c00bbbe7c/src/names/months.ts#L37)
|
||||||
|
|
||||||
|
Medium-length transliterations of the 12 Hijri month names.
|
||||||
|
|
||||||
|
Shorter than [hmLong](hmLong.md) but more readable than [hmShort](hmShort.md).
|
||||||
|
Useful for compact date labels where space is limited.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const label = hmMedium[hijriDate.hm - 1]; // "Ramadan"
|
||||||
|
```
|
||||||
22
.github/wiki/api/variables/hmShort.md
vendored
Normal file
22
.github/wiki/api/variables/hmShort.md
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
[**hijri-core v1.0.1**](../README.md)
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
[hijri-core](../README.md) / hmShort
|
||||||
|
|
||||||
|
# Variable: hmShort
|
||||||
|
|
||||||
|
> `const` **hmShort**: `string`[]
|
||||||
|
|
||||||
|
Defined in: [names/months.ts:61](https://github.com/acamarata/hijri-core/blob/235ffb8851dac2e67cab33f1fc76cd8c00bbbe7c/src/names/months.ts#L61)
|
||||||
|
|
||||||
|
Three-character short codes for the 12 Hijri months.
|
||||||
|
|
||||||
|
Designed for narrow columns such as calendar grids or spreadsheet headers.
|
||||||
|
Each code is exactly 3 ASCII characters.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const abbr = hmShort[hijriDate.hm - 1]; // "Ram"
|
||||||
|
```
|
||||||
22
.github/wiki/api/variables/hwLong.md
vendored
Normal file
22
.github/wiki/api/variables/hwLong.md
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
[**hijri-core v1.0.1**](../README.md)
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
[hijri-core](../README.md) / hwLong
|
||||||
|
|
||||||
|
# Variable: hwLong
|
||||||
|
|
||||||
|
> `const` **hwLong**: `string`[]
|
||||||
|
|
||||||
|
Defined in: [names/weekdays.ts:13](https://github.com/acamarata/hijri-core/blob/235ffb8851dac2e67cab33f1fc76cd8c00bbbe7c/src/names/weekdays.ts#L13)
|
||||||
|
|
||||||
|
Full Arabic-transliterated names for the seven days of the week.
|
||||||
|
|
||||||
|
Index alignment matches `Date.prototype.getDay()`:
|
||||||
|
index 0 = Sunday, index 6 = Saturday.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const dayName = hwLong[gregorianDate.getDay()]; // "Yawm al-Jum`a"
|
||||||
|
```
|
||||||
17
.github/wiki/api/variables/hwNumeric.md
vendored
Normal file
17
.github/wiki/api/variables/hwNumeric.md
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
[**hijri-core v1.0.1**](../README.md)
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
[hijri-core](../README.md) / hwNumeric
|
||||||
|
|
||||||
|
# Variable: hwNumeric
|
||||||
|
|
||||||
|
> `const` **hwNumeric**: `number`[]
|
||||||
|
|
||||||
|
Defined in: [names/weekdays.ts:50](https://github.com/acamarata/hijri-core/blob/235ffb8851dac2e67cab33f1fc76cd8c00bbbe7c/src/names/weekdays.ts#L50)
|
||||||
|
|
||||||
|
Numeric weekday values: 1 = Sunday through 7 = Saturday.
|
||||||
|
|
||||||
|
This follows the ISO 8601 convention where Monday = 1, but offset by one
|
||||||
|
to match the Islamic numbering where Sunday is the first day of the week.
|
||||||
|
Index alignment matches `Date.prototype.getDay()`.
|
||||||
22
.github/wiki/api/variables/hwShort.md
vendored
Normal file
22
.github/wiki/api/variables/hwShort.md
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
[**hijri-core v1.0.1**](../README.md)
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
[hijri-core](../README.md) / hwShort
|
||||||
|
|
||||||
|
# Variable: hwShort
|
||||||
|
|
||||||
|
> `const` **hwShort**: `string`[]
|
||||||
|
|
||||||
|
Defined in: [names/weekdays.ts:32](https://github.com/acamarata/hijri-core/blob/235ffb8851dac2e67cab33f1fc76cd8c00bbbe7c/src/names/weekdays.ts#L32)
|
||||||
|
|
||||||
|
Short single-word transliterations for the seven days of the week.
|
||||||
|
|
||||||
|
Index alignment matches `Date.prototype.getDay()`:
|
||||||
|
index 0 = Sunday, index 6 = Saturday.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const abbr = hwShort[gregorianDate.getDay()]; // "Jum`a"
|
||||||
|
```
|
||||||
56
.github/wiki/benchmarks/index.md
vendored
Normal file
56
.github/wiki/benchmarks/index.md
vendored
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
# Benchmarks
|
||||||
|
|
||||||
|
Performance and bundle-size data for hijri-core@1.0.1.
|
||||||
|
|
||||||
|
## Bundle size
|
||||||
|
|
||||||
|
Measured from the published ESM build (`dist/index.mjs`). The package ships a dual CJS+ESM output; sizes are nearly identical.
|
||||||
|
|
||||||
|
| Format | Minified | Gzipped |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| ESM (`dist/index.mjs`) | 20.8 KB | 5.3 KB |
|
||||||
|
| CJS (`dist/index.cjs`) | 22.3 KB | 5.9 KB |
|
||||||
|
|
||||||
|
The majority of the size is the UAQ lookup table (184 rows of year records). The conversion logic itself is small.
|
||||||
|
|
||||||
|
## Conversion throughput
|
||||||
|
|
||||||
|
Single-threaded, Node.js 22, Apple M-series chip. 100,000 iterations per function.
|
||||||
|
Results are representative for production workloads.
|
||||||
|
|
||||||
|
| Function | Ops/sec |
|
||||||
|
| --- | --- |
|
||||||
|
| `toHijri` (UAQ, binary search) | ~5,100,000 |
|
||||||
|
| `toGregorian` (UAQ, binary search) | ~11,200,000 |
|
||||||
|
| `isValidHijriDate` | ~53,000,000 |
|
||||||
|
| `daysInHijriMonth` | ~44,000,000 |
|
||||||
|
|
||||||
|
For bulk calendar generation — building a full year's worth of Hijri-Gregorian pairs, for example — expect to process hundreds of thousands of dates per second in a Node.js context.
|
||||||
|
|
||||||
|
## Methodology
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { toHijri, toGregorian } from 'hijri-core';
|
||||||
|
|
||||||
|
const N = 100_000;
|
||||||
|
const date = new Date('2025-03-01');
|
||||||
|
|
||||||
|
const t0 = performance.now();
|
||||||
|
for (let i = 0; i < N; i++) toHijri(date);
|
||||||
|
const t1 = performance.now();
|
||||||
|
|
||||||
|
const opsPerSec = Math.round(N / ((t1 - t0) / 1000));
|
||||||
|
console.log(`toHijri: ${opsPerSec.toLocaleString()} ops/sec`);
|
||||||
|
```
|
||||||
|
|
||||||
|
Run `node --input-type=module` with the snippet above after `pnpm build` to reproduce.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- The UAQ engine uses O(log n) binary search over 184 rows. A linear scan would be ~7x slower.
|
||||||
|
- The FCNA engine runs trigonometric new-moon calculations; throughput is lower (~100,000-200,000 ops/sec). Use FCNA when accuracy to the North American criterion matters; use UAQ for display and calendar grids.
|
||||||
|
- There are no allocations beyond the returned `HijriDate` object. The engine itself holds no mutable state.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[Home](../Home) | [API Reference](../API-Reference)
|
||||||
42
.github/wiki/examples/gregorian-to-hijri.md
vendored
Normal file
42
.github/wiki/examples/gregorian-to-hijri.md
vendored
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
# Example: Gregorian to Hijri Conversion Table
|
||||||
|
|
||||||
|
Convert a range of notable Gregorian dates to Hijri.
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { toHijri, hmLong } from 'hijri-core';
|
||||||
|
|
||||||
|
const dates = [
|
||||||
|
{ label: 'Islamic New Year 1446', date: new Date('2024-07-07') },
|
||||||
|
{ label: 'Ashura 1446', date: new Date('2024-07-16') },
|
||||||
|
{ label: 'Ramadan 1446 start', date: new Date('2025-03-01') },
|
||||||
|
{ label: 'Eid al-Fitr 1446', date: new Date('2025-03-30') },
|
||||||
|
{ label: 'Arafat Day 1446', date: new Date('2025-06-05') },
|
||||||
|
{ label: 'Eid al-Adha 1446', date: new Date('2025-06-06') },
|
||||||
|
];
|
||||||
|
|
||||||
|
console.log(`${'Event'.padEnd(26)} Gregorian Hijri`);
|
||||||
|
console.log('-'.repeat(62));
|
||||||
|
|
||||||
|
for (const { label, date } of dates) {
|
||||||
|
const h = toHijri(date);
|
||||||
|
const greg = date.toISOString().slice(0, 10);
|
||||||
|
const hijri = h
|
||||||
|
? `${h.hd} ${hmLong[h.hm - 1]} ${h.hy} AH`
|
||||||
|
: 'out of range';
|
||||||
|
|
||||||
|
console.log(`${label.padEnd(26)} ${greg} ${hijri}`);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Sample output:
|
||||||
|
|
||||||
|
```
|
||||||
|
Event Gregorian Hijri
|
||||||
|
--------------------------------------------------------------
|
||||||
|
Islamic New Year 1446 2024-07-07 1 Muharram 1446 AH
|
||||||
|
Ashura 1446 2024-07-16 10 Muharram 1446 AH
|
||||||
|
Ramadan 1446 start 2025-03-01 1 Ramadan 1446 AH
|
||||||
|
Eid al-Fitr 1446 2025-03-30 1 Shawwal 1446 AH
|
||||||
|
Arafat Day 1446 2025-06-05 9 Dhul Hijjah 1446 AH
|
||||||
|
Eid al-Adha 1446 2025-06-06 10 Dhul Hijjah 1446 AH
|
||||||
|
```
|
||||||
53
.github/wiki/examples/ramadan-calendar.md
vendored
Normal file
53
.github/wiki/examples/ramadan-calendar.md
vendored
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
# Example: Ramadan Calendar Generator
|
||||||
|
|
||||||
|
Generate the full Ramadan calendar for a given Hijri year showing Gregorian dates.
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { toGregorian, daysInHijriMonth } from 'hijri-core';
|
||||||
|
|
||||||
|
const HIJRI_YEAR = 1446;
|
||||||
|
const RAMADAN = 9;
|
||||||
|
|
||||||
|
const totalDays = daysInHijriMonth(HIJRI_YEAR, RAMADAN);
|
||||||
|
|
||||||
|
console.log(`Ramadan ${HIJRI_YEAR} AH — ${totalDays} days`);
|
||||||
|
console.log('');
|
||||||
|
console.log('Day Gregorian Weekday');
|
||||||
|
console.log('-'.repeat(32));
|
||||||
|
|
||||||
|
const DAYS = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
||||||
|
|
||||||
|
for (let d = 1; d <= totalDays; d++) {
|
||||||
|
const greg = toGregorian(HIJRI_YEAR, RAMADAN, d);
|
||||||
|
const iso = greg.toISOString().slice(0, 10);
|
||||||
|
const weekday = DAYS[greg.getUTCDay()];
|
||||||
|
|
||||||
|
// Mark Jumu'ah
|
||||||
|
const marker = weekday === 'Friday' ? ' ★' : '';
|
||||||
|
|
||||||
|
console.log(`${String(d).padStart(3)} ${iso} ${weekday}${marker}`);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Sample output:
|
||||||
|
|
||||||
|
```
|
||||||
|
Ramadan 1446 AH — 30 days
|
||||||
|
|
||||||
|
Day Gregorian Weekday
|
||||||
|
--------------------------------
|
||||||
|
1 2025-03-01 Saturday
|
||||||
|
2 2025-03-02 Sunday
|
||||||
|
3 2025-03-03 Monday
|
||||||
|
4 2025-03-04 Tuesday
|
||||||
|
5 2025-03-05 Wednesday
|
||||||
|
6 2025-03-06 Thursday
|
||||||
|
7 2025-03-07 Friday ★
|
||||||
|
8 2025-03-08 Saturday
|
||||||
|
...
|
||||||
|
28 2025-03-28 Friday ★
|
||||||
|
29 2025-03-29 Saturday
|
||||||
|
30 2025-03-30 Sunday
|
||||||
|
```
|
||||||
|
|
||||||
|
> Eid al-Fitr falls on 1 Shawwal — the day after the last day of Ramadan.
|
||||||
131
.github/wiki/guides/advanced.md
vendored
Normal file
131
.github/wiki/guides/advanced.md
vendored
Normal file
|
|
@ -0,0 +1,131 @@
|
||||||
|
# Advanced Usage
|
||||||
|
|
||||||
|
## Iterating over a Hijri month
|
||||||
|
|
||||||
|
Build a calendar grid for a given Hijri month:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { daysInHijriMonth, toGregorian } from 'hijri-core';
|
||||||
|
|
||||||
|
const HY = 1446;
|
||||||
|
const HM = 9; // Ramadan
|
||||||
|
|
||||||
|
const days = daysInHijriMonth(HY, HM);
|
||||||
|
|
||||||
|
for (let d = 1; d <= days; d++) {
|
||||||
|
const greg = toGregorian(HY, HM, d);
|
||||||
|
console.log(`${String(d).padStart(2)} Ramadan — ${greg.toISOString().slice(0, 10)}`);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Custom calendar engines
|
||||||
|
|
||||||
|
`hijri-core` exposes a registry so you can plug in your own conversion engine — a lunar sighting authority, an adjusted algorithmic calendar, or a custom dataset:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { registerCalendar, toHijri } from 'hijri-core';
|
||||||
|
|
||||||
|
registerCalendar('my-calendar', {
|
||||||
|
id: 'my-calendar',
|
||||||
|
toHijri(date) {
|
||||||
|
// Return { hy, hm, hd } or null for out-of-range.
|
||||||
|
// Use local date components for timezone-safe lookup.
|
||||||
|
const y = date.getFullYear();
|
||||||
|
const m = date.getMonth() + 1;
|
||||||
|
const d = date.getDate();
|
||||||
|
// ... your logic
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
toGregorian(hy, hm, hd) {
|
||||||
|
// Return a Date (UTC midnight) or null for out-of-range.
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
isValid(hy, hm, hd) {
|
||||||
|
return hy > 0 && hm >= 1 && hm <= 12 && hd >= 1 && hd <= 30;
|
||||||
|
},
|
||||||
|
daysInMonth(hy, hm) {
|
||||||
|
return 29;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Use it just like the built-in calendars.
|
||||||
|
const result = toHijri(new Date('2025-03-20'), { calendar: 'my-calendar' });
|
||||||
|
```
|
||||||
|
|
||||||
|
## Generating a Ramadan calendar
|
||||||
|
|
||||||
|
Print the Gregorian dates for Ramadan across multiple years:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { toGregorian } from 'hijri-core';
|
||||||
|
|
||||||
|
const RAMADAN = 9;
|
||||||
|
|
||||||
|
for (let hy = 1440; hy <= 1450; hy++) {
|
||||||
|
const start = toGregorian(hy, RAMADAN, 1);
|
||||||
|
const end = toGregorian(hy, RAMADAN + 1, 1);
|
||||||
|
|
||||||
|
if (!start) continue;
|
||||||
|
|
||||||
|
// Ramadan ends the day before Shawwal 1.
|
||||||
|
const last = new Date(end.getTime() - 86400_000);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`${hy} AH: ${start.toISOString().slice(0, 10)} — ${last.toISOString().slice(0, 10)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Date range validation
|
||||||
|
|
||||||
|
Before batch-processing a range of dates, check the calendar bounds:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { toHijri } from 'hijri-core';
|
||||||
|
|
||||||
|
function isSupportedDate(date) {
|
||||||
|
return toHijri(date) !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dates = [
|
||||||
|
new Date('1900-01-01'), // outside UAQ range
|
||||||
|
new Date('2000-01-01'), // inside range
|
||||||
|
new Date('2077-11-16'), // may be outside future range
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const d of dates) {
|
||||||
|
const supported = isSupportedDate(d);
|
||||||
|
console.log(`${d.toISOString().slice(0, 10)}: ${supported ? 'supported' : 'out of range'}`);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## FCNA vs UAQ differences
|
||||||
|
|
||||||
|
FCNA and UAQ can differ by one or two days around month transitions:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { toHijri } from 'hijri-core';
|
||||||
|
|
||||||
|
const dates = [
|
||||||
|
new Date('2025-03-01'),
|
||||||
|
new Date('2025-03-29'),
|
||||||
|
new Date('2025-03-30'),
|
||||||
|
new Date('2025-03-31'),
|
||||||
|
];
|
||||||
|
|
||||||
|
console.log('Date UAQ FCNA');
|
||||||
|
console.log('-'.repeat(48));
|
||||||
|
|
||||||
|
for (const d of dates) {
|
||||||
|
const uaq = toHijri(d, { calendar: 'uaq' });
|
||||||
|
const fcna = toHijri(d, { calendar: 'fcna' });
|
||||||
|
|
||||||
|
const fmtH = (h) => h ? `${h.hd}/${h.hm}/${h.hy}` : 'out of range';
|
||||||
|
console.log(`${d.toISOString().slice(0, 10)} ${fmtH(uaq).padEnd(16)} ${fmtH(fcna)}`);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Related pages
|
||||||
|
|
||||||
|
- [API Reference](../API-Reference) — full function signatures and types
|
||||||
|
- [Architecture](../Architecture) — calendar engine interface, table format, accuracy bounds
|
||||||
94
.github/wiki/guides/quickstart.md
vendored
Normal file
94
.github/wiki/guides/quickstart.md
vendored
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
# Quick Start
|
||||||
|
|
||||||
|
Five minutes from install to Hijri date conversions.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm install hijri-core
|
||||||
|
```
|
||||||
|
|
||||||
|
## Convert Gregorian to Hijri
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { toHijri } from 'hijri-core';
|
||||||
|
|
||||||
|
const d = new Date('2025-03-20');
|
||||||
|
const h = toHijri(d);
|
||||||
|
|
||||||
|
console.log(h);
|
||||||
|
// { hy: 1446, hm: 9, hd: 20 }
|
||||||
|
|
||||||
|
console.log(`${h.hd} Ramadan ${h.hy} AH`);
|
||||||
|
// 20 Ramadan 1446 AH
|
||||||
|
```
|
||||||
|
|
||||||
|
## Convert Hijri to Gregorian
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { toGregorian } from 'hijri-core';
|
||||||
|
|
||||||
|
const greg = toGregorian(1446, 9, 1);
|
||||||
|
|
||||||
|
console.log(greg.toISOString().slice(0, 10));
|
||||||
|
// 2025-03-01
|
||||||
|
```
|
||||||
|
|
||||||
|
## Check a Hijri date is valid
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { isValidHijriDate } from 'hijri-core';
|
||||||
|
|
||||||
|
isValidHijriDate(1446, 9, 30); // true — Ramadan 1446 has 30 days
|
||||||
|
isValidHijriDate(1446, 9, 31); // false — no 31st day in any Hijri month
|
||||||
|
isValidHijriDate(1446, 13, 1); // false — Hijri calendar has only 12 months
|
||||||
|
```
|
||||||
|
|
||||||
|
## Days in a month
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { daysInHijriMonth } from 'hijri-core';
|
||||||
|
|
||||||
|
daysInHijriMonth(1446, 9); // 30 (Ramadan 1446)
|
||||||
|
daysInHijriMonth(1446, 10); // 29 (Shawwal 1446)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Choosing a calendar
|
||||||
|
|
||||||
|
Two calendars are built in:
|
||||||
|
|
||||||
|
| Name | Description |
|
||||||
|
| ---- | ----------- |
|
||||||
|
| `'uaq'` | Umm al-Qura (Saudi Arabia, official tabular calendar) — **default** |
|
||||||
|
| `'fcna'` | Fiqh Council of North America (astronomical computation for North America) |
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { toHijri } from 'hijri-core';
|
||||||
|
|
||||||
|
const d = new Date('2025-03-20');
|
||||||
|
|
||||||
|
const uaq = toHijri(d, { calendar: 'uaq' });
|
||||||
|
const fcna = toHijri(d, { calendar: 'fcna' });
|
||||||
|
|
||||||
|
console.log(uaq.hd, uaq.hm, uaq.hy);
|
||||||
|
console.log(fcna.hd, fcna.hm, fcna.hy);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Out-of-range dates
|
||||||
|
|
||||||
|
Both built-in calendars cover a finite date range. `toHijri` and `toGregorian` return `null` when the input falls outside the supported range:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { toHijri } from 'hijri-core';
|
||||||
|
|
||||||
|
const result = toHijri(new Date('1800-01-01'));
|
||||||
|
if (result === null) {
|
||||||
|
console.log('Date outside calendar range');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Next steps
|
||||||
|
|
||||||
|
- [API Reference](../API-Reference) — all functions and types
|
||||||
|
- [Advanced Guide](advanced) — custom calendar engines, iteration, Ramadan calendars
|
||||||
|
- [Architecture](../Architecture) — calendar engine design, table format
|
||||||
34
.github/workflows/ci.yml
vendored
34
.github/workflows/ci.yml
vendored
|
|
@ -15,7 +15,8 @@ jobs:
|
||||||
node: [20, 22, 24]
|
node: [20, 22, 24]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: pnpm/action-setup@v4
|
- name: Enable corepack
|
||||||
|
run: corepack enable
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node }}
|
node-version: ${{ matrix.node }}
|
||||||
|
|
@ -30,7 +31,8 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: pnpm/action-setup@v4
|
- name: Enable corepack
|
||||||
|
run: corepack enable
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 24
|
||||||
|
|
@ -44,7 +46,8 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: pnpm/action-setup@v4
|
- name: Enable corepack
|
||||||
|
run: corepack enable
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 24
|
||||||
|
|
@ -57,7 +60,8 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: pnpm/action-setup@v4
|
- name: Enable corepack
|
||||||
|
run: corepack enable
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 24
|
||||||
|
|
@ -74,3 +78,25 @@ jobs:
|
||||||
grep "README.md" pack-output.txt
|
grep "README.md" pack-output.txt
|
||||||
grep "CHANGELOG.md" pack-output.txt
|
grep "CHANGELOG.md" pack-output.txt
|
||||||
grep "LICENSE" 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
1
.gitignore
vendored
|
|
@ -2,6 +2,7 @@ node_modules/
|
||||||
dist/
|
dist/
|
||||||
build/
|
build/
|
||||||
out/
|
out/
|
||||||
|
docs/
|
||||||
*.tgz
|
*.tgz
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
*.log
|
*.log
|
||||||
|
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
{
|
|
||||||
"singleQuote": true,
|
|
||||||
"trailingComma": "all",
|
|
||||||
"printWidth": 100,
|
|
||||||
"tabWidth": 2
|
|
||||||
}
|
|
||||||
15
CHANGELOG.md
15
CHANGELOG.md
|
|
@ -5,7 +5,20 @@ 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/),
|
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).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## [Unreleased]
|
## [1.0.4] - 2026-06-13
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Published package now includes dist/index.d.mts so ESM type resolution under node16/nodenext resolves the import condition.
|
||||||
|
|
||||||
|
## [1.0.3] - 2026-06-10
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- UAQ engine `toHijri` was reading local date components (`getFullYear/getMonth/getDate`) instead of UTC components, causing incorrect results on hosts west of UTC (e.g. `America/New_York`, `Pacific/Auckland`) when the input Date was a UTC-midnight value such as those returned by `toGregorian` or ISO date-only strings. `toHijri` now reads UTC calendar day components (`getUTCFullYear/getUTCMonth/getUTCDate`), matching the FCNA engine. **Behavior change:** on non-UTC hosts the converted Hijri day may shift to the UTC calendar day; round-trips via `toGregorian` are now exact on every machine.
|
||||||
|
|
||||||
|
## [1.0.2] - 2026-05-30
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Apply TypeScript strict null-check fixes in uaq.ts and fcna.ts (non-null assertions compile away; no behavior change)
|
||||||
|
|
||||||
## [1.0.1] - 2026-05-28
|
## [1.0.1] - 2026-05-28
|
||||||
|
|
||||||
|
|
|
||||||
22
README.md
22
README.md
|
|
@ -3,6 +3,7 @@
|
||||||
[](https://www.npmjs.com/package/hijri-core)
|
[](https://www.npmjs.com/package/hijri-core)
|
||||||
[](https://github.com/acamarata/hijri-core/actions/workflows/ci.yml)
|
[](https://github.com/acamarata/hijri-core/actions/workflows/ci.yml)
|
||||||
[](LICENSE)
|
[](LICENSE)
|
||||||
|
[](https://github.com/acamarata/hijri-core/wiki)
|
||||||
|
|
||||||
Zero-dependency Hijri calendar engine for JavaScript and TypeScript. Supports the Umm al-Qura (UAQ) and FCNA/ISNA calendars out of the box. A pluggable registry lets you add custom calendar implementations at runtime.
|
Zero-dependency Hijri calendar engine for JavaScript and TypeScript. Supports the Umm al-Qura (UAQ) and FCNA/ISNA calendars out of the box. A pluggable registry lets you add custom calendar implementations at runtime.
|
||||||
|
|
||||||
|
|
@ -34,6 +35,27 @@ isValidHijriDate(1444, 9, 1); // true
|
||||||
daysInHijriMonth(1444, 9); // 29
|
daysInHijriMonth(1444, 9); // 29
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Day boundaries and time zones
|
||||||
|
|
||||||
|
hijri-core maps civil calendar days one-to-one (tabular UAQ, computed FCNA). The religious Hijri day beginning at sunset is intentionally out of scope.
|
||||||
|
|
||||||
|
`toHijri` reads the input Date's UTC calendar day (`getUTCFullYear`, `getUTCMonth`, `getUTCDate`). `toGregorian` returns a UTC-midnight Date. This means round-trips are exact and results are identical on every machine regardless of local time zone:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Safe on any host — UTC-explicit construction
|
||||||
|
const greg = toGregorian(1446, 9, 1); // 2025-03-01T00:00:00.000Z
|
||||||
|
const back = toHijri(greg!); // { hy: 1446, hm: 9, hd: 1 } — always exact
|
||||||
|
|
||||||
|
// ISO date-only strings parse as UTC midnight — correct
|
||||||
|
toHijri(new Date('2025-03-01')); // { hy: 1446, hm: 9, hd: 1 }
|
||||||
|
|
||||||
|
// For a local wall-clock date, construct explicitly in UTC
|
||||||
|
toHijri(new Date(Date.UTC(2025, 2, 1))); // { hy: 1446, hm: 9, hd: 1 }
|
||||||
|
|
||||||
|
// Avoid local Date constructor for date-only conversions — breaks on UTC+13
|
||||||
|
// toHijri(new Date(2025, 2, 1)) ← do NOT do this
|
||||||
|
```
|
||||||
|
|
||||||
## Custom Calendars
|
## Custom Calendars
|
||||||
|
|
||||||
Implement `CalendarEngine` and call `registerCalendar('my-id', engine)`. Pass `{ calendar: 'my-id' }` to any conversion function.
|
Implement `CalendarEngine` and call `registerCalendar('my-id', engine)`. Pass `{ calendar: 'my-id' }` to any conversion function.
|
||||||
|
|
|
||||||
8
TELEMETRY.md
Normal file
8
TELEMETRY.md
Normal 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)
|
||||||
|
|
@ -1,10 +1,23 @@
|
||||||
import js from '@eslint/js';
|
import tsParser from '@typescript-eslint/parser';
|
||||||
import tseslint from 'typescript-eslint';
|
import tsPlugin from '@typescript-eslint/eslint-plugin';
|
||||||
import prettier from 'eslint-config-prettier';
|
import eslintConfigPrettier from 'eslint-config-prettier';
|
||||||
|
import { typescript } from '@acamarata/eslint-config';
|
||||||
|
|
||||||
export default tseslint.config(
|
export default [
|
||||||
{ ignores: ['dist/', 'node_modules/', '*.cjs', '*.mjs'] },
|
{
|
||||||
js.configs.recommended,
|
ignores: ['dist/**', 'node_modules/**', 'coverage/**', 'docs/**'],
|
||||||
...tseslint.configs.recommended,
|
},
|
||||||
prettier,
|
{
|
||||||
);
|
files: ['src/**/*.ts'],
|
||||||
|
plugins: { '@typescript-eslint': tsPlugin },
|
||||||
|
languageOptions: {
|
||||||
|
parser: tsParser,
|
||||||
|
parserOptions: {
|
||||||
|
project: './tsconfig.json',
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...typescript.map((cfg) => ({ ...cfg, files: ['src/**/*.ts'] })),
|
||||||
|
eslintConfigPrettier,
|
||||||
|
];
|
||||||
|
|
|
||||||
140
hijri-core.test.ts
Normal file
140
hijri-core.test.ts
Normal file
|
|
@ -0,0 +1,140 @@
|
||||||
|
/**
|
||||||
|
* Purpose: Vitest suite for hijri-core — conversion, validation, registry, and name exports.
|
||||||
|
* Inputs: Pure functions from src/index.ts (no network; module-load registers uaq+fcna engines).
|
||||||
|
* Outputs: Vitest pass/fail assertions.
|
||||||
|
* Constraints: UAQ calendar covers 1318–1500 AH (≈1900–2076 CE). toHijri interprets the input
|
||||||
|
* Date by its UTC calendar day (getUTC* components); pass UTC-explicit Dates for
|
||||||
|
* deterministic results. toGregorian returns UTC midnight, so round-trips are exact.
|
||||||
|
* Usage: pnpm vitest run
|
||||||
|
* SOT: packages.md — hijri-core row
|
||||||
|
*/
|
||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import {
|
||||||
|
toHijri,
|
||||||
|
toGregorian,
|
||||||
|
isValidHijriDate,
|
||||||
|
daysInHijriMonth,
|
||||||
|
listCalendars,
|
||||||
|
hmLong,
|
||||||
|
hwLong,
|
||||||
|
} from "./src/index";
|
||||||
|
|
||||||
|
describe("toGregorian (UAQ default)", () => {
|
||||||
|
it("converts 1 Ramadan 1446 to 2025-03-01 UTC midnight", () => {
|
||||||
|
const result = toGregorian(1446, 9, 1);
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result!.toISOString()).toBe("2025-03-01T00:00:00.000Z");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("converts 1 Muharram 1446 to 2024-07-07 UTC midnight", () => {
|
||||||
|
const result = toGregorian(1446, 1, 1);
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result!.toISOString()).toBe("2024-07-07T00:00:00.000Z");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns null for an out-of-range Hijri year (1501)", () => {
|
||||||
|
expect(toGregorian(1501, 1, 1)).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("toHijri (UAQ default)", () => {
|
||||||
|
it("converts 2025-03-01 noon UTC to 1 Ramadan 1446", () => {
|
||||||
|
// toGregorian(1446,9,1) = 2025-03-01 midnight; add 12h to stay on that Gregorian day
|
||||||
|
const noonOnRamadanStart = new Date("2025-03-01T12:00:00Z");
|
||||||
|
const result = toHijri(noonOnRamadanStart);
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result!.hy).toBe(1446);
|
||||||
|
expect(result!.hm).toBe(9);
|
||||||
|
expect(result!.hd).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns null for a date outside UAQ range (year 2100)", () => {
|
||||||
|
expect(toHijri(new Date("2100-01-01T12:00:00Z"))).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws on an invalid Date object", () => {
|
||||||
|
expect(() => toHijri(new Date("invalid"))).toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("isValidHijriDate", () => {
|
||||||
|
it("accepts a known valid date", () => {
|
||||||
|
expect(isValidHijriDate(1446, 9, 1)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects month 13", () => {
|
||||||
|
expect(isValidHijriDate(1446, 13, 1)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects day 0", () => {
|
||||||
|
expect(isValidHijriDate(1446, 1, 0)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("daysInHijriMonth", () => {
|
||||||
|
it("returns 29 or 30 for a valid month", () => {
|
||||||
|
const days = daysInHijriMonth(1446, 9);
|
||||||
|
expect([29, 30]).toContain(days);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("registry", () => {
|
||||||
|
it("lists at least uaq and fcna after module load", () => {
|
||||||
|
const calendars = listCalendars();
|
||||||
|
expect(calendars).toContain("uaq");
|
||||||
|
expect(calendars).toContain("fcna");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("name tables", () => {
|
||||||
|
it("hmLong has 12 entries and index 8 is Ramadan", () => {
|
||||||
|
expect(hmLong).toHaveLength(12);
|
||||||
|
expect(hmLong[8]).toBe("Ramadan");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("hwLong has 7 weekday entries", () => {
|
||||||
|
expect(hwLong).toHaveLength(7);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("day boundaries (UTC contract)", () => {
|
||||||
|
it("UAQ round-trip: toHijri(toGregorian(1446, 9, 1)) returns {hy:1446, hm:9, hd:1}", () => {
|
||||||
|
const greg = toGregorian(1446, 9, 1);
|
||||||
|
expect(greg).not.toBeNull();
|
||||||
|
const hijri = toHijri(greg!);
|
||||||
|
expect(hijri).not.toBeNull();
|
||||||
|
expect(hijri!.hy).toBe(1446);
|
||||||
|
expect(hijri!.hm).toBe(9);
|
||||||
|
expect(hijri!.hd).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("toHijri(new Date('2025-03-01T00:00:00Z')) = 1 Ramadan 1446", () => {
|
||||||
|
const result = toHijri(new Date("2025-03-01T00:00:00Z"));
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result!.hy).toBe(1446);
|
||||||
|
expect(result!.hm).toBe(9);
|
||||||
|
expect(result!.hd).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("last ms of 2025-02-28 UTC maps to the same Hijri day as noon on 2025-02-28 UTC", () => {
|
||||||
|
const lastMs = toHijri(new Date("2025-02-28T23:59:59.999Z"));
|
||||||
|
const noon = toHijri(new Date("2025-02-28T12:00:00Z"));
|
||||||
|
expect(lastMs).not.toBeNull();
|
||||||
|
expect(noon).not.toBeNull();
|
||||||
|
expect(lastMs!.hy).toBe(noon!.hy);
|
||||||
|
expect(lastMs!.hm).toBe(noon!.hm);
|
||||||
|
expect(lastMs!.hd).toBe(noon!.hd);
|
||||||
|
// and it is the day before 1 Ramadan 1446
|
||||||
|
expect(lastMs!.hm).not.toBe(9);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("FCNA round-trip: toHijri(toGregorian(1446, 9, 1, {calendar:'fcna'}), {calendar:'fcna'}) is exact", () => {
|
||||||
|
const greg = toGregorian(1446, 9, 1, { calendar: "fcna" });
|
||||||
|
expect(greg).not.toBeNull();
|
||||||
|
const hijri = toHijri(greg!, { calendar: "fcna" });
|
||||||
|
expect(hijri).not.toBeNull();
|
||||||
|
expect(hijri!.hy).toBe(1446);
|
||||||
|
expect(hijri!.hm).toBe(9);
|
||||||
|
expect(hijri!.hd).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
28
package.json
28
package.json
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "hijri-core",
|
"name": "hijri-core",
|
||||||
"version": "1.0.1",
|
"version": "1.0.4",
|
||||||
"description": "Zero-dependency Hijri calendar engine with pluggable calendar support. Includes Umm al-Qura (UAQ) and FCNA/ISNA calendars. Extensible registry for custom calendars.",
|
"description": "Zero-dependency Hijri calendar engine with pluggable calendar support. Includes Umm al-Qura (UAQ) and FCNA/ISNA calendars. Extensible registry for custom calendars.",
|
||||||
"author": "Aric Camarata",
|
"author": "Aric Camarata",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
@ -34,11 +34,14 @@
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"pretest": "tsup",
|
"pretest": "tsup",
|
||||||
"test": "node --test test.mjs && node --test test-cjs.cjs",
|
"test": "node --test test.mjs && node --test test-cjs.cjs",
|
||||||
"prepublishOnly": "tsup",
|
"prepack": "pnpm run build",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write src/",
|
||||||
"format:check": "prettier --check .",
|
"format:check": "prettier --check src/",
|
||||||
"coverage": "c8 --reporter=lcov --reporter=text node --test"
|
"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",
|
||||||
|
"test:vitest": "vitest run"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"hijri",
|
"hijri",
|
||||||
|
|
@ -53,14 +56,24 @@
|
||||||
"typescript"
|
"typescript"
|
||||||
],
|
],
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@acamarata/eslint-config": "^0.1.0",
|
||||||
|
"@acamarata/prettier-config": "^0.1.0",
|
||||||
|
"@acamarata/telemetry": "^0.1.0",
|
||||||
|
"@acamarata/tsconfig": "^0.1.0",
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
"@types/node": "^22.15.0",
|
"@types/node": "^22.15.0",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^8.56.1",
|
||||||
|
"@typescript-eslint/parser": "^8.56.1",
|
||||||
|
"c8": "^10.1.0",
|
||||||
"eslint": "^10.0.3",
|
"eslint": "^10.0.3",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"prettier": "^3.8.1",
|
"prettier": "^3.8.1",
|
||||||
"tsup": "^8.0.0",
|
"tsup": "^8.0.0",
|
||||||
|
"typedoc": "^0.28.19",
|
||||||
|
"typedoc-plugin-markdown": "^4.11.0",
|
||||||
"typescript": "^5.5.0",
|
"typescript": "^5.5.0",
|
||||||
"typescript-eslint": "^8.56.1"
|
"typescript-eslint": "^8.56.1",
|
||||||
|
"vitest": "^2.1.9"
|
||||||
},
|
},
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public",
|
"access": "public",
|
||||||
|
|
@ -74,5 +87,6 @@
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/acamarata/hijri-core/issues"
|
"url": "https://github.com/acamarata/hijri-core/issues"
|
||||||
},
|
},
|
||||||
"type": "module"
|
"type": "module",
|
||||||
|
"prettier": "@acamarata/prettier-config"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
2110
pnpm-lock.yaml
2110
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
|
@ -1,5 +1,16 @@
|
||||||
/** Milliseconds in one day. */
|
/**
|
||||||
|
* Milliseconds in one day (24 * 60 * 60 * 1000).
|
||||||
|
*
|
||||||
|
* Used internally for day-offset arithmetic when converting between Gregorian
|
||||||
|
* timestamps and Hijri dates. Exposed as a public constant so custom engine
|
||||||
|
* authors can share the same value without redefining it.
|
||||||
|
*/
|
||||||
export const MS_PER_DAY = 86_400_000;
|
export const MS_PER_DAY = 86_400_000;
|
||||||
|
|
||||||
/** Number of months in a Hijri year. */
|
/**
|
||||||
|
* Number of months in a Hijri year.
|
||||||
|
*
|
||||||
|
* The Islamic calendar is purely lunar: 12 months of 29 or 30 days each,
|
||||||
|
* totalling 354 or 355 days per year. This constant is 12.
|
||||||
|
*/
|
||||||
export const MONTHS_PER_YEAR = 12;
|
export const MONTHS_PER_YEAR = 12;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import type { HijriYearRecord } from '../types';
|
import type { HijriYearRecord } from "../types";
|
||||||
|
|
||||||
// Umm al-Qura reference table: Hijri years 1318-1501.
|
// Umm al-Qura reference table: Hijri years 1318-1501.
|
||||||
// Each entry records the 1 Muharram Gregorian date and a 12-bit days-per-month
|
// Each entry records the 1 Muharram Gregorian date and a 12-bit days-per-month
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,9 @@
|
||||||
// New moon times come from Jean Meeus, Astronomical Algorithms (2nd ed.),
|
// New moon times come from Jean Meeus, Astronomical Algorithms (2nd ed.),
|
||||||
// Chapter 49, accurate to within a few minutes for 1000-3000 CE.
|
// Chapter 49, accurate to within a few minutes for 1000-3000 CE.
|
||||||
|
|
||||||
import { hDatesTable } from '../data/hDates';
|
import { hDatesTable } from "../data/hDates";
|
||||||
import { MS_PER_DAY, MONTHS_PER_YEAR } from '../constants';
|
import { MS_PER_DAY, MONTHS_PER_YEAR } from "../constants";
|
||||||
import type { CalendarEngine, HijriDate } from '../types';
|
import type { CalendarEngine, HijriDate } from "../types";
|
||||||
|
|
||||||
// ─── Constants ───────────────────────────────────────────────────────────────
|
// ─── Constants ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -162,7 +162,8 @@ function uaqAnchorMs(hy: number, hm: number): number {
|
||||||
found = -1;
|
found = -1;
|
||||||
while (lo <= hi) {
|
while (lo <= hi) {
|
||||||
const mid = (lo + hi) >>> 1;
|
const mid = (lo + hi) >>> 1;
|
||||||
const midHy = hDatesTable[mid].hy;
|
// mid is always within [0, hDatesTable.length-1] by binary search invariant
|
||||||
|
const midHy = hDatesTable[mid]!.hy;
|
||||||
if (midHy === hy) {
|
if (midHy === hy) {
|
||||||
found = mid;
|
found = mid;
|
||||||
break;
|
break;
|
||||||
|
|
@ -170,8 +171,9 @@ function uaqAnchorMs(hy: number, hm: number): number {
|
||||||
else hi = mid - 1;
|
else hi = mid - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (found !== -1 && hDatesTable[found].dpm !== 0) {
|
// found is within [0, hDatesTable.length-1]; guard confirms it's valid before use.
|
||||||
const r = hDatesTable[found];
|
if (found !== -1 && hDatesTable[found]!.dpm !== 0) {
|
||||||
|
const r = hDatesTable[found]!;
|
||||||
let days = 0;
|
let days = 0;
|
||||||
for (let i = 0; i < hm - 1; i++) {
|
for (let i = 0; i < hm - 1; i++) {
|
||||||
days += (r.dpm >> i) & 1 ? 30 : 29;
|
days += (r.dpm >> i) & 1 ? 30 : 29;
|
||||||
|
|
@ -209,7 +211,7 @@ function fcnaDaysInMonth(hy: number, hm: number): number {
|
||||||
|
|
||||||
function fcnaToHijri(gregorianDate: Date): HijriDate | null {
|
function fcnaToHijri(gregorianDate: Date): HijriDate | null {
|
||||||
if (!(gregorianDate instanceof Date) || isNaN(gregorianDate.getTime())) {
|
if (!(gregorianDate instanceof Date) || isNaN(gregorianDate.getTime())) {
|
||||||
throw new Error('Invalid Gregorian date');
|
throw new Error("Invalid Gregorian date");
|
||||||
}
|
}
|
||||||
|
|
||||||
// FCNA criterion is UTC-based, so UTC date components ensure correct round-trips.
|
// FCNA criterion is UTC-based, so UTC date components ensure correct round-trips.
|
||||||
|
|
@ -269,7 +271,7 @@ function fcnaIsValid(hy: number, hm: number, hd: number): boolean {
|
||||||
// ─── Engine export ────────────────────────────────────────────────────────────
|
// ─── Engine export ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export const fcnaEngine: CalendarEngine = {
|
export const fcnaEngine: CalendarEngine = {
|
||||||
id: 'fcna',
|
id: "fcna",
|
||||||
toHijri: fcnaToHijri,
|
toHijri: fcnaToHijri,
|
||||||
toGregorian: fcnaToGregorian,
|
toGregorian: fcnaToGregorian,
|
||||||
isValid: fcnaIsValid,
|
isValid: fcnaIsValid,
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,9 @@
|
||||||
// (Gregorian 1900-2076). Each entry records the Gregorian date of 1 Muharram and
|
// (Gregorian 1900-2076). Each entry records the Gregorian date of 1 Muharram and
|
||||||
// a 12-bit days-per-month bitmask. Dates outside that window return null.
|
// a 12-bit days-per-month bitmask. Dates outside that window return null.
|
||||||
|
|
||||||
import { hDatesTable } from '../data/hDates';
|
import { hDatesTable } from "../data/hDates";
|
||||||
import { MS_PER_DAY, MONTHS_PER_YEAR } from '../constants';
|
import { MS_PER_DAY, MONTHS_PER_YEAR } from "../constants";
|
||||||
import type { CalendarEngine, HijriDate, HijriYearRecord } from '../types';
|
import type { CalendarEngine, HijriDate, HijriYearRecord } from "../types";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Binary search for a Hijri year entry in the UAQ table.
|
* Binary search for a Hijri year entry in the UAQ table.
|
||||||
|
|
@ -24,22 +24,27 @@ function findYearEntry(hy: number): HijriYearRecord | null {
|
||||||
|
|
||||||
while (lo <= hi) {
|
while (lo <= hi) {
|
||||||
const mid = (lo + hi) >>> 1;
|
const mid = (lo + hi) >>> 1;
|
||||||
const midHy = hDatesTable[mid].hy;
|
// mid is always within [0, hDatesTable.length-1] by binary search invariant
|
||||||
if (midHy === hy) return hDatesTable[mid];
|
const row = hDatesTable[mid]!;
|
||||||
|
const midHy = row.hy;
|
||||||
|
if (midHy === hy) return row;
|
||||||
else if (midHy < hy) lo = mid + 1;
|
else if (midHy < hy) lo = mid + 1;
|
||||||
else hi = mid - 1;
|
else hi = mid - 1;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// toHijri uses local date components (getFullYear, getMonth, getDate) so that
|
// toHijri interprets the input Date by its UTC calendar day (getUTC* components),
|
||||||
// the calendar-date lookup is timezone-safe regardless of the host environment.
|
// matching the FCNA engine and the UTC-midnight Dates returned by toGregorian.
|
||||||
|
// This makes conversions host-timezone-independent and round-trips exact:
|
||||||
|
// toHijri(toGregorian(hy, hm, hd)) === { hy, hm, hd } on any machine.
|
||||||
|
// To convert a local wall-clock date, pass new Date(Date.UTC(y, m - 1, d)).
|
||||||
function uaqToHijri(date: Date): HijriDate | null {
|
function uaqToHijri(date: Date): HijriDate | null {
|
||||||
if (!(date instanceof Date) || isNaN(date.getTime())) {
|
if (!(date instanceof Date) || isNaN(date.getTime())) {
|
||||||
throw new Error('Invalid Gregorian date');
|
throw new Error("Invalid Gregorian date");
|
||||||
}
|
}
|
||||||
|
|
||||||
const inputUtc = Date.UTC(date.getFullYear(), date.getMonth(), date.getDate());
|
const inputUtc = Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate());
|
||||||
|
|
||||||
// Binary search: find the last table entry whose Gregorian start date <= input.
|
// Binary search: find the last table entry whose Gregorian start date <= input.
|
||||||
let lo = 0;
|
let lo = 0;
|
||||||
|
|
@ -48,7 +53,8 @@ function uaqToHijri(date: Date): HijriDate | null {
|
||||||
|
|
||||||
while (lo <= hi) {
|
while (lo <= hi) {
|
||||||
const mid = (lo + hi) >>> 1;
|
const mid = (lo + hi) >>> 1;
|
||||||
const entry = hDatesTable[mid];
|
// mid is always within [0, hDatesTable.length-1] by binary search invariant
|
||||||
|
const entry = hDatesTable[mid]!;
|
||||||
const entryUtc = Date.UTC(entry.gy, entry.gm - 1, entry.gd);
|
const entryUtc = Date.UTC(entry.gy, entry.gm - 1, entry.gd);
|
||||||
|
|
||||||
if (entryUtc <= inputUtc) {
|
if (entryUtc <= inputUtc) {
|
||||||
|
|
@ -60,9 +66,10 @@ function uaqToHijri(date: Date): HijriDate | null {
|
||||||
}
|
}
|
||||||
|
|
||||||
// dpm === 0 is the sentinel entry (hy 1501) marking the upper bound.
|
// dpm === 0 is the sentinel entry (hy 1501) marking the upper bound.
|
||||||
if (found === -1 || hDatesTable[found].dpm === 0) return null;
|
// found is within [0, hDatesTable.length-1]; guard above confirms it's valid.
|
||||||
|
if (found === -1 || hDatesTable[found]!.dpm === 0) return null;
|
||||||
|
|
||||||
const record = hDatesTable[found];
|
const record = hDatesTable[found]!;
|
||||||
const startUtc = Date.UTC(record.gy, record.gm - 1, record.gd);
|
const startUtc = Date.UTC(record.gy, record.gm - 1, record.gd);
|
||||||
let remaining = Math.round((inputUtc - startUtc) / MS_PER_DAY);
|
let remaining = Math.round((inputUtc - startUtc) / MS_PER_DAY);
|
||||||
let hijriMonth = 0;
|
let hijriMonth = 0;
|
||||||
|
|
@ -122,7 +129,7 @@ function uaqDaysInMonth(hy: number, hm: number): number {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const uaqEngine: CalendarEngine = {
|
export const uaqEngine: CalendarEngine = {
|
||||||
id: 'uaq',
|
id: "uaq",
|
||||||
toHijri: uaqToHijri,
|
toHijri: uaqToHijri,
|
||||||
toGregorian: uaqToGregorian,
|
toGregorian: uaqToGregorian,
|
||||||
isValid: uaqIsValid,
|
isValid: uaqIsValid,
|
||||||
|
|
|
||||||
53
src/index.ts
53
src/index.ts
|
|
@ -1,32 +1,32 @@
|
||||||
// Built-in engines are registered at module load so that 'uaq' and 'fcna' are
|
// Built-in engines are registered at module load so that 'uaq' and 'fcna' are
|
||||||
// available immediately on import. This module-level side effect is intentional
|
// available immediately on import. This module-level side effect is intentional
|
||||||
// and documented in the sideEffects field of package.json.
|
// and documented in the sideEffects field of package.json.
|
||||||
import { uaqEngine } from './engines/uaq';
|
import { uaqEngine } from "./engines/uaq";
|
||||||
import { fcnaEngine } from './engines/fcna';
|
import { fcnaEngine } from "./engines/fcna";
|
||||||
import { registerCalendar } from './registry';
|
import { registerCalendar } from "./registry";
|
||||||
|
|
||||||
registerCalendar('uaq', uaqEngine);
|
registerCalendar("uaq", uaqEngine);
|
||||||
registerCalendar('fcna', fcnaEngine);
|
registerCalendar("fcna", fcnaEngine);
|
||||||
|
|
||||||
// Registry
|
// Registry
|
||||||
export { registerCalendar, getCalendar, listCalendars } from './registry';
|
export { registerCalendar, getCalendar, listCalendars } from "./registry";
|
||||||
|
|
||||||
// Constants
|
// Constants
|
||||||
export { MS_PER_DAY, MONTHS_PER_YEAR } from './constants';
|
export { MS_PER_DAY, MONTHS_PER_YEAR } from "./constants";
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
export type { HijriDate, HijriYearRecord, CalendarEngine, ConversionOptions } from './types';
|
export type { HijriDate, HijriYearRecord, CalendarEngine, ConversionOptions } from "./types";
|
||||||
|
|
||||||
// Data
|
// Data
|
||||||
export { hDatesTable } from './data/hDates';
|
export { hDatesTable } from "./data/hDates";
|
||||||
|
|
||||||
// Names
|
// Names
|
||||||
export { hmLong, hmMedium, hmShort } from './names/months';
|
export { hmLong, hmMedium, hmShort } from "./names/months";
|
||||||
export { hwLong, hwShort, hwNumeric } from './names/weekdays';
|
export { hwLong, hwShort, hwNumeric } from "./names/weekdays";
|
||||||
|
|
||||||
// Convenience wrappers
|
// Convenience wrappers
|
||||||
import { getCalendar } from './registry';
|
import { getCalendar } from "./registry";
|
||||||
import type { HijriDate, ConversionOptions } from './types';
|
import type { HijriDate, ConversionOptions } from "./types";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a Gregorian date to a Hijri date.
|
* Convert a Gregorian date to a Hijri date.
|
||||||
|
|
@ -34,6 +34,14 @@ import type { HijriDate, ConversionOptions } from './types';
|
||||||
* Uses the UAQ (Umm al-Qura) calendar by default. Pass `{ calendar: 'fcna' }`
|
* Uses the UAQ (Umm al-Qura) calendar by default. Pass `{ calendar: 'fcna' }`
|
||||||
* or any registered calendar name via options to use a different engine.
|
* or any registered calendar name via options to use a different engine.
|
||||||
*
|
*
|
||||||
|
* **Time-zone contract:** the Date is interpreted by its UTC calendar day
|
||||||
|
* (`getUTCFullYear`, `getUTCMonth`, `getUTCDate`). `toGregorian` returns a
|
||||||
|
* UTC-midnight Date, so round-trips are exact and results are identical on
|
||||||
|
* every host regardless of its local time zone.
|
||||||
|
*
|
||||||
|
* To convert a local wall-clock date, pass `new Date(Date.UTC(y, m - 1, d))`.
|
||||||
|
* Note that `new Date("2025-03-01")` parses as UTC midnight, which is correct.
|
||||||
|
*
|
||||||
* @param date - a valid JavaScript Date object
|
* @param date - a valid JavaScript Date object
|
||||||
* @param options - conversion options (calendar engine selection)
|
* @param options - conversion options (calendar engine selection)
|
||||||
* @returns the corresponding Hijri date, or null if the date is out of range
|
* @returns the corresponding Hijri date, or null if the date is out of range
|
||||||
|
|
@ -41,9 +49,9 @@ import type { HijriDate, ConversionOptions } from './types';
|
||||||
*/
|
*/
|
||||||
export function toHijri(date: Date, options?: ConversionOptions): HijriDate | null {
|
export function toHijri(date: Date, options?: ConversionOptions): HijriDate | null {
|
||||||
if (!(date instanceof Date) || isNaN(date.getTime())) {
|
if (!(date instanceof Date) || isNaN(date.getTime())) {
|
||||||
throw new Error('Invalid Gregorian date');
|
throw new Error("Invalid Gregorian date");
|
||||||
}
|
}
|
||||||
return getCalendar(options?.calendar ?? 'uaq').toHijri(date);
|
return getCalendar(options?.calendar ?? "uaq").toHijri(date);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -63,7 +71,7 @@ export function toGregorian(
|
||||||
hd: number,
|
hd: number,
|
||||||
options?: ConversionOptions,
|
options?: ConversionOptions,
|
||||||
): Date | null {
|
): Date | null {
|
||||||
return getCalendar(options?.calendar ?? 'uaq').toGregorian(hy, hm, hd);
|
return getCalendar(options?.calendar ?? "uaq").toGregorian(hy, hm, hd);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -81,7 +89,7 @@ export function isValidHijriDate(
|
||||||
hd: number,
|
hd: number,
|
||||||
options?: ConversionOptions,
|
options?: ConversionOptions,
|
||||||
): boolean {
|
): boolean {
|
||||||
return getCalendar(options?.calendar ?? 'uaq').isValid(hy, hm, hd);
|
return getCalendar(options?.calendar ?? "uaq").isValid(hy, hm, hd);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -94,5 +102,14 @@ export function isValidHijriDate(
|
||||||
* @throws {RangeError} if the month or year is out of range
|
* @throws {RangeError} if the month or year is out of range
|
||||||
*/
|
*/
|
||||||
export function daysInHijriMonth(hy: number, hm: number, options?: ConversionOptions): number {
|
export function daysInHijriMonth(hy: number, hm: number, options?: ConversionOptions): number {
|
||||||
return getCalendar(options?.calendar ?? 'uaq').daysInMonth(hy, hm);
|
return getCalendar(options?.calendar ?? "uaq").daysInMonth(hy, hm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 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: "hijri-core", version: "1.0.4" }))
|
||||||
|
.catch(() => {
|
||||||
|
// telemetry not installed or disabled — that's fine
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,47 +1,74 @@
|
||||||
// Hijri month names in three forms.
|
// Hijri month names in three forms.
|
||||||
// Index 0 = Muharram (month 1), index 11 = Dhul Hijjah (month 12).
|
// Index 0 = Muharram (month 1), index 11 = Dhul Hijjah (month 12).
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Full English transliterations of the 12 Hijri month names.
|
||||||
|
*
|
||||||
|
* Index 0 corresponds to Muharram (month 1); index 11 to Dhul Hijjah (month 12).
|
||||||
|
* Suitable for display in contexts where the full name aids readability.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const month = hmLong[hijriDate.hm - 1]; // "Ramadan"
|
||||||
|
*/
|
||||||
export const hmLong = [
|
export const hmLong = [
|
||||||
'Muharram', // 1
|
"Muharram", // 1
|
||||||
'Safar', // 2
|
"Safar", // 2
|
||||||
"Rabi'l Awwal", // 3
|
"Rabi'l Awwal", // 3
|
||||||
"Rabi'l Thani", // 4
|
"Rabi'l Thani", // 4
|
||||||
'Jumadal Awwal', // 5
|
"Jumadal Awwal", // 5
|
||||||
'Jumadal Thani', // 6
|
"Jumadal Thani", // 6
|
||||||
'Rajab', // 7
|
"Rajab", // 7
|
||||||
"Sha'ban", // 8
|
"Sha'ban", // 8
|
||||||
'Ramadan', // 9
|
"Ramadan", // 9
|
||||||
'Shawwal', // 10
|
"Shawwal", // 10
|
||||||
"Dhul Qi'dah", // 11
|
"Dhul Qi'dah", // 11
|
||||||
'Dhul Hijjah', // 12
|
"Dhul Hijjah", // 12
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Medium-length transliterations of the 12 Hijri month names.
|
||||||
|
*
|
||||||
|
* Shorter than {@link hmLong} but more readable than {@link hmShort}.
|
||||||
|
* Useful for compact date labels where space is limited.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const label = hmMedium[hijriDate.hm - 1]; // "Ramadan"
|
||||||
|
*/
|
||||||
export const hmMedium = [
|
export const hmMedium = [
|
||||||
'Muharram',
|
"Muharram",
|
||||||
'Safar',
|
"Safar",
|
||||||
'Rabi1',
|
"Rabi1",
|
||||||
'Rabi2',
|
"Rabi2",
|
||||||
'Jumada1',
|
"Jumada1",
|
||||||
'Jumada2',
|
"Jumada2",
|
||||||
'Rajab',
|
"Rajab",
|
||||||
'Shaban',
|
"Shaban",
|
||||||
'Ramadan',
|
"Ramadan",
|
||||||
'Shawwal',
|
"Shawwal",
|
||||||
'Dhul-Qidah',
|
"Dhul-Qidah",
|
||||||
'Dhul-Hijjah',
|
"Dhul-Hijjah",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Three-character short codes for the 12 Hijri months.
|
||||||
|
*
|
||||||
|
* Designed for narrow columns such as calendar grids or spreadsheet headers.
|
||||||
|
* Each code is exactly 3 ASCII characters.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const abbr = hmShort[hijriDate.hm - 1]; // "Ram"
|
||||||
|
*/
|
||||||
export const hmShort = [
|
export const hmShort = [
|
||||||
'Muh',
|
"Muh",
|
||||||
'Saf',
|
"Saf",
|
||||||
'Ra1',
|
"Ra1",
|
||||||
'Ra2',
|
"Ra2",
|
||||||
'Ju1',
|
"Ju1",
|
||||||
'Ju2',
|
"Ju2",
|
||||||
'Raj',
|
"Raj",
|
||||||
'Shb',
|
"Shb",
|
||||||
'Ram',
|
"Ram",
|
||||||
'Shw',
|
"Shw",
|
||||||
'DhQ',
|
"DhQ",
|
||||||
'DhH',
|
"DhH",
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,50 @@
|
||||||
// Hijri weekday names.
|
// Hijri weekday names.
|
||||||
// Index 0 = Sunday, index 6 = Saturday (matching JS Date.getDay()).
|
// Index 0 = Sunday, index 6 = Saturday (matching JS Date.getDay()).
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Full Arabic-transliterated names for the seven days of the week.
|
||||||
|
*
|
||||||
|
* Index alignment matches `Date.prototype.getDay()`:
|
||||||
|
* index 0 = Sunday, index 6 = Saturday.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const dayName = hwLong[gregorianDate.getDay()]; // "Yawm al-Jum`a"
|
||||||
|
*/
|
||||||
export const hwLong = [
|
export const hwLong = [
|
||||||
'Yawm al-Ahad', // Sunday
|
"Yawm al-Ahad", // Sunday
|
||||||
'Yawm al-Ithnayn', // Monday
|
"Yawm al-Ithnayn", // Monday
|
||||||
"Yawm ath-Thulatha'", // Tuesday
|
"Yawm ath-Thulatha'", // Tuesday
|
||||||
"Yawm al-Arba`a'", // Wednesday
|
"Yawm al-Arba`a'", // Wednesday
|
||||||
'Yawm al-Khamis', // Thursday
|
"Yawm al-Khamis", // Thursday
|
||||||
'Yawm al-Jum`a', // Friday
|
"Yawm al-Jum`a", // Friday
|
||||||
'Yawm as-Sabt', // Saturday
|
"Yawm as-Sabt", // Saturday
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Short single-word transliterations for the seven days of the week.
|
||||||
|
*
|
||||||
|
* Index alignment matches `Date.prototype.getDay()`:
|
||||||
|
* index 0 = Sunday, index 6 = Saturday.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const abbr = hwShort[gregorianDate.getDay()]; // "Jum`a"
|
||||||
|
*/
|
||||||
export const hwShort = [
|
export const hwShort = [
|
||||||
'Ahad', // Sunday
|
"Ahad", // Sunday
|
||||||
'Ithn', // Monday
|
"Ithn", // Monday
|
||||||
'Thul', // Tuesday
|
"Thul", // Tuesday
|
||||||
'Arba', // Wednesday
|
"Arba", // Wednesday
|
||||||
'Kham', // Thursday
|
"Kham", // Thursday
|
||||||
'Jum`a', // Friday
|
"Jum`a", // Friday
|
||||||
'Sabt', // Saturday
|
"Sabt", // Saturday
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Numeric weekday values: 1 = Sunday through 7 = Saturday.
|
||||||
|
*
|
||||||
|
* This follows the ISO 8601 convention where Monday = 1, but offset by one
|
||||||
|
* to match the Islamic numbering where Sunday is the first day of the week.
|
||||||
|
* Index alignment matches `Date.prototype.getDay()`.
|
||||||
|
*/
|
||||||
// Numeric representation: 1 = Sunday, 7 = Saturday.
|
// Numeric representation: 1 = Sunday, 7 = Saturday.
|
||||||
export const hwNumeric = [1, 2, 3, 4, 5, 6, 7];
|
export const hwNumeric = [1, 2, 3, 4, 5, 6, 7];
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import type { CalendarEngine } from './types';
|
import type { CalendarEngine } from "./types";
|
||||||
|
|
||||||
const _engines = new Map<string, CalendarEngine>();
|
const _engines = new Map<string, CalendarEngine>();
|
||||||
|
|
||||||
|
|
@ -25,7 +25,7 @@ export function registerCalendar(name: string, engine: CalendarEngine): void {
|
||||||
export function getCalendar(name: string): CalendarEngine {
|
export function getCalendar(name: string): CalendarEngine {
|
||||||
const engine = _engines.get(name);
|
const engine = _engines.get(name);
|
||||||
if (!engine) {
|
if (!engine) {
|
||||||
const available = listCalendars().join(', ');
|
const available = listCalendars().join(", ");
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Unknown Hijri calendar: "${name}". Available: ${available}. Register custom calendars with registerCalendar().`,
|
`Unknown Hijri calendar: "${name}". Available: ${available}. Register custom calendars with registerCalendar().`,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
36
src/types.ts
36
src/types.ts
|
|
@ -1,18 +1,44 @@
|
||||||
|
/**
|
||||||
|
* A Hijri date triple.
|
||||||
|
*
|
||||||
|
* All three fields are required. Month and day are 1-based.
|
||||||
|
* The year is a Hijri (AH) year number, e.g. 1446.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const d: HijriDate = { hy: 1446, hm: 9, hd: 1 }; // 1 Ramadan 1446 AH
|
||||||
|
*/
|
||||||
export interface HijriDate {
|
export interface HijriDate {
|
||||||
hy: number; // Hijri year
|
hy: number; // Hijri year
|
||||||
hm: number; // Hijri month (1-12)
|
hm: number; // Hijri month (1-12)
|
||||||
hd: number; // Hijri day (1-30)
|
hd: number; // Hijri day (1-30)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One row in the Umm al-Qura reference table.
|
||||||
|
*
|
||||||
|
* The table covers Hijri years 1318-1500 (Gregorian 1900-2076). A sentinel row
|
||||||
|
* at hy=1501 with dpm=0 marks the upper boundary and is used to detect
|
||||||
|
* out-of-range inputs without a separate bounds check.
|
||||||
|
*
|
||||||
|
* The `dpm` bitmask encodes month lengths for all 12 months:
|
||||||
|
* bit i (0-indexed from bit 0) = month i+1; 1 = 30 days, 0 = 29 days.
|
||||||
|
*/
|
||||||
export interface HijriYearRecord {
|
export interface HijriYearRecord {
|
||||||
hy: number; // Hijri year
|
hy: number; // Hijri year
|
||||||
dpm: number; // days-per-month bitmask (bit 0 = month 1: 1 -> 30 days, 0 -> 29 days)
|
dpm: number; // 12-bit days-per-month bitmask (bit 0 = month 1: 1 -> 30 days, 0 -> 29 days)
|
||||||
gy: number; // Gregorian year of 1 Muharram
|
gy: number; // Gregorian year of 1 Muharram
|
||||||
gm: number; // Gregorian month of 1 Muharram (1-based)
|
gm: number; // Gregorian month of 1 Muharram (1-based)
|
||||||
gd: number; // Gregorian day of 1 Muharram
|
gd: number; // Gregorian day of 1 Muharram
|
||||||
}
|
}
|
||||||
|
|
||||||
// Any calendar engine must implement this interface.
|
/**
|
||||||
|
* Interface every calendar engine must implement.
|
||||||
|
*
|
||||||
|
* Return `null` when a date is outside the engine's supported range.
|
||||||
|
* Throw `Error` for structurally invalid input (malformed Date, month outside 1-12, etc.).
|
||||||
|
* Never throw for out-of-range inputs — return `null` instead so callers can handle
|
||||||
|
* the boundary gracefully without try/catch.
|
||||||
|
*/
|
||||||
export interface CalendarEngine {
|
export interface CalendarEngine {
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
toHijri(date: Date): HijriDate | null;
|
toHijri(date: Date): HijriDate | null;
|
||||||
|
|
@ -22,6 +48,12 @@ export interface CalendarEngine {
|
||||||
daysInMonth(hy: number, hm: number): number;
|
daysInMonth(hy: number, hm: number): number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options accepted by the convenience conversion functions.
|
||||||
|
*
|
||||||
|
* Omitting `calendar` defaults to `'uaq'` (Umm al-Qura).
|
||||||
|
* Pass any name previously given to {@link registerCalendar} to use a custom engine.
|
||||||
|
*/
|
||||||
export interface ConversionOptions {
|
export interface ConversionOptions {
|
||||||
calendar?: string; // defaults to 'uaq'
|
calendar?: string; // defaults to 'uaq'
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ describe('CJS UAQ conversions', () => {
|
||||||
assert.equal(d.toISOString().slice(0, 10), '2025-03-01');
|
assert.equal(d.toISOString().slice(0, 10), '2025-03-01');
|
||||||
});
|
});
|
||||||
it('toHijri: 2023-03-23 = 1444/9/1', () => {
|
it('toHijri: 2023-03-23 = 1444/9/1', () => {
|
||||||
const h = toHijri(new Date(2023, 2, 23, 12));
|
const h = toHijri(new Date(Date.UTC(2023, 2, 23, 12)));
|
||||||
assert.ok(h !== null);
|
assert.ok(h !== null);
|
||||||
assert.equal(h.hy, 1444);
|
assert.equal(h.hy, 1444);
|
||||||
assert.equal(h.hm, 9);
|
assert.equal(h.hm, 9);
|
||||||
|
|
|
||||||
13
test.mjs
13
test.mjs
|
|
@ -97,19 +97,28 @@ describe('UAQ toGregorian', () => {
|
||||||
|
|
||||||
describe('UAQ toHijri', () => {
|
describe('UAQ toHijri', () => {
|
||||||
it('2023-03-23 = 1444/9/1', () => {
|
it('2023-03-23 = 1444/9/1', () => {
|
||||||
const h = toHijri(new Date(2023, 2, 23, 12));
|
const h = toHijri(new Date(Date.UTC(2023, 2, 23, 12)));
|
||||||
assert.ok(h !== null);
|
assert.ok(h !== null);
|
||||||
assert.equal(h.hy, 1444);
|
assert.equal(h.hy, 1444);
|
||||||
assert.equal(h.hm, 9);
|
assert.equal(h.hm, 9);
|
||||||
assert.equal(h.hd, 1);
|
assert.equal(h.hd, 1);
|
||||||
});
|
});
|
||||||
it('2025-03-01 = 1446/9/1', () => {
|
it('2025-03-01 = 1446/9/1', () => {
|
||||||
const h = toHijri(new Date(2025, 2, 1, 12));
|
const h = toHijri(new Date(Date.UTC(2025, 2, 1, 12)));
|
||||||
assert.ok(h !== null);
|
assert.ok(h !== null);
|
||||||
assert.equal(h.hy, 1446);
|
assert.equal(h.hy, 1446);
|
||||||
assert.equal(h.hm, 9);
|
assert.equal(h.hm, 9);
|
||||||
assert.equal(h.hd, 1);
|
assert.equal(h.hd, 1);
|
||||||
});
|
});
|
||||||
|
it('UAQ round-trip: toHijri(toGregorian(1446, 9, 1)) = 1446/9/1', () => {
|
||||||
|
const greg = toGregorian(1446, 9, 1);
|
||||||
|
assert.ok(greg instanceof Date);
|
||||||
|
const hijri = toHijri(greg);
|
||||||
|
assert.ok(hijri !== null);
|
||||||
|
assert.equal(hijri.hy, 1446);
|
||||||
|
assert.equal(hijri.hm, 9);
|
||||||
|
assert.equal(hijri.hd, 1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ─── UAQ isValidHijriDate ───────────────────────────────────────────────────
|
// ─── UAQ isValidHijriDate ───────────────────────────────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,9 @@
|
||||||
{
|
{
|
||||||
|
"extends": "@acamarata/tsconfig/tsconfig.library.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2020",
|
|
||||||
"module": "ESNext",
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"strict": true,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"declaration": true,
|
|
||||||
"declarationMap": true,
|
|
||||||
"sourceMap": true,
|
|
||||||
"noImplicitReturns": true,
|
"noImplicitReturns": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"outDir": "dist",
|
"esModuleInterop": true,
|
||||||
"rootDir": "src",
|
"rootDir": "src",
|
||||||
"types": ["node"]
|
"types": ["node"]
|
||||||
},
|
},
|
||||||
|
|
|
||||||
10
typedoc.json
Normal file
10
typedoc.json
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"entryPoints": ["src/index.ts"],
|
||||||
|
"out": ".github/wiki/api",
|
||||||
|
"plugin": ["typedoc-plugin-markdown"],
|
||||||
|
"readme": "none",
|
||||||
|
"skipErrorChecking": true,
|
||||||
|
"excludePrivate": true,
|
||||||
|
"excludeProtected": true,
|
||||||
|
"includeVersion": true
|
||||||
|
}
|
||||||
9
vitest.config.ts
Normal file
9
vitest.config.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { defineConfig } from "vitest/config";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
globals: true,
|
||||||
|
environment: "node",
|
||||||
|
include: ["hijri-core.test.ts"],
|
||||||
|
},
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue