chore: E6 polish wiki + CI + TypeDoc integration (P1)

This commit is contained in:
Aric Camarata 2026-05-30 18:38:38 -04:00
parent c89dbaf5c7
commit 546ec2d302
9 changed files with 266 additions and 271 deletions

View file

@ -13,6 +13,17 @@
- [Architecture](Architecture)
- [Benchmarks](benchmarks/index)
**API Pages**
- [plugin (default)](api/plugin)
- [toHijri](api/toHijri)
- [isValidHijri](api/isValidHijri)
- [hijriYear](api/hijriYear)
- [hijriMonth](api/hijriMonth)
- [hijriDay](api/hijriDay)
- [formatHijri](api/formatHijri)
- [fromHijri](api/fromHijri)
- [registerCalendar](api/registerCalendar)
**Community**
- [Contributing](CONTRIBUTING)
- [Code of Conduct](CODE_OF_CONDUCT)

View file

@ -8,7 +8,7 @@
> `const` **default**: `PluginFunc`
Defined in: [src/index.ts:164](https://github.com/acamarata/dayjs-hijri-plus/blob/599c7481510f5ea625b79c98a7030b9c8d90e4f7/src/index.ts#L164)
Defined in: [src/index.ts:164](https://github.com/acamarata/dayjs-hijri-plus/blob/033ca475760db2b9e4c6edd69cc9dc1f013b8d8f/src/index.ts#L164)
Day.js plugin that adds Hijri calendar support.

View file

@ -4,9 +4,9 @@
```typescript
import dayjs from 'dayjs';
import hijri from 'dayjs-hijri-plus';
import hijriPlugin from 'dayjs-hijri-plus';
dayjs.extend(hijri);
dayjs.extend(hijriPlugin);
```
## Convert today's date to Hijri
@ -14,9 +14,11 @@ dayjs.extend(hijri);
```typescript
const today = dayjs();
const h = today.toHijri();
// { hy: 1444, hm: 9, hd: 1 } (example — actual output depends on the current date)
// { hy: 1446, hm: 11, hd: 3 } (example — actual output depends on today's date)
console.log(`${h.hd} / ${h.hm} / ${h.hy}`);
if (h !== null) {
console.log(`${h.hd} / ${h.hm} / ${h.hy}`);
}
```
## Convert a known Gregorian date
@ -26,16 +28,16 @@ console.log(`${h.hd} / ${h.hm} / ${h.hy}`);
const d = dayjs('2023-03-23');
const h = d.toHijri();
console.log(h.hy); // 1444
console.log(h.hm); // 9 (Ramadan is the 9th month)
console.log(h.hd); // 1
console.log(h?.hy); // 1444
console.log(h?.hm); // 9 (Ramadan is the 9th month)
console.log(h?.hd); // 1
```
## Convert from Hijri to Gregorian
```typescript
const gregorian = dayjs.fromHijri(1444, 9, 1);
console.log(gregorian.format('YYYY-MM-DD')); // '2023-03-23'
console.log(gregorian.format('YYYY-MM-DD')); // '2023-03-23'
```
## Hijri accessor methods
@ -43,9 +45,9 @@ console.log(gregorian.format('YYYY-MM-DD')); // '2023-03-23'
```typescript
const d = dayjs('2023-03-23');
console.log(d.iYear()); // 1444
console.log(d.iMonth()); // 9
console.log(d.iDate()); // 1
console.log(d.hijriYear()); // 1444
console.log(d.hijriMonth()); // 9
console.log(d.hijriDay()); // 1
```
## Format with Hijri tokens
@ -53,10 +55,10 @@ console.log(d.iDate()); // 1
```typescript
const d = dayjs('2023-03-23');
d.format('iD iMMMM iYYYY'); // '1 Ramadan 1444'
d.format('iDD/iMM/iYYYY'); // '01/09/1444'
d.format('YYYY-MM-DD'); // '2023-03-23' (Gregorian tokens still work)
d.format('YYYY (iYYYY/iM/iD)'); // '2023 (1444/9/1)'
d.formatHijri('iD iMMMM iYYYY'); // '1 Ramadan 1444'
d.formatHijri('iDD/iMM/iYYYY'); // '01/09/1444'
d.formatHijri('YYYY-MM-DD'); // 'YYYY-MM-DD' — use .format() for Gregorian-only
d.formatHijri('YYYY (iYYYY/iM/iD)'); // '2023 (1444/9/1)'
```
## Use FCNA calendar
@ -65,20 +67,20 @@ d.format('YYYY (iYYYY/iM/iD)'); // '2023 (1444/9/1)'
const d = dayjs('2023-03-23');
const fcna = d.toHijri({ calendar: 'fcna' });
console.log(fcna.hy); // 1444
console.log(fcna.hm); // 9
console.log(fcna.hd); // 1
// Near month boundaries, UAQ and FCNA may differ by one day
console.log(fcna?.hy); // 1444
console.log(fcna?.hm); // 9
console.log(fcna?.hd); // 1 or 2 depending on the month boundary
```
## CJS usage
```javascript
const dayjs = require('dayjs');
const hijri = require('dayjs-hijri-plus');
const hijriPlugin = require('dayjs-hijri-plus');
dayjs.extend(hijri);
dayjs.extend(hijriPlugin);
const d = dayjs('2023-03-23');
console.log(d.iYear()); // 1444
console.log(d.hijriYear()); // 1444
```

View file

@ -7,61 +7,60 @@
| `iYYYY` | Full Hijri year | `1444` |
| `iYY` | 2-digit Hijri year | `44` |
| `iMMMM` | Full month name | `Ramadan` |
| `iMMM` | Abbreviated month name | `Ram` |
| `iMMM` | Abbreviated month name | `Ramadan` |
| `iMM` | 2-digit month number | `09` |
| `iM` | Month number | `9` |
| `iDD` | 2-digit day | `01` |
| `iD` | Day number | `1` |
| `iEEEE` | Full weekday name | `Yawm al-Khamis` |
| `iEEE` | Short weekday name | `Kham` |
| `iE` | Weekday number | `5` (1=Sunday) |
| `ioooo` | Era | `AH` |
Tokens not prefixed with `i` are passed through to Day.js as Gregorian tokens.
Tokens not prefixed with `i` pass through to Day.js `.format()` as Gregorian tokens.
## Common format patterns
```typescript
import dayjs from 'dayjs';
import hijri from 'dayjs-hijri-plus';
import hijriPlugin from 'dayjs-hijri-plus';
dayjs.extend(hijri);
dayjs.extend(hijriPlugin);
const d = dayjs('2023-03-23');
// Day Month Year (long)
d.format('iD iMMMM iYYYY');
d.formatHijri('iD iMMMM iYYYY');
// '1 Ramadan 1444'
// Numeric date
d.format('iDD/iMM/iYYYY');
d.formatHijri('iDD/iMM/iYYYY');
// '01/09/1444'
// Short month name
d.format('iD iMMM iYYYY');
// '1 Ram 1444'
d.formatHijri('iD iMMM iYYYY');
// '1 Ramadan 1444'
// Combined Gregorian and Hijri
d.format('YYYY-MM-DD (iD iMMMM iYYYY)');
d.formatHijri('YYYY-MM-DD (iD iMMMM iYYYY)');
// '2023-03-23 (1 Ramadan 1444)'
// ISO-style Hijri
d.format('iYYYY-iMM-iDD');
d.formatHijri('iYYYY-iMM-iDD');
// '1444-09-01'
```
## Hijri month names
```typescript
const months = [
'Muharram', 'Safar', "Rabi' al-Awwal", "Rabi' al-Thani",
"Jumada al-Awwal", "Jumada al-Thani", 'Rajab', "Sha'ban",
'Ramadan', 'Shawwal', "Dhu al-Qa'dah", "Dhu al-Hijjah"
];
// iMMMM returns the standard transliteration for each month
for (let m = 1; m <= 12; m++) {
const d = dayjs.fromHijri(1444, m, 1);
console.log(d.format('iM iMMMM'));
console.log(d.formatHijri('iM iMMMM'));
}
// 1 Muharram
// 2 Safar
// 3 Rabi' al-Awwal
// ...
// 9 Ramadan
// ...
@ -72,26 +71,26 @@ for (let m = 1; m <= 12; m++) {
```tsx
import dayjs from 'dayjs';
import hijri from 'dayjs-hijri-plus';
import hijriPlugin from 'dayjs-hijri-plus';
dayjs.extend(hijri);
dayjs.extend(hijriPlugin);
interface HijriDateProps {
interface HijriDateDisplayProps {
date: Date;
}
function HijriDate({ date }: HijriDateProps) {
function HijriDateDisplay({ date }: HijriDateDisplayProps) {
const d = dayjs(date);
const gregorian = d.format('YYYY-MM-DD');
const hijriFormatted = d.format('iD iMMMM iYYYY');
const gregorianIso = d.format('YYYY-MM-DD');
const hijriFormatted = d.formatHijri('iD iMMMM iYYYY');
return (
<time dateTime={gregorian}>
{hijriFormatted}
<time dateTime={gregorianIso}>
{hijriFormatted || gregorianIso}
</time>
);
}
// Usage: <HijriDate date={new Date('2023-03-23')} />
// Usage: <HijriDateDisplay date={new Date('2023-03-23')} />
// Renders: <time datetime="2023-03-23">1 Ramadan 1444</time>
```

View file

@ -6,71 +6,74 @@ Each method accepts an optional options argument. You can mix UAQ and FCNA in th
```typescript
import dayjs from 'dayjs';
import hijri from 'dayjs-hijri-plus';
import hijriPlugin from 'dayjs-hijri-plus';
dayjs.extend(hijri);
dayjs.extend(hijriPlugin);
const d = dayjs('2023-03-23');
const uaqYear = d.iYear(); // UAQ (default)
const fcnaYear = d.iYear({ calendar: 'fcna' }); // FCNA
const uaqYear = d.hijriYear(); // UAQ (default)
const fcnaYear = d.hijriYear({ calendar: 'fcna' }); // FCNA
```
Near month boundaries, UAQ and FCNA may differ by one day. The calendar argument is per-call, not session-wide.
## Null safety
`d.toHijri()` (if the package exposes it) returns `null` for dates outside UAQ range (approximately 1900-2076 CE). Guard before using:
`toHijri()` returns `null` for dates outside the UAQ range (approximately 1900-2076 CE). Guard before using the result:
```typescript
const hijri = d.toHijri();
if (hijri !== null) {
console.log(hijri.hy, hijri.hm, hijri.hd);
const h = d.toHijri();
if (h !== null) {
console.log(h.hy, h.hm, h.hd);
}
```
Use `isValidHijri()` when you only need a boolean check.
## Combining with Day.js plugins
dayjs-hijri-plus works alongside other Day.js plugins. The order of `extend()` calls matters when two plugins patch the same method:
dayjs-hijri-plus works alongside other Day.js plugins. The order of `extend()` calls matters when two plugins patch the same method. Register `dayjs-hijri-plus` after plugins that also modify `.format()`:
```typescript
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone';
import hijri from 'dayjs-hijri-plus';
import hijriPlugin from 'dayjs-hijri-plus';
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.extend(hijri);
dayjs.extend(hijriPlugin);
// UTC-aware conversion
const d = dayjs.utc('2023-03-23');
console.log(d.iYear()); // 1444
console.log(d.hijriYear()); // 1444
```
## Formatting alongside Gregorian tokens
## Mixing Gregorian and Hijri tokens
Hijri tokens (`iYYYY`, `iMM`, `iDD`, `iMMMM`, etc.) coexist with Day.js Gregorian tokens. Use them in the same format string:
Hijri tokens (`iYYYY`, `iMM`, `iDD`, `iMMMM`, etc.) coexist with Day.js Gregorian tokens in `formatHijri`:
```typescript
d.format('YYYY-MM-DD (iD iMMMM iYYYY)');
d.formatHijri('YYYY-MM-DD (iD iMMMM iYYYY)');
// '2023-03-23 (1 Ramadan 1444)'
```
Standard Day.js `.format()` still handles Gregorian-only strings normally. Use `formatHijri` only when you need Hijri tokens.
## Tree-shaking
The package ships both ESM and CJS builds. In ESM bundlers (Vite, esbuild, Rollup), unused code is eliminated. The plugin itself is ~2 KB min+gz on top of Day.js.
The package ships both ESM and CJS builds. ESM bundlers (Vite, esbuild, Rollup) eliminate unused code. The plugin is approximately 2 KB min+gz on top of Day.js.
## TypeScript augmentation
The plugin augments the Day.js type definitions automatically. You do not need to import any separate types file:
The plugin augments the Day.js type definitions automatically. No separate type import is needed:
```typescript
import dayjs from 'dayjs';
import hijri from 'dayjs-hijri-plus';
import hijriPlugin from 'dayjs-hijri-plus';
dayjs.extend(hijri);
dayjs.extend(hijriPlugin);
const d = dayjs('2023-03-23');
const year: number = d.iYear(); // fully typed
const year: number = d.hijriYear()!; // fully typed; assert non-null if date is in range
```

View file

@ -1,6 +1,6 @@
# Quick Start
This guide covers the most common use cases in dayjs-hijri-plus. All examples use the default Umm al-Qura (UAQ) calendar.
This guide covers the most common use cases. All examples use the default Umm al-Qura (UAQ) calendar.
## Installation
@ -14,9 +14,9 @@ pnpm add dayjs dayjs-hijri-plus hijri-core
```typescript
import dayjs from 'dayjs';
import hijri from 'dayjs-hijri-plus';
import hijriPlugin from 'dayjs-hijri-plus';
dayjs.extend(hijri);
dayjs.extend(hijriPlugin);
```
After extending, all `dayjs()` instances gain Hijri methods.
@ -25,32 +25,32 @@ After extending, all `dayjs()` instances gain Hijri methods.
```typescript
import dayjs from 'dayjs';
import hijri from 'dayjs-hijri-plus';
import hijriPlugin from 'dayjs-hijri-plus';
dayjs.extend(hijri);
dayjs.extend(hijriPlugin);
const d = dayjs('2023-03-23'); // 1 Ramadan 1444
console.log(d.iYear()); // 1444
console.log(d.iMonth()); // 9
console.log(d.iDate()); // 1
console.log(d.hijriYear()); // 1444
console.log(d.hijriMonth()); // 9
console.log(d.hijriDay()); // 1
```
## Format with Hijri tokens
```typescript
d.format('iYYYY/iMM/iDD'); // '1444/09/01'
d.format('iD iMMMM iYYYY'); // '1 Ramadan 1444'
d.formatHijri('iYYYY/iMM/iDD'); // '1444/09/01'
d.formatHijri('iD iMMMM iYYYY'); // '1 Ramadan 1444'
```
Hijri format tokens are prefixed with `i` to avoid conflicts with Day.js Gregorian tokens.
Hijri tokens are prefixed with `i` to avoid conflicts with Day.js Gregorian tokens.
## Convert a Hijri date to a Day.js object
```typescript
import dayjs from 'dayjs';
import hijri from 'dayjs-hijri-plus';
import hijriPlugin from 'dayjs-hijri-plus';
dayjs.extend(hijri);
dayjs.extend(hijriPlugin);
const d = dayjs.fromHijri(1444, 9, 1);
console.log(d.format('YYYY-MM-DD')); // '2023-03-23'
@ -60,23 +60,24 @@ console.log(d.format('YYYY-MM-DD')); // '2023-03-23'
```typescript
const d = dayjs('2023-03-23');
console.log(d.iYear({ calendar: 'fcna' })); // 1444
console.log(d.iMonth({ calendar: 'fcna' })); // 9 or differs by a day near month start
const h = d.toHijri({ calendar: 'fcna' });
// Near month boundaries, UAQ and FCNA may differ by one day
console.log(h?.hy, h?.hm, h?.hd);
```
## CommonJS
```js
const dayjs = require('dayjs');
const hijri = require('dayjs-hijri-plus');
const hijriPlugin = require('dayjs-hijri-plus');
dayjs.extend(hijri);
dayjs.extend(hijriPlugin);
const d = dayjs('2023-03-23');
console.log(d.iYear(), d.iMonth(), d.iDate()); // 1444 9 1
console.log(d.hijriYear(), d.hijriMonth(), d.hijriDay()); // 1444 9 1
```
## Next steps
- [API Reference](API-Reference) for the full method list
- [Architecture](Architecture) for how the plugin integrates with Day.js
- [API Reference](../API-Reference) for the full method list
- [Architecture](../Architecture) for how the plugin integrates with Day.js

View file

@ -78,3 +78,24 @@ 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
- 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

182
README.md
View file

@ -4,9 +4,9 @@
[![CI](https://github.com/acamarata/dayjs-hijri-plus/actions/workflows/ci.yml/badge.svg)](https://github.com/acamarata/dayjs-hijri-plus/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
A Day.js plugin that adds Hijri calendar support. Converts Gregorian dates to and from Hijri, provides Hijri-aware formatting, and delegates all calendar logic to [hijri-core](https://github.com/acamarata/hijri-core). Keeps this package thin and testable.
A Day.js plugin that adds Hijri calendar support. Converts Gregorian dates to and from Hijri, provides Hijri-aware formatting, and delegates all calendar logic to [hijri-core](https://github.com/acamarata/hijri-core).
Supports Umm al-Qura (UAQ) and FCNA/ISNA calendars out of the box. Custom calendar engines can be registered at runtime.
Supports Umm al-Qura (UAQ) and FCNA/ISNA calendars. Custom engines can be registered at runtime.
## Installation
@ -14,7 +14,7 @@ Supports Umm al-Qura (UAQ) and FCNA/ISNA calendars out of the box. Custom calend
pnpm add dayjs dayjs-hijri-plus hijri-core
```
Both `dayjs` and `hijri-core` are peer dependencies and must be installed alongside this plugin.
Both `dayjs` and `hijri-core` are peer dependencies.
## Quick Start
@ -24,183 +24,23 @@ import hijriPlugin from 'dayjs-hijri-plus';
dayjs.extend(hijriPlugin);
// Convert a Gregorian date to Hijri
const d = dayjs(new Date(2023, 2, 23));
const hijri = d.toHijri();
// => { hy: 1444, hm: 9, hd: 1 } (1 Ramadan 1444 AH)
const d = dayjs('2023-03-23');
d.toHijri(); // { hy: 1444, hm: 9, hd: 1 }
d.formatHijri('iD iMMMM iYYYY'); // '1 Ramadan 1444'
d.formatHijri('iYYYY-iMM-iDD'); // '1444-09-01'
// Format using Hijri tokens mixed with standard Day.js tokens
d.formatHijri('iYYYY-iMM-iDD'); // => '1444-09-01'
d.formatHijri('iD iMMMM iYYYY'); // => '1 Ramadan 1444'
d.formatHijri('iD iMMMM iYYYY [at] HH:mm'); // => '1 Ramadan 1444 at 00:00'
// Individual Hijri components
d.hijriYear(); // => 1444
d.hijriMonth(); // => 9
d.hijriDay(); // => 1
// Construct a Day.js instance from a Hijri date
const eid = dayjs.fromHijri(1444, 10, 1);
eid.format('YYYY-MM-DD'); // => '2023-04-21'
// FCNA/ISNA calendar variant
d.toHijri({ calendar: 'fcna' }); // => { hy: 1444, hm: 9, hd: 2 } (varies by month)
dayjs.fromHijri(1444, 10, 1).format('YYYY-MM-DD'); // '2023-04-21'
```
## API
### dayjs.extend(hijriPlugin)
Register the plugin with your Day.js instance. Call once before using any plugin methods.
### Instance Methods
#### `.toHijri(opts?)`
Convert the Day.js date to a Hijri date object.
| Parameter | Type | Description |
| --- | --- | --- |
| `opts` | `ConversionOptions` | Optional. `{ calendar: 'uaq' \| 'fcna' \| string }` |
Returns `HijriDate | null`. Returns `null` if the date is outside the supported range (UAQ: AH 1318-1500 / 1900-2076 CE).
```ts
dayjs('2023-03-23').toHijri();
// => { hy: 1444, hm: 9, hd: 1 }
```
#### `.isValidHijri(opts?)`
Check whether the date maps to a valid Hijri date in the supported range.
Returns `boolean`.
#### `.hijriYear(opts?)`
Returns the Hijri year as a `number`, or `null` if out of range.
#### `.hijriMonth(opts?)`
Returns the Hijri month (1-12) as a `number`, or `null` if out of range.
#### `.hijriDay(opts?)`
Returns the Hijri day (1-30) as a `number`, or `null` if out of range.
#### `.formatHijri(formatStr, opts?)`
Format the date using a mix of Hijri tokens and standard Day.js tokens.
| Parameter | Type | Description |
| --- | --- | --- |
| `formatStr` | `string` | Format string. See token table below. |
| `opts` | `ConversionOptions` | Optional calendar selection |
Returns `string`. Returns an empty string if the date is out of range.
Hijri tokens are replaced first. The resulting string is then passed to Day.js `.format()`, so all standard tokens (YYYY, MM, DD, HH, mm, ss, etc.) resolve normally.
### Static Methods
#### `dayjs.fromHijri(hy, hm, hd, opts?)`
Construct a Day.js instance from a Hijri date.
| Parameter | Type | Description |
| --- | --- | --- |
| `hy` | `number` | Hijri year |
| `hm` | `number` | Hijri month (1-12) |
| `hd` | `number` | Hijri day (1-30) |
| `opts` | `ConversionOptions` | Optional calendar selection |
Returns a `dayjs.Dayjs` instance. Throws `Error` if the Hijri date is invalid or outside the table range.
## Format Tokens
All Hijri-specific tokens use the `i` prefix.
| Token | Example | Description |
| --- | --- | --- |
| `iYYYY` | `1444` | 4-digit Hijri year |
| `iYY` | `44` | 2-digit Hijri year |
| `iMMMM` | `Ramadan` | Full Hijri month name |
| `iMMM` | `Ramadan` | Medium Hijri month name |
| `iMM` | `09` | Zero-padded Hijri month number |
| `iM` | `9` | Hijri month number |
| `iDD` | `01` | Zero-padded Hijri day |
| `iD` | `1` | Hijri day number |
| `iEEEE` | `Yawm al-Khamis` | Full weekday name |
| `iEEE` | `Kham` | Short weekday name |
| `iE` | `5` | Weekday number (1=Sunday ... 7=Saturday) |
| `ioooo` | `AH` | Era (Anno Hegirae) |
| `iooo` | `AH` | Era (short form, same as ioooo) |
Standard Day.js tokens pass through untouched. Square-bracket escaping (`[literal text]`) also works as expected.
## Calendar Systems
Two calendars ship with hijri-core:
- **`uaq`** (default): Umm al-Qura, the official calendar of Saudi Arabia. Table-based, covers 1318-1500 AH (1900-2076 CE).
- **`fcna`**: Fiqh Council of North America calendar. Uses an astronomical calculation with fixed criteria, independent of moon sighting.
Select a calendar by passing `{ calendar: 'fcna' }` to any method. The default is `'uaq'` when no option is provided.
Custom calendar engines can be registered:
```ts
import { registerCalendar } from 'dayjs-hijri-plus';
import type { CalendarEngine } from 'dayjs-hijri-plus';
const myEngine: CalendarEngine = { ... };
registerCalendar('my-calendar', myEngine);
dayjs().toHijri({ calendar: 'my-calendar' });
```
See the [hijri-core CalendarEngine interface](https://github.com/acamarata/hijri-core) for the full contract.
## TypeScript
Full TypeScript support is included. The plugin augments the Day.js module to add types for all instance and static methods.
```ts
import type { HijriDate, ConversionOptions } from 'dayjs-hijri-plus';
const h: HijriDate = dayjs().toHijri()!;
const opts: ConversionOptions = { calendar: 'fcna' };
```
No `@types` package is needed.
## Architecture
A thin plugin wrapper over [hijri-core](https://github.com/acamarata/hijri-core). The plugin augments the Day.js prototype with Hijri methods, each delegating to the registered calendar engine. Zero global state: calendar selection is passed per call.
For more detail see the [Architecture wiki page](https://github.com/acamarata/dayjs-hijri-plus/wiki/Architecture).
## Documentation
Full API reference, architecture notes, and calendar system comparisons are on the [GitHub Wiki](https://github.com/acamarata/dayjs-hijri-plus/wiki).
Full API reference, examples, and architecture notes are on the [GitHub Wiki](https://github.com/acamarata/dayjs-hijri-plus/wiki).
## Related
- [hijri-core](https://github.com/acamarata/hijri-core): zero-dependency Hijri calendar engine this plugin wraps
- [luxon-hijri](https://github.com/acamarata/luxon-hijri): the same Hijri conversion for Luxon users
- [hijri-core](https://github.com/acamarata/hijri-core): the zero-dependency Hijri calendar engine this plugin wraps
- [luxon-hijri](https://github.com/acamarata/luxon-hijri): the same conversion for Luxon users
- [pray-calc](https://github.com/acamarata/pray-calc): Islamic prayer time calculation
- [nrel-spa](https://github.com/acamarata/nrel-spa): NREL Solar Position Algorithm in pure JavaScript
## Compatibility
- Node.js 20, 22, 24
- Day.js 1.x (peer dependency)
- ESM and CJS builds included
- TypeScript definitions bundled
## Acknowledgments
Calendar data and algorithms provided by [hijri-core](https://github.com/acamarata/hijri-core). The Umm al-Qura table is derived from data published by the King Abdulaziz City for Science and Technology (KACST). FCNA new moon calculations follow Jean Meeus, "Astronomical Algorithms," 2nd ed., Chapter 49.
## License

View file

@ -5,24 +5,83 @@ import type { ConversionOptions, HijriDate } from './types';
// Augment Day.js to expose plugin methods on the instance type.
declare module 'dayjs' {
interface Dayjs {
/** Convert to a Hijri date. Returns null if outside the supported range. */
/**
* Convert the Day.js date to a Hijri date.
*
* @param opts - Optional calendar selection. Defaults to `{ calendar: 'uaq' }`.
* @returns `{ hy, hm, hd }` on success, or `null` if the date is outside the
* supported range (UAQ: 1318-1500 AH / approximately 1900-2076 CE).
* @example
* dayjs('2023-03-23').toHijri();
* // => { hy: 1444, hm: 9, hd: 1 }
*/
toHijri(opts?: ConversionOptions): HijriDate | null;
/** Check whether the date maps to a valid Hijri date in the supported range. */
/**
* Check whether the date maps to a valid Hijri date in the supported range.
*
* Equivalent to `d.toHijri(opts) !== null`.
*
* @param opts - Optional calendar selection.
* @returns `true` if the date is in range, `false` otherwise.
* @example
* dayjs('2023-03-23').isValidHijri(); // true
* dayjs('1800-01-01').isValidHijri(); // false
*/
isValidHijri(opts?: ConversionOptions): boolean;
/** Hijri year component, or null if out of range. */
/**
* Return the Hijri year of the date.
*
* @param opts - Optional calendar selection.
* @returns The Hijri year as a `number`, or `null` if out of range.
* @example
* dayjs('2023-03-23').hijriYear(); // 1444
*/
hijriYear(opts?: ConversionOptions): number | null;
/** Hijri month component (1-12), or null if out of range. */
/**
* Return the Hijri month (1-12) of the date.
*
* Month 1 = Muharram, month 9 = Ramadan, month 12 = Dhu al-Hijjah.
*
* @param opts - Optional calendar selection.
* @returns The Hijri month in the range 1-12, or `null` if out of range.
* @example
* dayjs('2023-03-23').hijriMonth(); // 9 (Ramadan)
*/
hijriMonth(opts?: ConversionOptions): number | null;
/** Hijri day component (1-30), or null if out of range. */
/**
* Return the Hijri day of month (1-30) of the date.
*
* @param opts - Optional calendar selection.
* @returns The Hijri day in the range 1-30, or `null` if out of range.
* @example
* dayjs('2023-03-23').hijriDay(); // 1
*/
hijriDay(opts?: ConversionOptions): number | null;
/**
* Format the date using Hijri tokens (i-prefixed) and standard Day.js tokens.
* Returns an empty string if the date is outside the supported range.
* Format the date using a mix of Hijri tokens (`i`-prefixed) and standard
* Day.js tokens.
*
* Hijri tokens are replaced first. The resulting string is then passed to
* Day.js `.format()`, so all standard tokens (YYYY, MM, DD, HH, mm, ss, etc.)
* resolve normally.
*
* @param formatStr - Format string containing Hijri tokens, Day.js tokens, or both.
* @param opts - Optional calendar selection.
* @returns The formatted string, or an empty string if the date is out of range.
* @example
* dayjs('2023-03-23').formatHijri('iD iMMMM iYYYY');
* // => '1 Ramadan 1444'
*
* dayjs('2023-03-23').formatHijri('iYYYY-iMM-iDD');
* // => '1444-09-01'
*
* dayjs('2023-03-23').formatHijri('iD iMMMM iYYYY [at] HH:mm');
* // => '1 Ramadan 1444 at 00:00'
*/
formatHijri(formatStr: string, opts?: ConversionOptions): string;
}
@ -33,6 +92,26 @@ declare module 'dayjs' {
// because dayjs does not export an IStatic interface for module augmentation.
// import('dayjs').Dayjs is used explicitly to satisfy the tsup DTS emitter.
declare module 'dayjs' {
/**
* Construct a Day.js instance from a Hijri date.
*
* The result is built from an ISO date string (`YYYY-MM-DD`) to avoid
* UTC midnight converting to the previous local day in western timezones.
*
* @param hy - Hijri year.
* @param hm - Hijri month (1-12).
* @param hd - Hijri day (1-30).
* @param opts - Optional calendar selection. Defaults to `{ calendar: 'uaq' }`.
* @returns A `dayjs.Dayjs` instance at midnight local time on the corresponding
* Gregorian date.
* @throws {Error} If the Hijri date is invalid or outside the table range.
* @example
* dayjs.fromHijri(1444, 9, 1).format('YYYY-MM-DD');
* // => '2023-03-23'
*
* dayjs.fromHijri(1444, 10, 1).format('YYYY-MM-DD');
* // => '2023-04-21' (Eid al-Fitr 1444)
*/
function fromHijri(
hy: number,
hm: number,
@ -53,12 +132,35 @@ const HIJRI_TOKEN_RE = /iYYYY|iYY|iMMMM|iMMM|iMM|iM|iDD|iD|iEEEE|iEEE|iE|ioooo|i
* Day.js uses `[...]` for literal text. A `]` inside such a section would
* close it prematurely, so we split on `]` and re-join with `][` (which
* closes the current literal section, outputs a raw `]` (Day.js passes
* unrecognised characters through untouched), then opens a new one.
* unrecognised characters through untouched), then opens a new one).
*
* @param value - Plain string to escape.
* @returns The bracket-escaped string.
*/
function lit(value: string): string {
return '[' + value.split(']').join(']][') + ']';
}
/**
* Day.js plugin that adds Hijri calendar support.
*
* Register once with `dayjs.extend(hijriPlugin)`. After that, all `dayjs()`
* instances expose `.toHijri()`, `.isValidHijri()`, `.hijriYear()`,
* `.hijriMonth()`, `.hijriDay()`, and `.formatHijri()`. The static factory
* `dayjs.fromHijri(hy, hm, hd)` is also added.
*
* All calendar arithmetic is delegated to hijri-core. This plugin adds no
* conversion logic of its own.
*
* @example
* import dayjs from 'dayjs';
* import hijriPlugin from 'dayjs-hijri-plus';
*
* dayjs.extend(hijriPlugin);
*
* dayjs('2023-03-23').toHijri();
* // => { hy: 1444, hm: 9, hd: 1 }
*/
const plugin: PluginFunc = (_option, dayjsClass, dayjsFactory) => {
// ------------------------------------------------------------------ //
// Instance methods //
@ -172,10 +274,26 @@ const plugin: PluginFunc = (_option, dayjsClass, dayjsFactory) => {
export default plugin;
// Re-export hijri-core types for consumers who import from dayjs-hijri-plus.
/**
* Re-exported from hijri-core for consumers who import from dayjs-hijri-plus.
* Avoids requiring hijri-core as a direct dependency just to use these types.
*/
export type { HijriDate, ConversionOptions } from './types';
/**
* Re-exported CalendarEngine interface from hijri-core.
* Use this type to implement custom calendar engines for `registerCalendar`.
*/
export type { CalendarEngine } from 'hijri-core';
// Re-export the registry API so callers can register custom calendar engines
// without adding hijri-core as a direct dependency.
/**
* Re-exported registry API from hijri-core.
* Register, retrieve, or list custom calendar engines without adding
* hijri-core as a direct dependency.
*
* @example
* import { registerCalendar, listCalendars } from 'dayjs-hijri-plus';
* registerCalendar('my-cal', myEngine);
* listCalendars(); // ['uaq', 'fcna', 'my-cal']
*/
export { registerCalendar, getCalendar, listCalendars } from 'hijri-core';