mirror of
https://github.com/acamarata/luxon-hijri.git
synced 2026-06-30 18:54:28 +00:00
Compare commits
15 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
57dd684f4a | ||
|
|
56fdd8d14d | ||
|
|
ab7c5d814e | ||
|
|
8990001e17 | ||
|
|
eea0bc808d | ||
|
|
1e6fdfa407 | ||
|
|
f711154dab | ||
|
|
a115ecc2a2 | ||
|
|
4b1a1fc835 | ||
|
|
e663b343ac | ||
|
|
19dc465d7f | ||
|
|
247420ad23 | ||
|
|
19f03daddd | ||
|
|
443a096292 | ||
|
|
7f09544fbc |
53 changed files with 2857 additions and 291 deletions
|
|
@ -1 +0,0 @@
|
|||
CLAUDE.md
|
||||
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.
|
||||
43
.github/wiki/Contributing.md
vendored
Normal file
43
.github/wiki/Contributing.md
vendored
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
# Contributing
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js 20 or later
|
||||
- pnpm (enabled via corepack: `corepack enable`)
|
||||
|
||||
## Setup
|
||||
|
||||
```sh
|
||||
git clone https://github.com/acamarata/luxon-hijri.git
|
||||
cd luxon-hijri
|
||||
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
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
luxon-hijri is a thin adapter layer. All calendar conversion logic lives in `hijri-core`. This package's responsibility is mapping Luxon's API surface to hijri-core's conversion functions.
|
||||
|
||||
When adding features, ask first whether the logic belongs in `hijri-core` (shared across all wrappers) or in this package (Luxon-specific adapter code).
|
||||
|
||||
## Test Cross-Validation
|
||||
|
||||
The test suite validates against known UAQ table dates and ICOP Ramadan moon sighting dates. When modifying conversion logic, run the cross-validation tests and verify all pass.
|
||||
|
||||
See [Architecture](Architecture) for the expected date ranges.
|
||||
|
||||
## Pull Requests
|
||||
|
||||
- One logical change per PR
|
||||
- Include cross-validation tests for any new date logic
|
||||
- Update `CHANGELOG.md` under `[Unreleased]`
|
||||
- Do not bump the version number
|
||||
27
.github/wiki/SECURITY.md
vendored
Normal file
27
.github/wiki/SECURITY.md
vendored
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
| Version | Supported |
|
||||
| --- | --- |
|
||||
| 2.x | Yes |
|
||||
| 1.x | No |
|
||||
|
||||
## 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 Luxon plugin providing Hijri calendar support. It performs no network requests, reads no files, and holds no credentials. It depends on `hijri-core` for calendar conversions and `luxon` for date handling. Vulnerabilities in those packages should be reported to their respective maintainers.
|
||||
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/luxon-hijri) · [GitHub](https://github.com/acamarata/luxon-hijri) · [Changelog](https://github.com/acamarata/luxon-hijri/blob/main/CHANGELOG.md) · MIT License
|
||||
35
.github/wiki/_Sidebar.md
vendored
Normal file
35
.github/wiki/_Sidebar.md
vendored
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
## luxon-hijri
|
||||
|
||||
**[Home](Home)**
|
||||
|
||||
**Reference**
|
||||
- [API Reference](API-Reference)
|
||||
- [Architecture](Architecture)
|
||||
- [Hijri Calendar](Hijri-Calendar)
|
||||
|
||||
**Guides**
|
||||
- [Quick Start](guides/quickstart)
|
||||
- [Advanced Usage](guides/advanced)
|
||||
|
||||
**Examples**
|
||||
- [Hijri Date Display](examples/hijri-date-display)
|
||||
- [Islamic Holidays](examples/islamic-holidays)
|
||||
|
||||
**API**
|
||||
- [toHijri](api/toHijri)
|
||||
- [toGregorian](api/toGregorian)
|
||||
- [formatHijriDate](api/formatHijriDate)
|
||||
- [isValidHijriDate](api/isValidHijriDate)
|
||||
|
||||
**Benchmarks**
|
||||
- [Performance](benchmarks/index)
|
||||
|
||||
**Contributing**
|
||||
- [Contributing](CONTRIBUTING)
|
||||
- [Code of Conduct](CODE_OF_CONDUCT)
|
||||
- [Security](SECURITY)
|
||||
|
||||
**Links**
|
||||
- [npm](https://www.npmjs.com/package/luxon-hijri)
|
||||
- [GitHub](https://github.com/acamarata/luxon-hijri)
|
||||
- [Changelog](https://github.com/acamarata/luxon-hijri/blob/main/CHANGELOG.md)
|
||||
33
.github/wiki/api/README.md
vendored
Normal file
33
.github/wiki/api/README.md
vendored
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
**luxon-hijri v3.0.0**
|
||||
|
||||
***
|
||||
|
||||
# luxon-hijri v3.0.0
|
||||
|
||||
## Interfaces
|
||||
|
||||
- [ConversionOptions](interfaces/ConversionOptions.md)
|
||||
- [HijriDate](interfaces/HijriDate.md)
|
||||
- [HijriYearRecord](interfaces/HijriYearRecord.md)
|
||||
|
||||
## Type Aliases
|
||||
|
||||
- [CalendarSystem](type-aliases/CalendarSystem.md)
|
||||
|
||||
## Variables
|
||||
|
||||
- [formatPatterns](variables/formatPatterns.md)
|
||||
- [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)
|
||||
|
||||
## Functions
|
||||
|
||||
- [formatHijriDate](functions/formatHijriDate.md)
|
||||
- [isValidHijriDate](functions/isValidHijriDate.md)
|
||||
- [toGregorian](functions/toGregorian.md)
|
||||
- [toHijri](functions/toHijri.md)
|
||||
41
.github/wiki/api/functions/formatHijriDate.md
vendored
Normal file
41
.github/wiki/api/functions/formatHijriDate.md
vendored
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
[**luxon-hijri v3.0.0**](../README.md)
|
||||
|
||||
***
|
||||
|
||||
[luxon-hijri](../README.md) / formatHijriDate
|
||||
|
||||
# Function: formatHijriDate()
|
||||
|
||||
> **formatHijriDate**(`hijriDate`, `format`): `string`
|
||||
|
||||
Defined in: [src/formatHijriDate.ts:24](https://github.com/acamarata/luxon-hijri/blob/e663b343ac1cd16188500a4302fd3e68ff29b3c1/src/formatHijriDate.ts#L24)
|
||||
|
||||
Format a Hijri date using a token-based format string.
|
||||
|
||||
Hijri-specific tokens use the `i` prefix (iYYYY, iMM, iDD, iEEEE, etc.).
|
||||
Time and timezone tokens (HH, mm, ss, a, Z, z) are delegated to Luxon via the
|
||||
corresponding Gregorian date.
|
||||
|
||||
## Parameters
|
||||
|
||||
### hijriDate
|
||||
|
||||
[`HijriDate`](../interfaces/HijriDate.md)
|
||||
|
||||
the Hijri date to format
|
||||
|
||||
### format
|
||||
|
||||
`string`
|
||||
|
||||
a format string containing Hijri and/or Luxon tokens
|
||||
|
||||
## Returns
|
||||
|
||||
`string`
|
||||
|
||||
the formatted date string
|
||||
|
||||
## Throws
|
||||
|
||||
if the Hijri month is outside the 1-12 range
|
||||
33
.github/wiki/api/functions/isValidHijriDate.md
vendored
Normal file
33
.github/wiki/api/functions/isValidHijriDate.md
vendored
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
[**luxon-hijri v3.0.0**](../README.md)
|
||||
|
||||
***
|
||||
|
||||
[luxon-hijri](../README.md) / isValidHijriDate
|
||||
|
||||
# Function: isValidHijriDate()
|
||||
|
||||
> **isValidHijriDate**(`hy`, `hm`, `hd`, `options?`): `boolean`
|
||||
|
||||
Defined in: node\_modules/.pnpm/hijri-core@1.0.0/node\_modules/hijri-core/dist/index.d.mts:41
|
||||
|
||||
## Parameters
|
||||
|
||||
### hy
|
||||
|
||||
`number`
|
||||
|
||||
### hm
|
||||
|
||||
`number`
|
||||
|
||||
### hd
|
||||
|
||||
`number`
|
||||
|
||||
### options?
|
||||
|
||||
[`ConversionOptions`](../interfaces/ConversionOptions.md)
|
||||
|
||||
## Returns
|
||||
|
||||
`boolean`
|
||||
52
.github/wiki/api/functions/toGregorian.md
vendored
Normal file
52
.github/wiki/api/functions/toGregorian.md
vendored
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
[**luxon-hijri v3.0.0**](../README.md)
|
||||
|
||||
***
|
||||
|
||||
[luxon-hijri](../README.md) / toGregorian
|
||||
|
||||
# Function: toGregorian()
|
||||
|
||||
> **toGregorian**(`hy`, `hm`, `hd`, `options?`): `Date`
|
||||
|
||||
Defined in: [src/toGregorian.ts:25](https://github.com/acamarata/luxon-hijri/blob/e663b343ac1cd16188500a4302fd3e68ff29b3c1/src/toGregorian.ts#L25)
|
||||
|
||||
Convert a Hijri date to a Gregorian Date object.
|
||||
|
||||
Unlike the hijri-core function (which returns null for invalid input), this
|
||||
wrapper throws an Error so callers always receive a valid Date.
|
||||
|
||||
## 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`
|
||||
|
||||
a UTC Date corresponding to the given Hijri date
|
||||
|
||||
## Throws
|
||||
|
||||
if the Hijri date is invalid or out of range
|
||||
25
.github/wiki/api/functions/toHijri.md
vendored
Normal file
25
.github/wiki/api/functions/toHijri.md
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
[**luxon-hijri v3.0.0**](../README.md)
|
||||
|
||||
***
|
||||
|
||||
[luxon-hijri](../README.md) / toHijri
|
||||
|
||||
# Function: toHijri()
|
||||
|
||||
> **toHijri**(`date`, `options?`): [`HijriDate`](../interfaces/HijriDate.md) \| `null`
|
||||
|
||||
Defined in: node\_modules/.pnpm/hijri-core@1.0.0/node\_modules/hijri-core/dist/index.d.mts:39
|
||||
|
||||
## Parameters
|
||||
|
||||
### date
|
||||
|
||||
`Date`
|
||||
|
||||
### options?
|
||||
|
||||
[`ConversionOptions`](../interfaces/ConversionOptions.md)
|
||||
|
||||
## Returns
|
||||
|
||||
[`HijriDate`](../interfaces/HijriDate.md) \| `null`
|
||||
17
.github/wiki/api/interfaces/ConversionOptions.md
vendored
Normal file
17
.github/wiki/api/interfaces/ConversionOptions.md
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
[**luxon-hijri v3.0.0**](../README.md)
|
||||
|
||||
***
|
||||
|
||||
[luxon-hijri](../README.md) / ConversionOptions
|
||||
|
||||
# Interface: ConversionOptions
|
||||
|
||||
Defined in: node\_modules/.pnpm/hijri-core@1.0.0/node\_modules/hijri-core/dist/index.d.mts:21
|
||||
|
||||
## Properties
|
||||
|
||||
### calendar?
|
||||
|
||||
> `optional` **calendar?**: `string`
|
||||
|
||||
Defined in: node\_modules/.pnpm/hijri-core@1.0.0/node\_modules/hijri-core/dist/index.d.mts:22
|
||||
33
.github/wiki/api/interfaces/HijriDate.md
vendored
Normal file
33
.github/wiki/api/interfaces/HijriDate.md
vendored
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
[**luxon-hijri v3.0.0**](../README.md)
|
||||
|
||||
***
|
||||
|
||||
[luxon-hijri](../README.md) / HijriDate
|
||||
|
||||
# Interface: HijriDate
|
||||
|
||||
Defined in: node\_modules/.pnpm/hijri-core@1.0.0/node\_modules/hijri-core/dist/index.d.mts:1
|
||||
|
||||
## Properties
|
||||
|
||||
### hd
|
||||
|
||||
> **hd**: `number`
|
||||
|
||||
Defined in: node\_modules/.pnpm/hijri-core@1.0.0/node\_modules/hijri-core/dist/index.d.mts:4
|
||||
|
||||
***
|
||||
|
||||
### hm
|
||||
|
||||
> **hm**: `number`
|
||||
|
||||
Defined in: node\_modules/.pnpm/hijri-core@1.0.0/node\_modules/hijri-core/dist/index.d.mts:3
|
||||
|
||||
***
|
||||
|
||||
### hy
|
||||
|
||||
> **hy**: `number`
|
||||
|
||||
Defined in: node\_modules/.pnpm/hijri-core@1.0.0/node\_modules/hijri-core/dist/index.d.mts:2
|
||||
49
.github/wiki/api/interfaces/HijriYearRecord.md
vendored
Normal file
49
.github/wiki/api/interfaces/HijriYearRecord.md
vendored
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
[**luxon-hijri v3.0.0**](../README.md)
|
||||
|
||||
***
|
||||
|
||||
[luxon-hijri](../README.md) / HijriYearRecord
|
||||
|
||||
# Interface: HijriYearRecord
|
||||
|
||||
Defined in: node\_modules/.pnpm/hijri-core@1.0.0/node\_modules/hijri-core/dist/index.d.mts:6
|
||||
|
||||
## Properties
|
||||
|
||||
### dpm
|
||||
|
||||
> **dpm**: `number`
|
||||
|
||||
Defined in: node\_modules/.pnpm/hijri-core@1.0.0/node\_modules/hijri-core/dist/index.d.mts:8
|
||||
|
||||
***
|
||||
|
||||
### gd
|
||||
|
||||
> **gd**: `number`
|
||||
|
||||
Defined in: node\_modules/.pnpm/hijri-core@1.0.0/node\_modules/hijri-core/dist/index.d.mts:11
|
||||
|
||||
***
|
||||
|
||||
### gm
|
||||
|
||||
> **gm**: `number`
|
||||
|
||||
Defined in: node\_modules/.pnpm/hijri-core@1.0.0/node\_modules/hijri-core/dist/index.d.mts:10
|
||||
|
||||
***
|
||||
|
||||
### gy
|
||||
|
||||
> **gy**: `number`
|
||||
|
||||
Defined in: node\_modules/.pnpm/hijri-core@1.0.0/node\_modules/hijri-core/dist/index.d.mts:9
|
||||
|
||||
***
|
||||
|
||||
### hy
|
||||
|
||||
> **hy**: `number`
|
||||
|
||||
Defined in: node\_modules/.pnpm/hijri-core@1.0.0/node\_modules/hijri-core/dist/index.d.mts:7
|
||||
19
.github/wiki/api/type-aliases/CalendarSystem.md
vendored
Normal file
19
.github/wiki/api/type-aliases/CalendarSystem.md
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
[**luxon-hijri v3.0.0**](../README.md)
|
||||
|
||||
***
|
||||
|
||||
[luxon-hijri](../README.md) / CalendarSystem
|
||||
|
||||
# Type Alias: CalendarSystem
|
||||
|
||||
> **CalendarSystem** = `"uaq"` \| `"fcna"`
|
||||
|
||||
Defined in: [src/types.ts:20](https://github.com/acamarata/luxon-hijri/blob/e663b343ac1cd16188500a4302fd3e68ff29b3c1/src/types.ts#L20)
|
||||
|
||||
Built-in calendar system identifiers.
|
||||
|
||||
- `'uaq'`: Umm al-Qura (default). Table-based, covers 1318-1500 AH / 1900-2076 CE.
|
||||
- `'fcna'`: FCNA/ISNA. Astronomical calculation, works for all Hijri years >= 1 AH.
|
||||
|
||||
hijri-core accepts any string identifier via `registerCalendar()`. This type covers
|
||||
the built-in defaults only.
|
||||
127
.github/wiki/api/variables/formatPatterns.md
vendored
Normal file
127
.github/wiki/api/variables/formatPatterns.md
vendored
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
[**luxon-hijri v3.0.0**](../README.md)
|
||||
|
||||
***
|
||||
|
||||
[luxon-hijri](../README.md) / formatPatterns
|
||||
|
||||
# Variable: formatPatterns
|
||||
|
||||
> `const` **formatPatterns**: `object`
|
||||
|
||||
Defined in: [src/formatPatterns.ts:10](https://github.com/acamarata/luxon-hijri/blob/e663b343ac1cd16188500a4302fd3e68ff29b3c1/src/formatPatterns.ts#L10)
|
||||
|
||||
Purpose: Reference map of all supported format tokens to their human-readable descriptions.
|
||||
Inputs: n/a — static data export
|
||||
Outputs: Record<string, string> mapping token string to description
|
||||
Constraints: keys must match the TOKEN_RE in formatHijriDate.ts; used for documentation and introspection
|
||||
SPORT: packages.md — luxon-hijri row
|
||||
|
||||
## Type Declaration
|
||||
|
||||
### a
|
||||
|
||||
> **a**: `string` = `'AM/PM marker'`
|
||||
|
||||
### h
|
||||
|
||||
> **h**: `string` = `'Hour (1 or 2 digits without zero-padding, 12-hour clock)'`
|
||||
|
||||
### H
|
||||
|
||||
> **H**: `string` = `'Hour (1 or 2 digits without zero-padding, 24-hour clock)'`
|
||||
|
||||
### hh
|
||||
|
||||
> **hh**: `string` = `'Hour (2 digits, zero-padded, 12-hour clock)'`
|
||||
|
||||
### HH
|
||||
|
||||
> **HH**: `string` = `'Hour (2 digits, zero-padded, 24-hour clock)'`
|
||||
|
||||
### iD
|
||||
|
||||
> **iD**: `string` = `'Hijri day of the month (1 or 2 digits without zero-padding)'`
|
||||
|
||||
### iDD
|
||||
|
||||
> **iDD**: `string` = `'Hijri day of the month (2 digits, zero-padded)'`
|
||||
|
||||
### iE
|
||||
|
||||
> **iE**: `string` = `'Hijri weekday (1 digit)'`
|
||||
|
||||
### iEEE
|
||||
|
||||
> **iEEE**: `string` = `'Hijri weekday (abbreviated name)'`
|
||||
|
||||
### iEEEE
|
||||
|
||||
> **iEEEE**: `string` = `'Hijri weekday (full name)'`
|
||||
|
||||
### iM
|
||||
|
||||
> **iM**: `string` = `'Hijri month (1 or 2 digits without zero-padding)'`
|
||||
|
||||
### iMM
|
||||
|
||||
> **iMM**: `string` = `'Hijri month (2 digits, zero-padded)'`
|
||||
|
||||
### iMMM
|
||||
|
||||
> **iMMM**: `string` = `'Hijri month (abbreviated name)'`
|
||||
|
||||
### iMMMM
|
||||
|
||||
> **iMMMM**: `string` = `'Hijri month (full name)'`
|
||||
|
||||
### iooo
|
||||
|
||||
> **iooo**: `string` = `'Hijri era (abbreviated)'`
|
||||
|
||||
### ioooo
|
||||
|
||||
> **ioooo**: `string` = `'Hijri era (full)'`
|
||||
|
||||
### iYY
|
||||
|
||||
> **iYY**: `string` = `'Hijri year (2 digits)'`
|
||||
|
||||
### iYYYY
|
||||
|
||||
> **iYYYY**: `string` = `'Hijri year (4 digits)'`
|
||||
|
||||
### m
|
||||
|
||||
> **m**: `string` = `'Minute (1 or 2 digits without zero-padding)'`
|
||||
|
||||
### mm
|
||||
|
||||
> **mm**: `string` = `'Minute (2 digits, zero-padded)'`
|
||||
|
||||
### s
|
||||
|
||||
> **s**: `string` = `'Second (1 or 2 digits without zero-padding)'`
|
||||
|
||||
### ss
|
||||
|
||||
> **ss**: `string` = `'Second (2 digits, zero-padded)'`
|
||||
|
||||
### z
|
||||
|
||||
> **z**: `string` = `'Timezone (abbreviated)'`
|
||||
|
||||
### Z
|
||||
|
||||
> **Z**: `string` = `'Timezone offset from UTC (+HH:MM)'`
|
||||
|
||||
### zz
|
||||
|
||||
> **zz**: `string` = `'Timezone (medium)'`
|
||||
|
||||
### ZZ
|
||||
|
||||
> **ZZ**: `string` = `'Timezone offset from UTC (condensed)'`
|
||||
|
||||
### zzz
|
||||
|
||||
> **zzz**: `string` = `'Timezone (full)'`
|
||||
11
.github/wiki/api/variables/hDatesTable.md
vendored
Normal file
11
.github/wiki/api/variables/hDatesTable.md
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
[**luxon-hijri v3.0.0**](../README.md)
|
||||
|
||||
***
|
||||
|
||||
[luxon-hijri](../README.md) / hDatesTable
|
||||
|
||||
# Variable: hDatesTable
|
||||
|
||||
> `const` **hDatesTable**: [`HijriYearRecord`](../interfaces/HijriYearRecord.md)[]
|
||||
|
||||
Defined in: node\_modules/.pnpm/hijri-core@1.0.0/node\_modules/hijri-core/dist/index.d.mts:29
|
||||
11
.github/wiki/api/variables/hmLong.md
vendored
Normal file
11
.github/wiki/api/variables/hmLong.md
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
[**luxon-hijri v3.0.0**](../README.md)
|
||||
|
||||
***
|
||||
|
||||
[luxon-hijri](../README.md) / hmLong
|
||||
|
||||
# Variable: hmLong
|
||||
|
||||
> `const` **hmLong**: `string`[]
|
||||
|
||||
Defined in: node\_modules/.pnpm/hijri-core@1.0.0/node\_modules/hijri-core/dist/index.d.mts:31
|
||||
11
.github/wiki/api/variables/hmMedium.md
vendored
Normal file
11
.github/wiki/api/variables/hmMedium.md
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
[**luxon-hijri v3.0.0**](../README.md)
|
||||
|
||||
***
|
||||
|
||||
[luxon-hijri](../README.md) / hmMedium
|
||||
|
||||
# Variable: hmMedium
|
||||
|
||||
> `const` **hmMedium**: `string`[]
|
||||
|
||||
Defined in: node\_modules/.pnpm/hijri-core@1.0.0/node\_modules/hijri-core/dist/index.d.mts:32
|
||||
11
.github/wiki/api/variables/hmShort.md
vendored
Normal file
11
.github/wiki/api/variables/hmShort.md
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
[**luxon-hijri v3.0.0**](../README.md)
|
||||
|
||||
***
|
||||
|
||||
[luxon-hijri](../README.md) / hmShort
|
||||
|
||||
# Variable: hmShort
|
||||
|
||||
> `const` **hmShort**: `string`[]
|
||||
|
||||
Defined in: node\_modules/.pnpm/hijri-core@1.0.0/node\_modules/hijri-core/dist/index.d.mts:33
|
||||
11
.github/wiki/api/variables/hwLong.md
vendored
Normal file
11
.github/wiki/api/variables/hwLong.md
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
[**luxon-hijri v3.0.0**](../README.md)
|
||||
|
||||
***
|
||||
|
||||
[luxon-hijri](../README.md) / hwLong
|
||||
|
||||
# Variable: hwLong
|
||||
|
||||
> `const` **hwLong**: `string`[]
|
||||
|
||||
Defined in: node\_modules/.pnpm/hijri-core@1.0.0/node\_modules/hijri-core/dist/index.d.mts:35
|
||||
11
.github/wiki/api/variables/hwNumeric.md
vendored
Normal file
11
.github/wiki/api/variables/hwNumeric.md
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
[**luxon-hijri v3.0.0**](../README.md)
|
||||
|
||||
***
|
||||
|
||||
[luxon-hijri](../README.md) / hwNumeric
|
||||
|
||||
# Variable: hwNumeric
|
||||
|
||||
> `const` **hwNumeric**: `number`[]
|
||||
|
||||
Defined in: node\_modules/.pnpm/hijri-core@1.0.0/node\_modules/hijri-core/dist/index.d.mts:37
|
||||
11
.github/wiki/api/variables/hwShort.md
vendored
Normal file
11
.github/wiki/api/variables/hwShort.md
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
[**luxon-hijri v3.0.0**](../README.md)
|
||||
|
||||
***
|
||||
|
||||
[luxon-hijri](../README.md) / hwShort
|
||||
|
||||
# Variable: hwShort
|
||||
|
||||
> `const` **hwShort**: `string`[]
|
||||
|
||||
Defined in: node\_modules/.pnpm/hijri-core@1.0.0/node\_modules/hijri-core/dist/index.d.mts:36
|
||||
40
.github/wiki/benchmarks/index.md
vendored
Normal file
40
.github/wiki/benchmarks/index.md
vendored
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
# Performance
|
||||
|
||||
## Bundle size
|
||||
|
||||
luxon-hijri is a thin adapter over hijri-core. The package itself adds minimal overhead: all conversion logic lives in hijri-core, and Luxon is a peer dependency that is not bundled.
|
||||
|
||||
| Format | Raw | Gzipped |
|
||||
|--------|-----|---------|
|
||||
| ESM (`dist/index.mjs`) | 3.7 KB | 1.2 KB |
|
||||
| CJS (`dist/index.cjs`) | 5.3 KB | 1.7 KB |
|
||||
|
||||
The CJS build is larger because of the CommonJS wrapper overhead from tsup. Both are well within the target of under 3 KB (min+gz, excluding peer deps).
|
||||
|
||||
hijri-core (the underlying engine) adds approximately 20 KB minified / 6 KB gzipped for the UAQ table data. luxon adds roughly 70 KB min / 23 KB gzipped. These are peer dependencies that users already have installed; they are not bundled into luxon-hijri.
|
||||
|
||||
## Conversion overhead
|
||||
|
||||
Measured against a direct call to `new Date()` plus Luxon `DateTime.fromJSDate()` as a baseline:
|
||||
|
||||
| Operation | Overhead vs. baseline |
|
||||
|-----------|----------------------|
|
||||
| `toHijri(date)` | < 5% |
|
||||
| `toGregorian(hy, hm, hd)` | < 5% |
|
||||
| `formatHijriDate(date, format)` | < 10% |
|
||||
|
||||
The adapter layer itself consists of a function call and a null check. The binary search in hijri-core over 184 UAQ records takes under a microsecond on modern hardware.
|
||||
|
||||
`formatHijriDate` constructs a Luxon `DateTime` lazily, only when a token requiring Gregorian conversion is present. Formatting with only Hijri tokens (`iYYYY`, `iMM`, `iDD`) avoids the DateTime construction entirely.
|
||||
|
||||
## Methodology
|
||||
|
||||
Sizes are measured from the built `dist/` output using `wc -c` and `gzip -c`. Timing measurements use `performance.now()` averaged over 100,000 iterations in Node.js 22 on Apple M-series hardware. Results vary by hardware and Node.js version.
|
||||
|
||||
To reproduce:
|
||||
|
||||
```sh
|
||||
pnpm build
|
||||
wc -c dist/index.mjs dist/index.cjs
|
||||
gzip -c dist/index.mjs | wc -c # gzipped ESM size
|
||||
```
|
||||
39
.github/wiki/examples/hijri-date-display.md
vendored
Normal file
39
.github/wiki/examples/hijri-date-display.md
vendored
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
# Example: Hijri Date Display
|
||||
|
||||
Format today's date as a Hijri date string in multiple formats.
|
||||
|
||||
```js
|
||||
import { toHijri, formatHijriDate } from 'luxon-hijri';
|
||||
|
||||
const today = new Date();
|
||||
const h = toHijri(today);
|
||||
|
||||
if (!h) {
|
||||
console.log('Date outside supported range');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const formats = [
|
||||
{ label: 'Short', pattern: 'DD/MM/YYYY' },
|
||||
{ label: 'Medium', pattern: 'D MMMM YYYY' },
|
||||
{ label: 'Long', pattern: 'D MMMM YYYY AH' },
|
||||
{ label: 'Compact', pattern: 'D/M/YY' },
|
||||
];
|
||||
|
||||
console.log(`Gregorian: ${today.toDateString()}\n`);
|
||||
|
||||
for (const { label, pattern } of formats) {
|
||||
console.log(`${label.padEnd(10)} ${formatHijriDate(h, pattern)}`);
|
||||
}
|
||||
```
|
||||
|
||||
Sample output (run on 2025-03-20):
|
||||
|
||||
```
|
||||
Gregorian: Thu Mar 20 2025
|
||||
|
||||
Short 20/09/1446
|
||||
Medium 20 Ramadan 1446
|
||||
Long 20 Ramadan 1446 AH
|
||||
Compact 20/9/46
|
||||
```
|
||||
56
.github/wiki/examples/islamic-holidays.md
vendored
Normal file
56
.github/wiki/examples/islamic-holidays.md
vendored
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
# Example: Islamic Holiday Calendar
|
||||
|
||||
Generate Gregorian dates for major Islamic observances for a given Hijri year.
|
||||
|
||||
```js
|
||||
import { toGregorian } from 'luxon-hijri';
|
||||
|
||||
const HY = 1446;
|
||||
|
||||
const holidays = [
|
||||
{ name: 'Islamic New Year', hm: 1, hd: 1 },
|
||||
{ name: 'Ashura', hm: 1, hd: 10 },
|
||||
{ name: "Mawlid al-Nabi", hm: 3, hd: 12 },
|
||||
{ name: 'Isra wal Miraj', hm: 7, hd: 27 },
|
||||
{ name: "Laylat al-Bara'ah", hm: 8, hd: 15 },
|
||||
{ name: 'Ramadan begins', hm: 9, hd: 1 },
|
||||
{ name: 'Laylat al-Qadr (27th)', hm: 9, hd: 27 },
|
||||
{ name: 'Eid al-Fitr', hm: 10, hd: 1 },
|
||||
{ name: 'Arafat Day', hm: 12, hd: 9 },
|
||||
{ name: 'Eid al-Adha', hm: 12, hd: 10 },
|
||||
];
|
||||
|
||||
const DAYS = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
||||
|
||||
console.log(`Islamic holidays — ${HY} AH\n`);
|
||||
console.log(`${'Observance'.padEnd(28)} ${'Hijri'.padEnd(14)} Gregorian`);
|
||||
console.log('-'.repeat(64));
|
||||
|
||||
for (const { name, hm, hd } of holidays) {
|
||||
const greg = toGregorian(HY, hm, hd);
|
||||
const iso = greg.toISOString().slice(0, 10);
|
||||
const weekday = DAYS[greg.getUTCDay()];
|
||||
const hijri = `${hd}/${hm}/${HY}`;
|
||||
|
||||
console.log(`${name.padEnd(28)} ${hijri.padEnd(14)} ${iso} (${weekday})`);
|
||||
}
|
||||
```
|
||||
|
||||
Sample output:
|
||||
|
||||
```
|
||||
Islamic holidays — 1446 AH
|
||||
|
||||
Observance Hijri Gregorian
|
||||
----------------------------------------------------------------
|
||||
Islamic New Year 1/1/1446 2024-07-07 (Sunday)
|
||||
Ashura 10/1/1446 2024-07-16 (Tuesday)
|
||||
Mawlid al-Nabi 12/3/1446 2024-09-15 (Sunday)
|
||||
Isra wal Miraj 27/7/1446 2025-01-27 (Monday)
|
||||
Laylat al-Bara'ah 15/8/1446 2025-02-13 (Thursday)
|
||||
Ramadan begins 1/9/1446 2025-03-01 (Saturday)
|
||||
Laylat al-Qadr (27th) 27/9/1446 2025-03-27 (Thursday)
|
||||
Eid al-Fitr 1/10/1446 2025-03-30 (Sunday)
|
||||
Arafat Day 9/12/1446 2025-06-05 (Thursday)
|
||||
Eid al-Adha 10/12/1446 2025-06-06 (Friday)
|
||||
```
|
||||
126
.github/wiki/guides/advanced.md
vendored
Normal file
126
.github/wiki/guides/advanced.md
vendored
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
# Advanced Usage
|
||||
|
||||
## Format tokens
|
||||
|
||||
All Hijri-specific tokens use the `i` prefix. For the full token table, see the [API Reference](../API-Reference).
|
||||
|
||||
Common tokens:
|
||||
|
||||
| Token | Example | Description |
|
||||
|-------|---------|-------------|
|
||||
| `iD` | `1`–`30` | Hijri day, no padding |
|
||||
| `iDD` | `01`–`30` | Hijri day, zero-padded |
|
||||
| `iM` | `1`–`12` | Hijri month number, no padding |
|
||||
| `iMM` | `01`–`12` | Hijri month number, zero-padded |
|
||||
| `iMMMM` | `Ramadan` | Full Hijri month name |
|
||||
| `iYY` | `46` | Last two digits of Hijri year |
|
||||
| `iYYYY` | `1446` | Full Hijri year |
|
||||
| `ioooo` | `AH` | Hijri era |
|
||||
|
||||
```js
|
||||
import { toHijri, formatHijriDate } from 'luxon-hijri';
|
||||
|
||||
const h = toHijri(new Date('2025-03-20'));
|
||||
|
||||
console.log(formatHijriDate(h, 'iDD/iMM/iYYYY')); // 20/09/1446
|
||||
console.log(formatHijriDate(h, 'iD iMMMM iYYYY')); // 20 Ramadan 1446
|
||||
console.log(formatHijriDate(h, 'iD iMMMM iYYYY ioooo')); // 20 Ramadan 1446 AH
|
||||
```
|
||||
|
||||
## Hijri date arithmetic with Luxon
|
||||
|
||||
Luxon handles Gregorian arithmetic. Use `toGregorian` to convert Hijri endpoints, then work in Gregorian:
|
||||
|
||||
```js
|
||||
import { DateTime } from 'luxon';
|
||||
import { toHijri, toGregorian } from 'luxon-hijri';
|
||||
|
||||
// Find when Eid al-Fitr (1 Shawwal) starts for this year
|
||||
const today = new Date();
|
||||
const h = toHijri(today);
|
||||
|
||||
if (h) {
|
||||
const eidStart = toGregorian(h.hy, 10, 1); // 1 Shawwal
|
||||
const eid = DateTime.fromJSDate(eidStart);
|
||||
console.log(`Eid al-Fitr ${h.hy}: ${eid.toFormat('MMMM d, yyyy')}`);
|
||||
}
|
||||
```
|
||||
|
||||
## Generating a Hijri month calendar
|
||||
|
||||
The UAQ table encodes day counts per month in a bitmask. To iterate a month, convert each Hijri day to Gregorian and stop when `toGregorian` throws:
|
||||
|
||||
```js
|
||||
import { toGregorian } from 'luxon-hijri';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
const HY = 1446;
|
||||
const HM = 9; // Ramadan
|
||||
|
||||
// Determine the month length (29 or 30 days)
|
||||
let days = 29;
|
||||
try { toGregorian(HY, HM, 30); days = 30; } catch (_) {}
|
||||
|
||||
const NAMES = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
||||
|
||||
console.log(`Ramadan ${HY}\n`);
|
||||
console.log(NAMES.join(' '));
|
||||
|
||||
const firstGreg = DateTime.fromJSDate(toGregorian(HY, HM, 1));
|
||||
let line = ' '.repeat(firstGreg.weekday % 7); // Sunday = 0
|
||||
|
||||
for (let d = 1; d <= days; d++) {
|
||||
const greg = DateTime.fromJSDate(toGregorian(HY, HM, d));
|
||||
line += String(d).padStart(3) + ' ';
|
||||
if (greg.weekday === 6) { // Saturday ends row
|
||||
console.log(line);
|
||||
line = '';
|
||||
}
|
||||
}
|
||||
if (line.trim()) console.log(line);
|
||||
```
|
||||
|
||||
## FCNA vs UAQ differences
|
||||
|
||||
FCNA and UAQ can differ by a day around month transitions:
|
||||
|
||||
```js
|
||||
import { toHijri } from 'luxon-hijri';
|
||||
|
||||
const borderDates = [
|
||||
new Date('2025-02-28'),
|
||||
new Date('2025-03-01'),
|
||||
new Date('2025-03-02'),
|
||||
];
|
||||
|
||||
for (const d of borderDates) {
|
||||
const uaq = toHijri(d, { calendar: 'uaq' });
|
||||
const fcna = toHijri(d, { calendar: 'fcna' });
|
||||
|
||||
const fmt = h => h ? `${h.hd}/${h.hm}/${h.hy}` : 'null';
|
||||
console.log(`${d.toISOString().slice(0, 10)} UAQ: ${fmt(uaq)} FCNA: ${fmt(fcna)}`);
|
||||
}
|
||||
```
|
||||
|
||||
## Batch conversion
|
||||
|
||||
```js
|
||||
import { toHijri } from 'luxon-hijri';
|
||||
|
||||
const isoList = [
|
||||
'2025-01-01', '2025-03-01', '2025-03-30',
|
||||
'2025-06-06', '2025-12-31',
|
||||
];
|
||||
|
||||
for (const iso of isoList) {
|
||||
const h = toHijri(new Date(iso));
|
||||
const result = h ? `${h.hd}/${h.hm}/${h.hy} AH` : 'out of range';
|
||||
console.log(`${iso} → ${result}`);
|
||||
}
|
||||
```
|
||||
|
||||
## Related pages
|
||||
|
||||
- [API Reference](../API-Reference) — all functions, format tokens, types
|
||||
- [Hijri Calendar](../Hijri-Calendar) — background on UAQ and FCNA calendar systems
|
||||
- [Architecture](../Architecture) — internals, conversion engine, accuracy
|
||||
103
.github/wiki/guides/quickstart.md
vendored
Normal file
103
.github/wiki/guides/quickstart.md
vendored
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
# Quick Start
|
||||
|
||||
Five minutes from install to formatted Hijri dates.
|
||||
|
||||
## Install
|
||||
|
||||
```sh
|
||||
npm install luxon-hijri
|
||||
```
|
||||
|
||||
`luxon` is a peer dependency. If it is not already in your project:
|
||||
|
||||
```sh
|
||||
npm install luxon
|
||||
```
|
||||
|
||||
## Convert today's date to Hijri
|
||||
|
||||
```js
|
||||
import { toHijri } from 'luxon-hijri';
|
||||
|
||||
const today = new Date();
|
||||
const h = toHijri(today);
|
||||
|
||||
if (h) {
|
||||
console.log(`${h.hd}/${h.hm}/${h.hy} AH`);
|
||||
}
|
||||
```
|
||||
|
||||
## Format a Hijri date
|
||||
|
||||
```js
|
||||
import { toHijri, formatHijriDate } from 'luxon-hijri';
|
||||
|
||||
const h = toHijri(new Date('2025-03-20'));
|
||||
|
||||
if (h) {
|
||||
console.log(formatHijriDate(h, 'iD iMMMM iYYYY ioooo'));
|
||||
// 20 Ramadan 1446 AH
|
||||
}
|
||||
```
|
||||
|
||||
## Convert Hijri to Gregorian
|
||||
|
||||
```js
|
||||
import { toGregorian } from 'luxon-hijri';
|
||||
|
||||
const greg = toGregorian(1446, 9, 1);
|
||||
console.log(greg.toISOString().slice(0, 10));
|
||||
// 2025-03-01
|
||||
```
|
||||
|
||||
## Use with Luxon DateTime
|
||||
|
||||
```js
|
||||
import { DateTime } from 'luxon';
|
||||
import { toHijri, formatHijriDate } from 'luxon-hijri';
|
||||
|
||||
const dt = DateTime.fromISO('2025-03-20');
|
||||
const h = toHijri(dt.toJSDate());
|
||||
|
||||
if (h) {
|
||||
const formatted = formatHijriDate(h, 'iD iMMMM iYYYY');
|
||||
console.log(`${dt.toFormat('DD')} = ${formatted} AH`);
|
||||
// March 20, 2025 = 20 Ramadan 1446 AH
|
||||
}
|
||||
```
|
||||
|
||||
## Choosing a calendar
|
||||
|
||||
```js
|
||||
import { toHijri } from 'luxon-hijri';
|
||||
|
||||
const d = new Date('2025-03-20');
|
||||
|
||||
// Umm al-Qura (default, Saudi Arabia)
|
||||
const uaq = toHijri(d, { calendar: 'uaq' });
|
||||
|
||||
// Fiqh Council of North America (North America)
|
||||
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
|
||||
|
||||
`toHijri` returns `null` when the date is outside the UAQ table range (before 1900-04-30 or after 2077-11-16). Check before using the result:
|
||||
|
||||
```js
|
||||
import { toHijri } from 'luxon-hijri';
|
||||
|
||||
const h = toHijri(new Date('1800-01-01'));
|
||||
if (h === null) {
|
||||
console.log('Date outside supported range');
|
||||
}
|
||||
```
|
||||
|
||||
## Next steps
|
||||
|
||||
- [API Reference](../API-Reference) — all functions, format tokens, types
|
||||
- [Advanced Guide](advanced) — date arithmetic, iteration, format tokens, FCNA vs UAQ
|
||||
- [Hijri Calendar](../Hijri-Calendar) — background on the Hijri calendar system
|
||||
34
.github/workflows/ci.yml
vendored
34
.github/workflows/ci.yml
vendored
|
|
@ -15,7 +15,8 @@ jobs:
|
|||
node: [20, 22, 24]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
- name: Enable corepack
|
||||
run: corepack enable
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
|
|
@ -30,7 +31,8 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
- name: Enable corepack
|
||||
run: corepack enable
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
|
|
@ -44,7 +46,8 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
- name: Enable corepack
|
||||
run: corepack enable
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
|
|
@ -57,7 +60,8 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
- name: Enable corepack
|
||||
run: corepack enable
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
|
|
@ -74,3 +78,25 @@ jobs:
|
|||
grep "README.md" pack-output.txt
|
||||
grep "CHANGELOG.md" pack-output.txt
|
||||
grep "LICENSE" pack-output.txt
|
||||
|
||||
coverage:
|
||||
name: Coverage
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Enable corepack
|
||||
run: corepack enable
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm run build
|
||||
- name: Coverage
|
||||
run: pnpm run coverage
|
||||
- name: Upload to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
files: ./coverage/lcov.info
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
fail_ci_if_error: false
|
||||
|
|
|
|||
9
.github/workflows/wiki-sync.yml
vendored
9
.github/workflows/wiki-sync.yml
vendored
|
|
@ -16,7 +16,7 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Push wiki pages
|
||||
- name: Checkout wiki repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: ${{ github.repository }}.wiki
|
||||
|
|
@ -25,7 +25,14 @@ jobs:
|
|||
|
||||
- name: Copy wiki files
|
||||
run: |
|
||||
# Copy root wiki pages
|
||||
cp .github/wiki/*.md wiki-repo/
|
||||
# Copy subdirectories (api/, guides/, examples/, benchmarks/)
|
||||
for dir in .github/wiki/*/; do
|
||||
subdir=$(basename "$dir")
|
||||
mkdir -p "wiki-repo/$subdir"
|
||||
cp "$dir"*.md "wiki-repo/$subdir/" 2>/dev/null || true
|
||||
done
|
||||
|
||||
- name: Commit and push
|
||||
working-directory: wiki-repo
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"printWidth": 100,
|
||||
"tabWidth": 2
|
||||
}
|
||||
51
CHANGELOG.md
Normal file
51
CHANGELOG.md
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [3.0.3] - 2026-06-13
|
||||
|
||||
### Fixed
|
||||
|
||||
- Published package now includes dist/index.d.mts so ESM type resolution under node16/nodenext resolves the import condition.
|
||||
|
||||
## [3.0.2] - 2026-06-10
|
||||
|
||||
### Fixed
|
||||
|
||||
- Inherits hijri-core's UTC-day fix: `toHijri` with UTC-midnight Dates is now exact on all hosts
|
||||
(previously, LOCAL date components were read, causing off-by-one errors west of UTC and on UTC+13+).
|
||||
- Round-trips (`toGregorian` then `toHijri`) are now exact for both the UAQ (default) and FCNA engines.
|
||||
- Tests updated to use `new Date(Date.UTC(...))` anchors throughout; UAQ engine round-trip regression
|
||||
suite added. Lock-step release with hijri-core 1.0.3 fix (commit 3419378).
|
||||
|
||||
## [3.0.1] - 2026-05-30
|
||||
|
||||
### Fixed
|
||||
|
||||
- Improved type safety in `formatHijriDate`: explicit return type annotation on `replace` callback and non-null assertions on array lookups with JSDoc justification comments.
|
||||
- Added in-code comment blocks to all source modules documenting purpose, inputs, outputs, constraints, and SPORT references.
|
||||
|
||||
## [3.0.0] - 2026-05-28
|
||||
|
||||
### Changed
|
||||
|
||||
- BREAKING: `luxon` and `hijri-core` moved from `dependencies` to `peerDependencies`. Consumers must now install both alongside `luxon-hijri`. See the migration note below.
|
||||
- Peer range for `luxon` widened from `^3.5.0` to `^3.0.0` — any Luxon 3.x release is compatible.
|
||||
|
||||
### Migration from v2.x
|
||||
|
||||
```bash
|
||||
pnpm add luxon-hijri luxon hijri-core
|
||||
# or
|
||||
npm install luxon-hijri luxon hijri-core
|
||||
```
|
||||
|
||||
Prior to v3.0.0, `luxon` and `hijri-core` were bundled as runtime dependencies. This caused Luxon to appear twice in bundled applications where it was already installed. v3.0.0 aligns with the peer-dependency pattern used by all other hijri wrapper packages (`date-fns-hijri`, `dayjs-hijri-plus`, `moment-hijri-plus`, `temporal-hijri`).
|
||||
|
||||
## [2.1.0] - 2026-05-28
|
||||
|
||||
### Added
|
||||
- Initial release
|
||||
179
README.md
179
README.md
|
|
@ -3,13 +3,12 @@
|
|||
[](https://www.npmjs.com/package/luxon-hijri)
|
||||
[](https://github.com/acamarata/luxon-hijri/actions/workflows/ci.yml)
|
||||
[](./LICENSE)
|
||||
[](https://github.com/acamarata/luxon-hijri/wiki)
|
||||
|
||||
Hijri/Gregorian date conversion and formatting. Supports two calendar systems: Umm al-Qura (default, table-based) and FCNA/ISNA (astronomical, all Hijri years). Built on Luxon.
|
||||
Hijri/Gregorian date conversion and formatting for Luxon users. Thin adapter over [hijri-core](https://github.com/acamarata/hijri-core). Supports the Umm al-Qura calendar (1318-1500 AH, table-based) and the FCNA/ISNA calendar (astronomical, all Hijri years).
|
||||
|
||||
## Installation
|
||||
|
||||
`luxon-hijri` requires `luxon` and `hijri-core` as peer dependencies. Install all three:
|
||||
|
||||
```bash
|
||||
pnpm add luxon-hijri luxon hijri-core
|
||||
# or
|
||||
|
|
@ -21,187 +20,67 @@ npm install luxon-hijri luxon hijri-core
|
|||
```javascript
|
||||
import { toHijri, toGregorian, formatHijriDate } from 'luxon-hijri';
|
||||
|
||||
// Gregorian to Hijri (Umm al-Qura, default)
|
||||
const h = toHijri(new Date(2023, 2, 23, 12)); // March 23, 2023
|
||||
// Gregorian to Hijri (Umm al-Qura, default) — use Date.UTC for cross-host consistency
|
||||
const h = toHijri(new Date(Date.UTC(2023, 2, 23)));
|
||||
// { hy: 1444, hm: 9, hd: 1 }
|
||||
|
||||
// Hijri to Gregorian
|
||||
const g = toGregorian(1444, 9, 1); // 1 Ramadan 1444
|
||||
const g = toGregorian(1444, 9, 1);
|
||||
// Date: 2023-03-23T00:00:00.000Z
|
||||
|
||||
// Format a Hijri date
|
||||
formatHijriDate({ hy: 1444, hm: 9, hd: 1 }, 'iEEEE, iD iMMMM iYYYY ioooo');
|
||||
// "Yawm al-Khamis, 1 Ramadan 1444 AH"
|
||||
|
||||
// FCNA/ISNA calendar (astronomical, works for all Hijri years)
|
||||
toHijri(new Date(2025, 2, 1, 12), { calendar: 'fcna' }); // { hy: 1446, hm: 9, hd: 1 }
|
||||
toGregorian(1446, 9, 1, { calendar: 'fcna' }); // Date: 2025-03-01T00:00:00.000Z
|
||||
// FCNA/ISNA calendar
|
||||
toHijri(new Date(Date.UTC(2025, 2, 1)), { calendar: 'fcna' });
|
||||
```
|
||||
|
||||
## API
|
||||
## Day boundaries and time zones
|
||||
|
||||
### `toHijri(date, options?)`
|
||||
`toHijri(date)` reads the **UTC calendar day** of the Date you pass. `toGregorian(hy, hm, hd)` returns a Date at **UTC midnight** on the corresponding Gregorian day. Round-trips are therefore exact and produce identical results on any machine regardless of local timezone.
|
||||
|
||||
Converts a Gregorian `Date` to a Hijri date object.
|
||||
|
||||
```typescript
|
||||
function toHijri(date: Date, options?: ConversionOptions): HijriDate | null;
|
||||
```
|
||||
|
||||
For `'uaq'` (default): returns `null` if the date falls outside the table range (before 1 Muharram 1318 H / 1900-04-30, or at/after 1 Muharram 1501 H / 2077-11-17). Uses local date components.
|
||||
|
||||
For `'fcna'`: returns `null` only for dates before 1 AH. Uses UTC date components (FCNA boundaries are defined in UTC).
|
||||
|
||||
Throws `Error("Invalid Gregorian date")` if `date` is not a valid `Date`.
|
||||
**Converting a zone-aware Luxon DateTime.** Pass the DateTime's calendar fields, not `.toJSDate()`, unless the DateTime is already pinned to UTC:
|
||||
|
||||
```javascript
|
||||
toHijri(new Date(2024, 6, 7, 12)); // { hy: 1446, hm: 1, hd: 1 } (UAQ)
|
||||
toHijri(new Date(2025, 2, 1, 12), { calendar: 'fcna' }); // { hy: 1446, hm: 9, hd: 1 } (FCNA)
|
||||
toHijri(new Date(1800, 0, 1)); // null - before UAQ table range
|
||||
import { DateTime } from 'luxon';
|
||||
import { toHijri } from 'luxon-hijri';
|
||||
|
||||
const dt = DateTime.now().setZone('America/New_York');
|
||||
|
||||
// Correct — reads the calendar date in the DateTime's own zone
|
||||
const h = toHijri(new Date(Date.UTC(dt.year, dt.month - 1, dt.day)));
|
||||
|
||||
// Wrong if dt is not UTC-anchored — toJSDate() produces local-zone midnight,
|
||||
// which may land on the previous UTC day for western timezones
|
||||
// const h = toHijri(dt.toJSDate());
|
||||
```
|
||||
|
||||
### `toGregorian(hy, hm, hd, options?)`
|
||||
**ISO string parsing.** `new Date("2025-03-01")` parses as UTC midnight — that is exactly the right input for a calendar-day conversion and will produce the correct Hijri date.
|
||||
|
||||
Converts a Hijri date to a Gregorian `Date` at UTC midnight.
|
||||
|
||||
```typescript
|
||||
function toGregorian(hy: number, hm: number, hd: number, options?: ConversionOptions): Date;
|
||||
```
|
||||
|
||||
Throws `Error("Invalid Hijri date")` if the date is invalid for the selected calendar.
|
||||
|
||||
```javascript
|
||||
toGregorian(1446, 1, 1); // Date: 2024-07-07T00:00:00.000Z (UAQ)
|
||||
toGregorian(1446, 9, 1, { calendar: 'fcna' }); // Date: 2025-03-01T00:00:00.000Z (FCNA)
|
||||
toGregorian(1, 1, 1, { calendar: 'fcna' }); // Date: 0622-07-18T00:00:00.000Z (Islamic epoch)
|
||||
```
|
||||
|
||||
### `formatHijriDate(date, format)`
|
||||
|
||||
Formats a Hijri date using the token patterns below. Tokens not listed pass through unchanged.
|
||||
|
||||
```typescript
|
||||
function formatHijriDate(date: HijriDate, format: string): string;
|
||||
```
|
||||
|
||||
| Token | Output | Example |
|
||||
| -------------------- | ------------------------ | --------------------- |
|
||||
| `iYYYY` | Year, 4 digits | `1444` |
|
||||
| `iYY` | Year, last 2 digits | `44` |
|
||||
| `iMMMM` | Month, full name | `Ramadan` |
|
||||
| `iMMM` | Month, medium name | `Ramadan` |
|
||||
| `iMM` | Month, zero-padded | `09` |
|
||||
| `iM` | Month, no padding | `9` |
|
||||
| `iDD` | Day, zero-padded | `01` |
|
||||
| `iD` | Day, no padding | `1` |
|
||||
| `iEEEE` | Weekday, full name | `Yawm al-Khamis` |
|
||||
| `iEEE` | Weekday, abbreviated | `Kham` |
|
||||
| `iE` | Weekday, numeric (Sun=1) | `5` |
|
||||
| `ioooo` | Era, full | `AH` |
|
||||
| `iooo` | Era, abbreviated | `AH` |
|
||||
| `HH`, `H`, `hh`, `h` | Hour (via Luxon) | `14`, `14`, `02`, `2` |
|
||||
| `mm`, `m` | Minute (via Luxon) | `05`, `5` |
|
||||
| `ss`, `s` | Second (via Luxon) | `30`, `30` |
|
||||
| `a` | AM/PM | `AM` |
|
||||
| `z`, `zz`, `zzz` | Timezone | `UTC` |
|
||||
| `Z`, `ZZ` | Timezone offset | `+00:00` |
|
||||
|
||||
### `isValidHijriDate(hy, hm, hd, options?)`
|
||||
|
||||
Returns `true` if the Hijri date is valid for the selected calendar.
|
||||
|
||||
```typescript
|
||||
function isValidHijriDate(hy: number, hm: number, hd: number, options?: ConversionOptions): boolean;
|
||||
```
|
||||
|
||||
For `'uaq'` (default): year must be 1318–1500, month 1–12, day must not exceed the actual month length from the UAQ table.
|
||||
|
||||
For `'fcna'`: year must be ≥ 1, month 1–12, day must not exceed the computed FCNA month length.
|
||||
|
||||
### Types
|
||||
|
||||
```typescript
|
||||
interface HijriDate {
|
||||
hy: number; // Hijri year
|
||||
hm: number; // Hijri month (1–12)
|
||||
hd: number; // Hijri day (1–30)
|
||||
}
|
||||
|
||||
type CalendarSystem = 'uaq' | 'fcna';
|
||||
|
||||
interface ConversionOptions {
|
||||
calendar?: CalendarSystem; // default: 'uaq'
|
||||
}
|
||||
|
||||
interface HijriYearRecord {
|
||||
hy: number; // Hijri year
|
||||
dpm: number; // days-per-month bitmask (bit 0 = month 1, 1 = 30 days, 0 = 29 days)
|
||||
gy: number; // Gregorian year of 1 Muharram
|
||||
gm: number; // Gregorian month of 1 Muharram
|
||||
gd: number; // Gregorian day of 1 Muharram
|
||||
}
|
||||
```
|
||||
|
||||
### Additional exports
|
||||
|
||||
```javascript
|
||||
import {
|
||||
hDatesTable, // HijriYearRecord[] - the full Umm al-Qura table (184 entries)
|
||||
hmLong, // string[12] - full month names
|
||||
hmMedium, // string[12] - medium month names
|
||||
hmShort, // string[12] - abbreviated month names
|
||||
hwLong, // string[7] - full weekday names (Sunday first)
|
||||
hwShort, // string[7] - abbreviated weekday names
|
||||
hwNumeric, // number[7] - weekday numbers (1-7, Sunday=1)
|
||||
formatPatterns, // Record<string, string> - token reference
|
||||
} from 'luxon-hijri';
|
||||
```
|
||||
|
||||
## Calendar Systems
|
||||
|
||||
**Umm al-Qura (`'uaq'`, default):** Official Saudi calendar, table-based, covers Hijri years 1318–1500 (April 1900 to November 2076). Authoritative for Saudi Arabia and widely used across the Arab world.
|
||||
|
||||
**FCNA/ISNA (`'fcna'`):** Used by the Fiqh Council of North America and ISNA. Astronomical criterion: if the new moon conjunction occurs before 12:00 UTC on day D, the month begins at midnight of D+1; otherwise D+2. Works for all Hijri years (no range limit). New moon times use the full Meeus Chapter 49 algorithm, accurate to within a few minutes for 1000–3000 CE.
|
||||
|
||||
## Architecture
|
||||
|
||||
The UAQ engine is a pure table lookup with binary search (O(log 183)). The FCNA engine computes new moon times astronomically using the Meeus Ch.49 formula: 3 to 5 trigonometric evaluations per call, sub-millisecond on any modern JS engine.
|
||||
|
||||
For more detail see the [Architecture wiki page](https://github.com/acamarata/luxon-hijri/wiki/Architecture).
|
||||
|
||||
## Compatibility
|
||||
|
||||
- Node.js 20+ (ESM and CJS)
|
||||
- Bundlers: webpack, Rollup, Vite, esbuild (tree-shakeable, `sideEffects: false`)
|
||||
- TypeScript: full type definitions included
|
||||
Note: determining when the Hijri day begins at local sunset is out of scope for this library.
|
||||
|
||||
## TypeScript
|
||||
|
||||
```typescript
|
||||
import { toHijri, toGregorian, formatHijriDate, isValidHijriDate } from 'luxon-hijri';
|
||||
import type { HijriDate, HijriYearRecord, CalendarSystem, ConversionOptions } from 'luxon-hijri';
|
||||
|
||||
const h: HijriDate | null = toHijri(new Date());
|
||||
const g: Date = toGregorian(1444, 9, 1, { calendar: 'fcna' });
|
||||
import type { HijriDate, CalendarSystem, ConversionOptions } from 'luxon-hijri';
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
Full API reference, architecture notes, calendar background, and format token guide:
|
||||
[https://github.com/acamarata/luxon-hijri/wiki](https://github.com/acamarata/luxon-hijri/wiki)
|
||||
Full API reference, format token guide, calendar background, and architecture notes: [GitHub Wiki](https://github.com/acamarata/luxon-hijri/wiki)
|
||||
|
||||
## Related
|
||||
|
||||
- [nrel-spa](https://www.npmjs.com/package/nrel-spa): NREL Solar Position Algorithm (pure JS)
|
||||
- [pray-calc](https://www.npmjs.com/package/pray-calc): Islamic prayer times, depends on nrel-spa
|
||||
- [solar-spa](https://www.npmjs.com/package/solar-spa): NREL SPA compiled to WebAssembly
|
||||
- [hijri-core](https://github.com/acamarata/hijri-core): The underlying calendar engine
|
||||
- [pray-calc](https://www.npmjs.com/package/pray-calc): Islamic prayer times
|
||||
- [nrel-spa](https://www.npmjs.com/package/nrel-spa): NREL Solar Position Algorithm
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
The Umm al-Qura table is derived from data published by the King Abdulaziz City for Science and Technology (KACST). The FCNA new moon algorithm follows Jean Meeus, "Astronomical Algorithms," 2nd ed., Chapter 49.
|
||||
The Umm al-Qura table is derived from data published by KACST (King Abdulaziz City for Science and Technology). The FCNA new moon algorithm follows Jean Meeus, "Astronomical Algorithms," 2nd ed., Chapter 49.
|
||||
|
||||
## License
|
||||
|
||||
MIT. Copyright (c) 2024-2026 Aric Camarata.
|
||||
|
||||
See [LICENSE](./LICENSE) for the full text.
|
||||
|
|
|
|||
|
|
@ -1,10 +1,23 @@
|
|||
import js from '@eslint/js';
|
||||
import tseslint from 'typescript-eslint';
|
||||
import prettier from 'eslint-config-prettier';
|
||||
import tsParser from '@typescript-eslint/parser';
|
||||
import tsPlugin from '@typescript-eslint/eslint-plugin';
|
||||
import eslintConfigPrettier from 'eslint-config-prettier';
|
||||
import { typescript } from '@acamarata/eslint-config';
|
||||
|
||||
export default tseslint.config(
|
||||
{ ignores: ['dist/', 'node_modules/', '*.cjs', '*.mjs'] },
|
||||
js.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
prettier,
|
||||
);
|
||||
export default [
|
||||
{
|
||||
plugins: { '@typescript-eslint': tsPlugin },
|
||||
languageOptions: {
|
||||
parser: tsParser,
|
||||
parserOptions: {
|
||||
project: true,
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
files: ['src/**/*.ts'],
|
||||
},
|
||||
...typescript.map((config) => ({ ...config, files: ['src/**/*.ts'] })),
|
||||
eslintConfigPrettier,
|
||||
{
|
||||
ignores: ['dist/', 'node_modules/', 'coverage/', 'test.mjs', 'test-cjs.cjs', 'test-crossval.mjs', 'tsup.config.ts', 'typedoc.json'],
|
||||
},
|
||||
];
|
||||
|
|
|
|||
103
luxon-hijri.test.ts
Normal file
103
luxon-hijri.test.ts
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
/**
|
||||
* Purpose: Vitest suite for luxon-hijri — conversion, formatting, and validation.
|
||||
* Inputs: Pure functions from src/index.ts. Requires luxon + hijri-core as peer deps.
|
||||
* Outputs: Vitest pass/fail assertions.
|
||||
* Constraints: UAQ range 1318–1500 AH. toGregorian throws (not null) on invalid input.
|
||||
* toHijri reads the Date's UTC calendar day; pass UTC midnight or use
|
||||
* Date.UTC(year, month-1, day) for exact results on all hosts.
|
||||
* Usage: pnpm vitest run
|
||||
* SOT: packages.md — luxon-hijri row
|
||||
*/
|
||||
import { describe, it, expect } from "vitest";
|
||||
import {
|
||||
toHijri,
|
||||
toGregorian,
|
||||
isValidHijriDate,
|
||||
formatHijriDate,
|
||||
hmLong,
|
||||
hmMedium,
|
||||
hmShort,
|
||||
} from "./src/index";
|
||||
|
||||
// Anchor: toGregorian(1446, 9, 1) = 2025-03-01 midnight UTC
|
||||
// toHijri on noon 2025-03-01 reliably returns { hm: 9, hd: 1 }
|
||||
const RAMADAN_1446_NOON = new Date("2025-03-01T12:00:00Z");
|
||||
|
||||
describe("toHijri", () => {
|
||||
it("converts noon 2025-03-01 UTC to 1 Ramadan 1446", () => {
|
||||
const result = toHijri(RAMADAN_1446_NOON);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.hy).toBe(1446);
|
||||
expect(result!.hm).toBe(9);
|
||||
expect(result!.hd).toBe(1);
|
||||
});
|
||||
|
||||
it("returns null for dates outside UAQ range", () => {
|
||||
expect(toHijri(new Date("2100-01-01"))).toBeNull();
|
||||
});
|
||||
|
||||
it("throws on an invalid Date", () => {
|
||||
expect(() => toHijri(new Date("not-a-date"))).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("toGregorian", () => {
|
||||
it("converts 1 Ramadan 1446 to 2025-03-01 UTC midnight", () => {
|
||||
const result = toGregorian(1446, 9, 1);
|
||||
expect(result.toISOString()).toBe("2025-03-01T00:00:00.000Z");
|
||||
});
|
||||
|
||||
it("throws on invalid Hijri date (out of range)", () => {
|
||||
expect(() => toGregorian(1501, 1, 1)).toThrow("Invalid Hijri date");
|
||||
});
|
||||
});
|
||||
|
||||
describe("isValidHijriDate", () => {
|
||||
it("returns true for 1 Ramadan 1446", () => {
|
||||
expect(isValidHijriDate(1446, 9, 1)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false for month 0", () => {
|
||||
expect(isValidHijriDate(1446, 0, 1)).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for day 31", () => {
|
||||
expect(isValidHijriDate(1446, 1, 31)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatHijriDate", () => {
|
||||
const hijriDate = { hy: 1446, hm: 9, hd: 1 };
|
||||
|
||||
it("formats iYYYY-iMM-iDD correctly", () => {
|
||||
expect(formatHijriDate(hijriDate, "iYYYY-iMM-iDD")).toBe("1446-09-01");
|
||||
});
|
||||
|
||||
it("formats iMMMM as full month name Ramadan", () => {
|
||||
expect(formatHijriDate(hijriDate, "iMMMM")).toBe("Ramadan");
|
||||
});
|
||||
|
||||
it("formats iMMM as a non-empty medium month name", () => {
|
||||
const result = formatHijriDate(hijriDate, "iMMM");
|
||||
expect(typeof result).toBe("string");
|
||||
expect(result.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("throws RangeError on invalid month 0", () => {
|
||||
expect(() => formatHijriDate({ hy: 1446, hm: 0, hd: 1 }, "iMMMM")).toThrow(RangeError);
|
||||
});
|
||||
});
|
||||
|
||||
describe("month name tables", () => {
|
||||
it("hmLong index 8 is Ramadan", () => {
|
||||
expect(hmLong[8]).toBe("Ramadan");
|
||||
});
|
||||
|
||||
it("hmMedium has 12 entries", () => {
|
||||
expect(hmMedium).toHaveLength(12);
|
||||
});
|
||||
|
||||
it("hmShort has 12 entries", () => {
|
||||
expect(hmShort).toHaveLength(12);
|
||||
});
|
||||
});
|
||||
31
package.json
31
package.json
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "luxon-hijri",
|
||||
"version": "3.0.0",
|
||||
"version": "3.0.3",
|
||||
"description": "Hijri/Gregorian date conversion and formatting using the Umm al-Qura calendar. Built on Luxon. Supports toHijri, toGregorian, formatHijriDate, and isValidHijriDate.",
|
||||
"author": "Aric Camarata",
|
||||
"license": "MIT",
|
||||
|
|
@ -34,11 +34,14 @@
|
|||
"typecheck": "tsc --noEmit",
|
||||
"pretest": "tsup",
|
||||
"test": "node --test test.mjs && node --test test-cjs.cjs",
|
||||
"prepublishOnly": "tsup",
|
||||
"prepack": "pnpm run build",
|
||||
"lint": "eslint .",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check .",
|
||||
"coverage": "c8 --reporter=lcov --reporter=text node --test"
|
||||
"format": "prettier --write src/",
|
||||
"format:check": "prettier --check src/",
|
||||
"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": [
|
||||
"hijri",
|
||||
|
|
@ -65,17 +68,26 @@
|
|||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@acamarata/eslint-config": "^0.1.0",
|
||||
"@acamarata/prettier-config": "^0.1.0",
|
||||
"@acamarata/tsconfig": "^0.1.0",
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@types/luxon": "^3.4.2",
|
||||
"hijri-core": "^1.0.0",
|
||||
"luxon": "^3.5.0",
|
||||
"@types/node": "^22.15.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.56.1",
|
||||
"@typescript-eslint/parser": "^8.56.1",
|
||||
"c8": "^10.1.2",
|
||||
"eslint": "^10.0.3",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"hijri-core": "^1.0.3",
|
||||
"luxon": "^3.5.0",
|
||||
"prettier": "^3.8.1",
|
||||
"tsup": "^8.0.0",
|
||||
"typedoc": "^0.28.19",
|
||||
"typedoc-plugin-markdown": "^4.11.0",
|
||||
"typescript": "^5.5.0",
|
||||
"typescript-eslint": "^8.56.1"
|
||||
"typescript-eslint": "^8.56.1",
|
||||
"vitest": "^2.1.9"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
|
|
@ -89,5 +101,6 @@
|
|||
"bugs": {
|
||||
"url": "https://github.com/acamarata/luxon-hijri/issues"
|
||||
},
|
||||
"type": "module"
|
||||
"type": "module",
|
||||
"prettier": "@acamarata/prettier-config"
|
||||
}
|
||||
|
|
|
|||
1203
pnpm-lock.yaml
1203
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
|
@ -1,9 +1,9 @@
|
|||
// formatHijriDate.ts
|
||||
import { DateTime } from 'luxon';
|
||||
import { hmLong, hmMedium } from './hMonths';
|
||||
import { hwLong, hwShort, hwNumeric } from './hWeekdays';
|
||||
import { toGregorian } from './toGregorian';
|
||||
import type { HijriDate } from './types';
|
||||
import { DateTime } from "luxon";
|
||||
import { hmLong, hmMedium } from "./hMonths";
|
||||
import { hwLong, hwShort, hwNumeric } from "./hWeekdays";
|
||||
import { toGregorian } from "./toGregorian";
|
||||
import type { HijriDate } from "./types";
|
||||
|
||||
// Token regex: longest tokens first to prevent partial matches.
|
||||
const TOKEN_RE =
|
||||
|
|
@ -33,42 +33,49 @@ export function formatHijriDate(hijriDate: HijriDate, format: string): string {
|
|||
function getGregDt(): DateTime {
|
||||
if (!_gregDt) {
|
||||
const greg = toGregorian(hijriDate.hy, hijriDate.hm, hijriDate.hd);
|
||||
_gregDt = DateTime.fromJSDate(greg, { zone: 'UTC' });
|
||||
_gregDt = DateTime.fromJSDate(greg, { zone: "UTC" });
|
||||
}
|
||||
return _gregDt;
|
||||
}
|
||||
|
||||
return format.replace(TOKEN_RE, (match) => {
|
||||
return format.replace(TOKEN_RE, (match): string => {
|
||||
switch (match) {
|
||||
case 'iYYYY':
|
||||
return String(hijriDate.hy).padStart(4, '0');
|
||||
case 'iYY':
|
||||
return String(hijriDate.hy % 100).padStart(2, '0');
|
||||
case 'iMM':
|
||||
return String(hijriDate.hm).padStart(2, '0');
|
||||
case 'iM':
|
||||
case "iYYYY":
|
||||
return String(hijriDate.hy).padStart(4, "0");
|
||||
case "iYY":
|
||||
return String(hijriDate.hy % 100).padStart(2, "0");
|
||||
case "iMM":
|
||||
return String(hijriDate.hm).padStart(2, "0");
|
||||
case "iM":
|
||||
return String(hijriDate.hm);
|
||||
case 'iMMM':
|
||||
return hmMedium[hijriDate.hm - 1];
|
||||
case 'iMMMM':
|
||||
return hmLong[hijriDate.hm - 1];
|
||||
case 'iDD':
|
||||
return String(hijriDate.hd).padStart(2, '0');
|
||||
case 'iD':
|
||||
case "iMMM":
|
||||
// Non-null: hm is validated 1-12 above; index hm-1 is always 0-11, within array bounds.
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
return hmMedium[hijriDate.hm - 1]!;
|
||||
case "iMMMM":
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
return hmLong[hijriDate.hm - 1]!;
|
||||
case "iDD":
|
||||
return String(hijriDate.hd).padStart(2, "0");
|
||||
case "iD":
|
||||
return String(hijriDate.hd);
|
||||
case 'iE':
|
||||
case 'iEEE':
|
||||
case 'iEEEE': {
|
||||
case "iE":
|
||||
case "iEEE":
|
||||
case "iEEEE": {
|
||||
// Luxon weekday: 1=Mon … 7=Sun. Modulo 7: Mon=1 … Sat=6, Sun=0.
|
||||
// hwLong/hwShort/hwNumeric arrays: index 0=Sunday, 1=Monday, … 6=Saturday.
|
||||
const idx = getGregDt().weekday % 7;
|
||||
if (match === 'iE') return String(hwNumeric[idx]);
|
||||
if (match === 'iEEE') return hwShort[idx];
|
||||
return hwLong[idx];
|
||||
// Non-null: idx is always 0-6 (weekday%7), within all hw* array bounds.
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
if (match === "iE") return String(hwNumeric[idx]!);
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
if (match === "iEEE") return hwShort[idx]!;
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
return hwLong[idx]!;
|
||||
}
|
||||
case 'iooo':
|
||||
case 'ioooo':
|
||||
return 'AH';
|
||||
case "iooo":
|
||||
case "ioooo":
|
||||
return "AH";
|
||||
default:
|
||||
// Delegate time and timezone tokens to Luxon using the Gregorian DateTime.
|
||||
return getGregDt().toFormat(match);
|
||||
|
|
|
|||
|
|
@ -1,47 +1,54 @@
|
|||
/**
|
||||
* Purpose: Reference map of all supported format tokens to their human-readable descriptions.
|
||||
* Inputs: n/a — static data export
|
||||
* Outputs: Record<string, string> mapping token string to description
|
||||
* Constraints: keys must match the TOKEN_RE in formatHijriDate.ts; used for documentation and introspection
|
||||
* SPORT: packages.md — luxon-hijri row
|
||||
*/
|
||||
// formatPatterns.ts
|
||||
// Define a mapping of Hijri format tokens to their meanings
|
||||
export const formatPatterns = {
|
||||
// Hijri Year
|
||||
iYYYY: 'Hijri year (4 digits)',
|
||||
iYY: 'Hijri year (2 digits)',
|
||||
iYYYY: "Hijri year (4 digits)",
|
||||
iYY: "Hijri year (2 digits)",
|
||||
|
||||
// Hijri Month
|
||||
iMM: 'Hijri month (2 digits, zero-padded)',
|
||||
iM: 'Hijri month (1 or 2 digits without zero-padding)',
|
||||
iMMM: 'Hijri month (abbreviated name)',
|
||||
iMMMM: 'Hijri month (full name)',
|
||||
iMM: "Hijri month (2 digits, zero-padded)",
|
||||
iM: "Hijri month (1 or 2 digits without zero-padding)",
|
||||
iMMM: "Hijri month (abbreviated name)",
|
||||
iMMMM: "Hijri month (full name)",
|
||||
|
||||
// Hijri Day
|
||||
iDD: 'Hijri day of the month (2 digits, zero-padded)',
|
||||
iD: 'Hijri day of the month (1 or 2 digits without zero-padding)',
|
||||
iDD: "Hijri day of the month (2 digits, zero-padded)",
|
||||
iD: "Hijri day of the month (1 or 2 digits without zero-padding)",
|
||||
|
||||
// Hijri Weekday
|
||||
iE: 'Hijri weekday (1 digit)',
|
||||
iEEE: 'Hijri weekday (abbreviated name)',
|
||||
iEEEE: 'Hijri weekday (full name)',
|
||||
iE: "Hijri weekday (1 digit)",
|
||||
iEEE: "Hijri weekday (abbreviated name)",
|
||||
iEEEE: "Hijri weekday (full name)",
|
||||
|
||||
// Hour, Minute, Second
|
||||
// These can remain the same as in Gregorian as they don’t change in Hijri
|
||||
HH: 'Hour (2 digits, zero-padded, 24-hour clock)',
|
||||
H: 'Hour (1 or 2 digits without zero-padding, 24-hour clock)',
|
||||
hh: 'Hour (2 digits, zero-padded, 12-hour clock)',
|
||||
h: 'Hour (1 or 2 digits without zero-padding, 12-hour clock)',
|
||||
mm: 'Minute (2 digits, zero-padded)',
|
||||
m: 'Minute (1 or 2 digits without zero-padding)',
|
||||
ss: 'Second (2 digits, zero-padded)',
|
||||
s: 'Second (1 or 2 digits without zero-padding)',
|
||||
HH: "Hour (2 digits, zero-padded, 24-hour clock)",
|
||||
H: "Hour (1 or 2 digits without zero-padding, 24-hour clock)",
|
||||
hh: "Hour (2 digits, zero-padded, 12-hour clock)",
|
||||
h: "Hour (1 or 2 digits without zero-padding, 12-hour clock)",
|
||||
mm: "Minute (2 digits, zero-padded)",
|
||||
m: "Minute (1 or 2 digits without zero-padding)",
|
||||
ss: "Second (2 digits, zero-padded)",
|
||||
s: "Second (1 or 2 digits without zero-padding)",
|
||||
|
||||
// AM/PM
|
||||
a: 'AM/PM marker',
|
||||
a: "AM/PM marker",
|
||||
|
||||
// Other
|
||||
iooo: 'Hijri era (abbreviated)',
|
||||
ioooo: 'Hijri era (full)',
|
||||
iooo: "Hijri era (abbreviated)",
|
||||
ioooo: "Hijri era (full)",
|
||||
|
||||
// Timezone
|
||||
z: 'Timezone (abbreviated)',
|
||||
zz: 'Timezone (medium)',
|
||||
zzz: 'Timezone (full)',
|
||||
Z: 'Timezone offset from UTC (+HH:MM)',
|
||||
ZZ: 'Timezone offset from UTC (condensed)',
|
||||
z: "Timezone (abbreviated)",
|
||||
zz: "Timezone (medium)",
|
||||
zzz: "Timezone (full)",
|
||||
Z: "Timezone offset from UTC (+HH:MM)",
|
||||
ZZ: "Timezone offset from UTC (condensed)",
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,3 +1,10 @@
|
|||
/**
|
||||
* Purpose: UAQ table data and year record type, re-exported from hijri-core.
|
||||
* Inputs: n/a — data export
|
||||
* Outputs: hDatesTable (HijriYearRecord[184]) and HijriYearRecord type
|
||||
* Constraints: table covers 1318-1501 AH (183 real years + 1 sentinel); maintained in hijri-core
|
||||
* SPORT: packages.md — luxon-hijri row
|
||||
*/
|
||||
// hDates.ts: re-exports from hijri-core; table is maintained in the core package
|
||||
export { hDatesTable } from 'hijri-core';
|
||||
export type { HijriYearRecord } from 'hijri-core';
|
||||
export { hDatesTable } from "hijri-core";
|
||||
export type { HijriYearRecord } from "hijri-core";
|
||||
|
|
|
|||
|
|
@ -1,2 +1,9 @@
|
|||
/**
|
||||
* Purpose: Hijri month name arrays (long, medium, short), re-exported from hijri-core.
|
||||
* Inputs: n/a — data exports
|
||||
* Outputs: hmLong[12], hmMedium[12], hmShort[12] — index 0 = Muharram, index 11 = Dhul Hijjah
|
||||
* Constraints: arrays are fixed-length 12; maintained in hijri-core
|
||||
* SPORT: packages.md — luxon-hijri row
|
||||
*/
|
||||
// hMonths.ts: re-exports from hijri-core
|
||||
export { hmLong, hmMedium, hmShort } from 'hijri-core';
|
||||
export { hmLong, hmMedium, hmShort } from "hijri-core";
|
||||
|
|
|
|||
|
|
@ -1,2 +1,9 @@
|
|||
/**
|
||||
* Purpose: Hijri weekday name and numeric arrays, re-exported from hijri-core.
|
||||
* Inputs: n/a — data exports
|
||||
* Outputs: hwLong[7], hwShort[7], hwNumeric[7] — index 0 = Sunday (Islamic convention)
|
||||
* Constraints: arrays are fixed-length 7; Sunday=1 in hwNumeric; maintained in hijri-core
|
||||
* SPORT: packages.md — luxon-hijri row
|
||||
*/
|
||||
// hWeekdays.ts: re-exports from hijri-core
|
||||
export { hwLong, hwShort, hwNumeric } from 'hijri-core';
|
||||
export { hwLong, hwShort, hwNumeric } from "hijri-core";
|
||||
|
|
|
|||
20
src/index.ts
20
src/index.ts
|
|
@ -1,11 +1,11 @@
|
|||
// index.ts
|
||||
export { formatPatterns } from './formatPatterns';
|
||||
export { hDatesTable } from './hDates';
|
||||
export type { HijriYearRecord } from './hDates';
|
||||
export { hmLong, hmMedium, hmShort } from './hMonths';
|
||||
export { hwLong, hwShort, hwNumeric } from './hWeekdays';
|
||||
export { toGregorian } from './toGregorian';
|
||||
export { toHijri } from './toHijri';
|
||||
export { formatHijriDate } from './formatHijriDate';
|
||||
export { isValidHijriDate } from './utils';
|
||||
export type { HijriDate, CalendarSystem, ConversionOptions } from './types';
|
||||
export { formatPatterns } from "./formatPatterns";
|
||||
export { hDatesTable } from "./hDates";
|
||||
export type { HijriYearRecord } from "./hDates";
|
||||
export { hmLong, hmMedium, hmShort } from "./hMonths";
|
||||
export { hwLong, hwShort, hwNumeric } from "./hWeekdays";
|
||||
export { toGregorian } from "./toGregorian";
|
||||
export { toHijri } from "./toHijri";
|
||||
export { formatHijriDate } from "./formatHijriDate";
|
||||
export { isValidHijriDate } from "./utils";
|
||||
export type { HijriDate, CalendarSystem, ConversionOptions } from "./types";
|
||||
|
|
|
|||
|
|
@ -1,6 +1,13 @@
|
|||
/**
|
||||
* Purpose: Convert a Hijri date to a UTC Gregorian Date, throwing on invalid input.
|
||||
* Inputs: hy: number, hm: number (1-12), hd: number (1-30), options?: ConversionOptions
|
||||
* Outputs: Date — UTC midnight on the corresponding Gregorian day
|
||||
* Constraints: throws Error (not null) for invalid dates; UAQ range 1318-1500 AH; FCNA all years >= 1
|
||||
* SPORT: packages.md — luxon-hijri row
|
||||
*/
|
||||
// toGregorian.ts: thin wrapper over hijri-core; preserves throw-on-invalid behavior
|
||||
import { toGregorian as coreToGregorian } from 'hijri-core';
|
||||
import type { ConversionOptions } from './types';
|
||||
import { toGregorian as coreToGregorian } from "hijri-core";
|
||||
import type { ConversionOptions } from "./types";
|
||||
|
||||
/**
|
||||
* Convert a Hijri date to a Gregorian Date object.
|
||||
|
|
@ -17,6 +24,6 @@ import type { ConversionOptions } from './types';
|
|||
*/
|
||||
export function toGregorian(hy: number, hm: number, hd: number, options?: ConversionOptions): Date {
|
||||
const result = coreToGregorian(hy, hm, hd, options);
|
||||
if (result === null) throw new Error('Invalid Hijri date');
|
||||
if (result === null) throw new Error("Invalid Hijri date");
|
||||
return result;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,2 +1,9 @@
|
|||
/**
|
||||
* Purpose: Convert a Gregorian Date to a Hijri date object.
|
||||
* Inputs: date: Date, options?: ConversionOptions
|
||||
* Outputs: HijriDate | null — null when date is outside the calendar range
|
||||
* Constraints: delegates entirely to hijri-core; no conversion logic here
|
||||
* SPORT: packages.md — luxon-hijri row
|
||||
*/
|
||||
// toHijri.ts: delegates to hijri-core
|
||||
export { toHijri } from 'hijri-core';
|
||||
export { toHijri } from "hijri-core";
|
||||
|
|
|
|||
22
src/types.ts
22
src/types.ts
|
|
@ -1,6 +1,20 @@
|
|||
/**
|
||||
* Purpose: Shared type definitions for luxon-hijri's public API.
|
||||
* Inputs: n/a — type-only exports
|
||||
* Outputs: HijriDate, HijriYearRecord, ConversionOptions (re-exported from hijri-core), CalendarSystem
|
||||
* Constraints: CalendarSystem covers built-in engines only; hijri-core accepts any string via registerCalendar()
|
||||
* SPORT: packages.md — luxon-hijri row
|
||||
*/
|
||||
// types.ts: re-exports from hijri-core for backward compatibility
|
||||
export type { HijriDate, HijriYearRecord, ConversionOptions } from 'hijri-core';
|
||||
export type { HijriDate, HijriYearRecord, ConversionOptions } from "hijri-core";
|
||||
|
||||
// CalendarSystem documents the built-in calendar identifiers.
|
||||
// hijri-core accepts any string via registerCalendar(); this type covers the defaults.
|
||||
export type CalendarSystem = 'uaq' | 'fcna';
|
||||
/**
|
||||
* Built-in calendar system identifiers.
|
||||
*
|
||||
* - `'uaq'`: Umm al-Qura (default). Table-based, covers 1318-1500 AH / 1900-2076 CE.
|
||||
* - `'fcna'`: FCNA/ISNA. Astronomical calculation, works for all Hijri years >= 1 AH.
|
||||
*
|
||||
* hijri-core accepts any string identifier via `registerCalendar()`. This type covers
|
||||
* the built-in defaults only.
|
||||
*/
|
||||
export type CalendarSystem = "uaq" | "fcna";
|
||||
|
|
|
|||
|
|
@ -1,2 +1,9 @@
|
|||
/**
|
||||
* Purpose: Validate a Hijri date against the active calendar system.
|
||||
* Inputs: hy: number, hm: number, hd: number, options?: ConversionOptions
|
||||
* Outputs: boolean — true if date is valid for the given calendar
|
||||
* Constraints: delegates to hijri-core; UAQ range is 1318-1500 AH; FCNA supports all years >= 1
|
||||
* SPORT: packages.md — luxon-hijri row
|
||||
*/
|
||||
// utils.ts: delegates to hijri-core
|
||||
export { isValidHijriDate } from 'hijri-core';
|
||||
export { isValidHijriDate } from "hijri-core";
|
||||
|
|
|
|||
|
|
@ -43,11 +43,11 @@ describe('CJS core conversions', () => {
|
|||
assert.strictEqual(d.toISOString().slice(0, 10), '2023-03-23');
|
||||
});
|
||||
it('toHijri(2022-07-30) = 1 Muharram 1444', () => {
|
||||
const h = toHijri(new Date(2022, 6, 30, 12));
|
||||
const h = toHijri(new Date(Date.UTC(2022, 6, 30)));
|
||||
assert.deepEqual(h, { hy: 1444, hm: 1, hd: 1 });
|
||||
});
|
||||
it('toHijri(2023-03-23) = 1 Ramadan 1444', () => {
|
||||
const h = toHijri(new Date(2023, 2, 23, 12));
|
||||
const h = toHijri(new Date(Date.UTC(2023, 2, 23)));
|
||||
assert.deepEqual(h, { hy: 1444, hm: 9, hd: 1 });
|
||||
});
|
||||
});
|
||||
|
|
@ -106,7 +106,7 @@ describe('CJS FCNA calendar', () => {
|
|||
assert.strictEqual(d.toISOString().slice(0, 10), '2025-03-01');
|
||||
});
|
||||
it('2025-03-01 = 1 Ramadan 1446', () => {
|
||||
const h = toHijri(new Date(2025, 2, 1, 12), FCNA);
|
||||
const h = toHijri(new Date(Date.UTC(2025, 2, 1)), FCNA);
|
||||
assert.deepEqual(h, { hy: 1446, hm: 9, hd: 1 });
|
||||
});
|
||||
it('isValidHijriDate(1446, 9, 1) = true', () => {
|
||||
|
|
|
|||
175
test-crossval.mjs
Normal file
175
test-crossval.mjs
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
// test-crossval.mjs — Cross-validation suite for luxon-hijri
|
||||
//
|
||||
// Purpose: verify toHijri and toGregorian produce exact Umm al-Qura dates.
|
||||
// Covers:
|
||||
// - 58 UAQ spot-check dates spanning 1318–1462 AH
|
||||
// - 22 ICOP Ramadan/Eid start dates for 1440–1450 AH (UAQ)
|
||||
//
|
||||
// Reference data is derived from toGregorian and cross-checked against the
|
||||
// official UAQ calendar published by the Kingdom of Saudi Arabia, and
|
||||
// against independently verified Islamic event dates.
|
||||
//
|
||||
// Run: node test-crossval.mjs
|
||||
// Must pass with zero failures before any publish.
|
||||
|
||||
import { describe, it } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
import { toHijri, toGregorian } from './dist/index.mjs';
|
||||
|
||||
// ─── Helpers ────────────────────────────────────────────────────────────────
|
||||
|
||||
function g(iso) {
|
||||
// Use noon UTC to avoid local-timezone edge cases in toHijri
|
||||
return new Date(iso + 'T12:00:00Z');
|
||||
}
|
||||
|
||||
// ─── UAQ spot-check reference data ──────────────────────────────────────────
|
||||
//
|
||||
// Format: [gregorian_ISO, hijri_year, hijri_month, hijri_day]
|
||||
// Verified against UAQ table embedded in hijri-core + external Islamic
|
||||
// calendar references for well-known dates (Islamic New Year 1400,
|
||||
// Ramadan start dates from 1431 onward).
|
||||
|
||||
const UAQ_SPOT_CHECKS = [
|
||||
// 1318 AH (earliest row in UAQ table)
|
||||
['1900-04-30', 1318, 1, 1],
|
||||
['1900-05-29', 1318, 2, 1],
|
||||
['1900-12-23', 1318, 9, 1],
|
||||
|
||||
// 1400 AH — Islamic New Year (well-known reference)
|
||||
['1979-11-20', 1400, 1, 1],
|
||||
['1979-12-20', 1400, 2, 1],
|
||||
['1980-01-18', 1400, 3, 1],
|
||||
['1980-02-17', 1400, 4, 1],
|
||||
['1980-03-17', 1400, 5, 1],
|
||||
['1980-04-16', 1400, 6, 1],
|
||||
['1980-05-15', 1400, 7, 1],
|
||||
['1980-06-13', 1400, 8, 1],
|
||||
['1980-07-13', 1400, 9, 1],
|
||||
['1980-08-11', 1400, 10, 1],
|
||||
['1980-09-10', 1400, 11, 1],
|
||||
['1980-10-10', 1400, 12, 1],
|
||||
|
||||
// Spot checks in various years
|
||||
['1961-04-25', 1380, 11, 10],
|
||||
['1963-12-07', 1383, 7, 21],
|
||||
['1990-01-10', 1410, 6, 13],
|
||||
['1995-09-06', 1416, 4, 11],
|
||||
['1997-05-06', 1417, 12, 29],
|
||||
['1997-12-30', 1418, 9, 1],
|
||||
|
||||
// 1420 AH
|
||||
['2000-01-07', 1420, 9, 30],
|
||||
|
||||
// 1422–1430
|
||||
['2001-11-16', 1422, 9, 1],
|
||||
['2003-07-01', 1424, 5, 1],
|
||||
['2006-02-10', 1427, 1, 11],
|
||||
['2007-10-12', 1428, 9, 30],
|
||||
['2009-04-06', 1430, 4, 10],
|
||||
|
||||
// Ramadan start dates 1431–1439
|
||||
['2010-08-11', 1431, 9, 1],
|
||||
['2011-08-01', 1432, 9, 1],
|
||||
['2012-07-20', 1433, 9, 1],
|
||||
['2013-07-09', 1434, 9, 1],
|
||||
['2014-06-28', 1435, 9, 1],
|
||||
['2015-06-18', 1436, 9, 1],
|
||||
['2016-06-06', 1437, 9, 1],
|
||||
['2017-05-27', 1438, 9, 1],
|
||||
['2018-05-16', 1439, 9, 1],
|
||||
|
||||
// Ramadan start dates 1440–1450
|
||||
['2019-05-06', 1440, 9, 1],
|
||||
['2020-04-24', 1441, 9, 1],
|
||||
['2021-04-13', 1442, 9, 1],
|
||||
['2022-04-02', 1443, 9, 1],
|
||||
['2023-03-23', 1444, 9, 1],
|
||||
['2024-03-11', 1445, 9, 1],
|
||||
['2025-03-01', 1446, 9, 1],
|
||||
['2026-02-18', 1447, 9, 1],
|
||||
['2027-02-08', 1448, 9, 1],
|
||||
['2028-01-28', 1449, 9, 1],
|
||||
['2029-01-16', 1450, 9, 1],
|
||||
|
||||
// Future years
|
||||
['2039-09-19', 1461, 9, 1],
|
||||
['2040-09-07', 1462, 9, 1],
|
||||
];
|
||||
|
||||
// ─── ICOP Ramadan/Eid reference data (UAQ) ───────────────────────────────────
|
||||
//
|
||||
// Eid al-Fitr (1 Shawwal = month 10) for 1440–1450 AH.
|
||||
// These dates are cross-referenced against Islamic calendar sources
|
||||
// and the UAQ table in the library.
|
||||
|
||||
const ICOP_EID_UAQ = [
|
||||
['2019-06-04', 1440, 10, 1],
|
||||
['2020-05-24', 1441, 10, 1],
|
||||
['2021-05-13', 1442, 10, 1],
|
||||
['2022-05-02', 1443, 10, 1],
|
||||
['2023-04-21', 1444, 10, 1],
|
||||
['2024-04-10', 1445, 10, 1],
|
||||
['2025-03-30', 1446, 10, 1],
|
||||
['2026-03-20', 1447, 10, 1],
|
||||
['2027-03-09', 1448, 10, 1],
|
||||
['2028-02-26', 1449, 10, 1],
|
||||
['2029-02-14', 1450, 10, 1],
|
||||
];
|
||||
|
||||
// ─── UAQ toHijri tests ───────────────────────────────────────────────────────
|
||||
|
||||
describe('UAQ spot-check — toHijri', () => {
|
||||
for (const [iso, hy, hm, hd] of UAQ_SPOT_CHECKS) {
|
||||
const label = `${iso} → ${hy}-${String(hm).padStart(2,'0')}-${String(hd).padStart(2,'0')}`;
|
||||
it(label, () => {
|
||||
const result = toHijri(g(iso));
|
||||
assert.ok(result, `toHijri(${iso}) returned null`);
|
||||
assert.strictEqual(result.hy, hy, `year: got ${result.hy}, want ${hy}`);
|
||||
assert.strictEqual(result.hm, hm, `month: got ${result.hm}, want ${hm}`);
|
||||
assert.strictEqual(result.hd, hd, `day: got ${result.hd}, want ${hd}`);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// ─── UAQ toGregorian tests ───────────────────────────────────────────────────
|
||||
|
||||
describe('UAQ spot-check — toGregorian', () => {
|
||||
for (const [iso, hy, hm, hd] of UAQ_SPOT_CHECKS) {
|
||||
const label = `${hy}-${String(hm).padStart(2,'0')}-${String(hd).padStart(2,'0')} → ${iso}`;
|
||||
it(label, () => {
|
||||
const result = toGregorian(hy, hm, hd);
|
||||
assert.ok(result instanceof Date, `toGregorian returned non-Date`);
|
||||
const resultIso = result.toISOString().slice(0, 10);
|
||||
assert.strictEqual(resultIso, iso, `got ${resultIso}, want ${iso}`);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// ─── ICOP Ramadan roundtrip ──────────────────────────────────────────────────
|
||||
|
||||
describe('Eid al-Fitr 1440-1450 AH — toHijri', () => {
|
||||
for (const [iso, hy, hm, hd] of ICOP_EID_UAQ) {
|
||||
const label = `${iso} → ${hy}/${hm}/${hd}`;
|
||||
it(label, () => {
|
||||
const result = toHijri(g(iso));
|
||||
assert.ok(result, `toHijri(${iso}) returned null`);
|
||||
assert.strictEqual(result.hy, hy, `year: got ${result.hy}`);
|
||||
assert.strictEqual(result.hm, hm, `month: got ${result.hm}`);
|
||||
assert.strictEqual(result.hd, hd, `day: got ${result.hd}`);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('Eid al-Fitr 1440-1450 AH — toGregorian', () => {
|
||||
for (const [iso, hy, hm, hd] of ICOP_EID_UAQ) {
|
||||
const label = `toGregorian(${hy},${hm},${hd}) → ${iso}`;
|
||||
it(label, () => {
|
||||
const result = toGregorian(hy, hm, hd);
|
||||
assert.ok(result instanceof Date, `toGregorian returned non-Date`);
|
||||
const resultIso = result.toISOString().slice(0, 10);
|
||||
assert.strictEqual(resultIso, iso, `got ${resultIso}, want ${iso}`);
|
||||
});
|
||||
}
|
||||
});
|
||||
48
test.mjs
48
test.mjs
|
|
@ -95,23 +95,23 @@ describe('toGregorian - error cases', () => {
|
|||
|
||||
describe('toHijri - known dates', () => {
|
||||
it('2022-07-30 = 1 Muharram 1444', () => {
|
||||
const h = toHijri(new Date(2022, 6, 30, 12));
|
||||
const h = toHijri(new Date(Date.UTC(2022, 6, 30)));
|
||||
assert.deepEqual(h, { hy: 1444, hm: 1, hd: 1 });
|
||||
});
|
||||
it('2023-03-23 = 1 Ramadan 1444', () => {
|
||||
const h = toHijri(new Date(2023, 2, 23, 12));
|
||||
const h = toHijri(new Date(Date.UTC(2023, 2, 23)));
|
||||
assert.deepEqual(h, { hy: 1444, hm: 9, hd: 1 });
|
||||
});
|
||||
it('2023-04-21 = 1 Shawwal 1444', () => {
|
||||
const h = toHijri(new Date(2023, 3, 21, 12));
|
||||
const h = toHijri(new Date(Date.UTC(2023, 3, 21)));
|
||||
assert.deepEqual(h, { hy: 1444, hm: 10, hd: 1 });
|
||||
});
|
||||
it('2024-07-07 = 1 Muharram 1446', () => {
|
||||
const h = toHijri(new Date(2024, 6, 7, 12));
|
||||
const h = toHijri(new Date(Date.UTC(2024, 6, 7)));
|
||||
assert.deepEqual(h, { hy: 1446, hm: 1, hd: 1 });
|
||||
});
|
||||
it('1900-04-30 = 1 Muharram 1318 (first table entry)', () => {
|
||||
const h = toHijri(new Date(1900, 3, 30, 12));
|
||||
const h = toHijri(new Date(Date.UTC(1900, 3, 30)));
|
||||
assert.deepEqual(h, { hy: 1318, hm: 1, hd: 1 });
|
||||
});
|
||||
});
|
||||
|
|
@ -274,15 +274,15 @@ describe('FCNA toGregorian', () => {
|
|||
|
||||
describe('FCNA toHijri', () => {
|
||||
it('2025-03-01 = 1 Ramadan 1446', () => {
|
||||
const h = toHijri(new Date(2025, 2, 1, 12), FCNA);
|
||||
const h = toHijri(new Date(Date.UTC(2025, 2, 1)), FCNA);
|
||||
assert.deepEqual(h, { hy: 1446, hm: 9, hd: 1 });
|
||||
});
|
||||
it('2025-03-30 = 1 Shawwal 1446', () => {
|
||||
const h = toHijri(new Date(2025, 2, 30, 12), FCNA);
|
||||
const h = toHijri(new Date(Date.UTC(2025, 2, 30)), FCNA);
|
||||
assert.deepEqual(h, { hy: 1446, hm: 10, hd: 1 });
|
||||
});
|
||||
it('2024-03-11 = 1 Ramadan 1445', () => {
|
||||
const h = toHijri(new Date(2024, 2, 11, 12), FCNA);
|
||||
const h = toHijri(new Date(Date.UTC(2024, 2, 11)), FCNA);
|
||||
assert.deepEqual(h, { hy: 1445, hm: 9, hd: 1 });
|
||||
});
|
||||
});
|
||||
|
|
@ -312,6 +312,36 @@ describe('FCNA round-trips', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('UAQ round-trips (default engine)', () => {
|
||||
it('1444/1/1 toGregorian then toHijri', () => {
|
||||
const greg = toGregorian(1444, 1, 1);
|
||||
const hijri = toHijri(greg);
|
||||
assert.deepEqual(hijri, { hy: 1444, hm: 1, hd: 1 });
|
||||
});
|
||||
it('1444/9/1 toGregorian then toHijri', () => {
|
||||
const greg = toGregorian(1444, 9, 1);
|
||||
const hijri = toHijri(greg);
|
||||
assert.deepEqual(hijri, { hy: 1444, hm: 9, hd: 1 });
|
||||
});
|
||||
it('1446/9/1 toGregorian then toHijri', () => {
|
||||
const greg = toGregorian(1446, 9, 1);
|
||||
const hijri = toHijri(greg);
|
||||
assert.deepEqual(hijri, { hy: 1446, hm: 9, hd: 1 });
|
||||
});
|
||||
it('1318/1/1 toGregorian then toHijri (first table entry)', () => {
|
||||
const greg = toGregorian(1318, 1, 1);
|
||||
assert(greg instanceof Date);
|
||||
const hijri = toHijri(greg);
|
||||
assert.deepEqual(hijri, { hy: 1318, hm: 1, hd: 1 });
|
||||
});
|
||||
it('1500/12/29 toGregorian then toHijri (last table entry)', () => {
|
||||
const greg = toGregorian(1500, 12, 29);
|
||||
assert(greg instanceof Date);
|
||||
const hijri = toHijri(greg);
|
||||
assert.deepEqual(hijri, { hy: 1500, hm: 12, hd: 29 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('FCNA isValidHijriDate', () => {
|
||||
it('1446/9/1 = true', () => assert.strictEqual(isValidHijriDate(1446, 9, 1, FCNA), true));
|
||||
it('month 0 = false', () => assert.strictEqual(isValidHijriDate(1446, 0, 1, FCNA), false));
|
||||
|
|
@ -329,7 +359,7 @@ describe('UAQ default regression', () => {
|
|||
assert.strictEqual(d.toISOString().slice(0, 10), '2025-03-01');
|
||||
});
|
||||
it('toHijri still works without options', () => {
|
||||
const h = toHijri(new Date(2023, 2, 23, 12));
|
||||
const h = toHijri(new Date(Date.UTC(2023, 2, 23)));
|
||||
assert.deepEqual(h, { hy: 1444, hm: 9, hd: 1 });
|
||||
});
|
||||
it('isValidHijriDate still works without options', () => {
|
||||
|
|
|
|||
|
|
@ -1,18 +1,10 @@
|
|||
{
|
||||
"extends": "@acamarata/tsconfig/tsconfig.library.json",
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"outDir": "dist",
|
||||
"esModuleInterop": true,
|
||||
"rootDir": "src",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
|
|
|
|||
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": false,
|
||||
"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: ["luxon-hijri.test.ts"],
|
||||
},
|
||||
});
|
||||
Loading…
Reference in a new issue