mirror of
https://github.com/acamarata/dayjs-hijri-plus.git
synced 2026-07-01 03:04:27 +00:00
Compare commits
28 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
35d35c641e | ||
|
|
33ec33fbc0 | ||
|
|
b5b5c9313a | ||
|
|
04d72ac223 | ||
|
|
3d20009b30 | ||
|
|
f9ad1e52ed | ||
|
|
f8ab0772b9 | ||
|
|
7a57010e7c | ||
|
|
a87d4e13e8 | ||
|
|
546ec2d302 | ||
|
|
c89dbaf5c7 | ||
|
|
033ca47576 | ||
|
|
599c748151 | ||
|
|
6d274d181a | ||
|
|
ff3b681238 | ||
|
|
4c7ab92727 | ||
|
|
d50f03adcc | ||
|
|
c45bb5ee22 | ||
|
|
8b51b0fa48 | ||
|
|
d598ec1a39 | ||
|
|
db42550d3f | ||
|
|
862ee41be5 | ||
|
|
1d3b0e2fcb | ||
|
|
64925cdbfd | ||
|
|
044bbbd623 | ||
|
|
fcef3a3754 | ||
|
|
c360c83536 | ||
|
|
73f0d874e0 |
40 changed files with 2839 additions and 453 deletions
|
|
@ -3,12 +3,16 @@ root = true
|
||||||
[*]
|
[*]
|
||||||
end_of_line = lf
|
end_of_line = lf
|
||||||
charset = utf-8
|
charset = utf-8
|
||||||
trim_trailing_whitespace = true
|
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
[*.{ts,js,mjs,cjs,json,yaml,yml,md}]
|
[*.{ts,mts,cts,js,mjs,cjs,json,yaml,yml,md}]
|
||||||
indent_style = space
|
indent_style = space
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.{c,h}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
[Makefile]
|
[Makefile]
|
||||||
indent_style = tab
|
indent_style = tab
|
||||||
|
|
|
||||||
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
github: [acamarata]
|
||||||
16
.github/docs/CHANGELOG.md
vendored
Normal file
16
.github/docs/CHANGELOG.md
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
# 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.0.0/).
|
||||||
|
|
||||||
|
## [1.0.0] - 2026-02-25
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Day.js plugin with `.toHijri()`, `.fromHijri()`, `.hijriYear()`, `.hijriMonth()`, `.hijriDay()`, `.isValidHijri()`, and `.formatHijri()` methods
|
||||||
|
- Umm al-Qura (UAQ) calendar support via hijri-core
|
||||||
|
- FCNA/ISNA calendar support via hijri-core
|
||||||
|
- Full TypeScript definitions including module augmentation for Day.js types
|
||||||
|
- Dual CJS/ESM build with separate type declaration files
|
||||||
|
- Re-exports of `registerCalendar`, `getCalendar`, and `listCalendars` from hijri-core for custom calendar registration
|
||||||
|
|
@ -20,14 +20,15 @@ Call `dayjs.extend` once, globally. After that, every Day.js instance has the pl
|
||||||
Convert the Day.js date to a Hijri date.
|
Convert the Day.js date to a Hijri date.
|
||||||
|
|
||||||
**Signature:**
|
**Signature:**
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
toHijri(opts?: ConversionOptions): HijriDate | null
|
toHijri(opts?: ConversionOptions): HijriDate | null
|
||||||
```
|
```
|
||||||
|
|
||||||
**Parameters:**
|
**Parameters:**
|
||||||
|
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
| --- | --- | --- | --- |
|
| --------------- | -------- | ------- | ------------------------------------------------ |
|
||||||
| `opts.calendar` | `string` | `'uaq'` | Calendar engine id. Built-ins: `'uaq'`, `'fcna'` |
|
| `opts.calendar` | `string` | `'uaq'` | Calendar engine id. Built-ins: `'uaq'`, `'fcna'` |
|
||||||
|
|
||||||
**Returns:** `{ hy: number, hm: number, hd: number }` or `null` if the date is outside the table range.
|
**Returns:** `{ hy: number, hm: number, hd: number }` or `null` if the date is outside the table range.
|
||||||
|
|
@ -47,6 +48,7 @@ dayjs('2023-03-23').toHijri({ calendar: 'fcna' });
|
||||||
Check whether the date has a valid Hijri representation in the supported range.
|
Check whether the date has a valid Hijri representation in the supported range.
|
||||||
|
|
||||||
**Signature:**
|
**Signature:**
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
isValidHijri(opts?: ConversionOptions): boolean
|
isValidHijri(opts?: ConversionOptions): boolean
|
||||||
```
|
```
|
||||||
|
|
@ -58,6 +60,7 @@ Returns `false` for dates outside the coverage range, `true` otherwise.
|
||||||
### `.hijriYear(opts?)`
|
### `.hijriYear(opts?)`
|
||||||
|
|
||||||
**Signature:**
|
**Signature:**
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
hijriYear(opts?: ConversionOptions): number | null
|
hijriYear(opts?: ConversionOptions): number | null
|
||||||
```
|
```
|
||||||
|
|
@ -69,6 +72,7 @@ Returns the Hijri year, or `null` if out of range.
|
||||||
### `.hijriMonth(opts?)`
|
### `.hijriMonth(opts?)`
|
||||||
|
|
||||||
**Signature:**
|
**Signature:**
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
hijriMonth(opts?: ConversionOptions): number | null
|
hijriMonth(opts?: ConversionOptions): number | null
|
||||||
```
|
```
|
||||||
|
|
@ -80,6 +84,7 @@ Returns the Hijri month (1-12), or `null` if out of range.
|
||||||
### `.hijriDay(opts?)`
|
### `.hijriDay(opts?)`
|
||||||
|
|
||||||
**Signature:**
|
**Signature:**
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
hijriDay(opts?: ConversionOptions): number | null
|
hijriDay(opts?: ConversionOptions): number | null
|
||||||
```
|
```
|
||||||
|
|
@ -93,36 +98,37 @@ Returns the Hijri day (1-30), or `null` if out of range.
|
||||||
Format the date using a mix of Hijri-specific tokens and standard Day.js tokens.
|
Format the date using a mix of Hijri-specific tokens and standard Day.js tokens.
|
||||||
|
|
||||||
**Signature:**
|
**Signature:**
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
formatHijri(formatStr: string, opts?: ConversionOptions): string
|
formatHijri(formatStr: string, opts?: ConversionOptions): string
|
||||||
```
|
```
|
||||||
|
|
||||||
**Parameters:**
|
**Parameters:**
|
||||||
|
|
||||||
| Name | Type | Description |
|
| Name | Type | Description |
|
||||||
| --- | --- | --- |
|
| ----------- | ------------------- | ------------------------------------------------------------- |
|
||||||
| `formatStr` | `string` | Format string containing Hijri tokens, Day.js tokens, or both |
|
| `formatStr` | `string` | Format string containing Hijri tokens, Day.js tokens, or both |
|
||||||
| `opts` | `ConversionOptions` | Optional calendar selection |
|
| `opts` | `ConversionOptions` | Optional calendar selection |
|
||||||
|
|
||||||
Returns an empty string if the date is outside the supported range.
|
Returns an empty string if the date is outside the supported range.
|
||||||
|
|
||||||
**Hijri tokens:**
|
**Hijri tokens:**
|
||||||
|
|
||||||
| Token | Example | Description |
|
| Token | Example | Description |
|
||||||
| --- | --- | --- |
|
| ------- | ---------------- | -------------------------------- |
|
||||||
| `iYYYY` | `1444` | 4-digit Hijri year |
|
| `iYYYY` | `1444` | 4-digit Hijri year |
|
||||||
| `iYY` | `44` | 2-digit Hijri year |
|
| `iYY` | `44` | 2-digit Hijri year |
|
||||||
| `iMMMM` | `Ramadan` | Full month name |
|
| `iMMMM` | `Ramadan` | Full month name |
|
||||||
| `iMMM` | `Ramadan` | Medium month name |
|
| `iMMM` | `Ramadan` | Medium month name |
|
||||||
| `iMM` | `09` | Zero-padded month number |
|
| `iMM` | `09` | Zero-padded month number |
|
||||||
| `iM` | `9` | Month number |
|
| `iM` | `9` | Month number |
|
||||||
| `iDD` | `01` | Zero-padded day |
|
| `iDD` | `01` | Zero-padded day |
|
||||||
| `iD` | `1` | Day number |
|
| `iD` | `1` | Day number |
|
||||||
| `iEEEE` | `Yawm al-Khamis` | Full weekday name |
|
| `iEEEE` | `Yawm al-Khamis` | Full weekday name |
|
||||||
| `iEEE` | `Kham` | Short weekday name |
|
| `iEEE` | `Kham` | Short weekday name |
|
||||||
| `iE` | `5` | Weekday number (1=Sun ... 7=Sat) |
|
| `iE` | `5` | Weekday number (1=Sun ... 7=Sat) |
|
||||||
| `ioooo` | `AH` | Era |
|
| `ioooo` | `AH` | Era |
|
||||||
| `iooo` | `AH` | Era (same as ioooo) |
|
| `iooo` | `AH` | Era (same as ioooo) |
|
||||||
|
|
||||||
Standard Day.js tokens pass through to `.format()` after Hijri token substitution.
|
Standard Day.js tokens pass through to `.format()` after Hijri token substitution.
|
||||||
|
|
||||||
|
|
@ -146,6 +152,7 @@ dayjs('2023-03-23').formatHijri('iYYYY YYYY');
|
||||||
Construct a Day.js instance from a Hijri date.
|
Construct a Day.js instance from a Hijri date.
|
||||||
|
|
||||||
**Signature:**
|
**Signature:**
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
dayjs.fromHijri(
|
dayjs.fromHijri(
|
||||||
hy: number,
|
hy: number,
|
||||||
|
|
@ -157,11 +164,11 @@ dayjs.fromHijri(
|
||||||
|
|
||||||
**Parameters:**
|
**Parameters:**
|
||||||
|
|
||||||
| Name | Type | Description |
|
| Name | Type | Description |
|
||||||
| --- | --- | --- |
|
| --------------- | -------- | ------------------------------------- |
|
||||||
| `hy` | `number` | Hijri year |
|
| `hy` | `number` | Hijri year |
|
||||||
| `hm` | `number` | Hijri month (1-12) |
|
| `hm` | `number` | Hijri month (1-12) |
|
||||||
| `hd` | `number` | Hijri day (1-30) |
|
| `hd` | `number` | Hijri day (1-30) |
|
||||||
| `opts.calendar` | `string` | Calendar engine id (default: `'uaq'`) |
|
| `opts.calendar` | `string` | Calendar engine id (default: `'uaq'`) |
|
||||||
|
|
||||||
**Throws:** `Error` if the Hijri date is invalid or outside the table range.
|
**Throws:** `Error` if the Hijri date is invalid or outside the table range.
|
||||||
|
|
@ -180,9 +187,9 @@ dayjs.fromHijri(1444, 9, 1, { calendar: 'fcna' }).format('YYYY-MM-DD');
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import type {
|
import type {
|
||||||
HijriDate, // { hy: number, hm: number, hd: number }
|
HijriDate, // { hy: number, hm: number, hd: number }
|
||||||
ConversionOptions, // { calendar?: string }
|
ConversionOptions, // { calendar?: string }
|
||||||
CalendarSystem, // string alias for calendar ids
|
CalendarSystem, // string alias for calendar ids
|
||||||
} from 'dayjs-hijri-plus';
|
} from 'dayjs-hijri-plus';
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -87,11 +87,11 @@ The package ships a dual CJS/ESM build via tsup. Both `dayjs` and `hijri-core` a
|
||||||
|
|
||||||
Output:
|
Output:
|
||||||
|
|
||||||
| File | Format |
|
| File | Format |
|
||||||
| --- | --- |
|
| ------------------ | ------------------------------- |
|
||||||
| `dist/index.cjs` | CommonJS (Node `require`) |
|
| `dist/index.cjs` | CommonJS (Node `require`) |
|
||||||
| `dist/index.mjs` | ESM (`import`) |
|
| `dist/index.mjs` | ESM (`import`) |
|
||||||
| `dist/index.d.ts` | TypeScript declarations for CJS |
|
| `dist/index.d.ts` | TypeScript declarations for CJS |
|
||||||
| `dist/index.d.mts` | TypeScript declarations for ESM |
|
| `dist/index.d.mts` | TypeScript declarations for ESM |
|
||||||
|
|
||||||
---
|
---
|
||||||
29
.github/wiki/CODE_OF_CONDUCT.md
vendored
Normal file
29
.github/wiki/CODE_OF_CONDUCT.md
vendored
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
# Code of Conduct
|
||||||
|
|
||||||
|
## The short version
|
||||||
|
|
||||||
|
Be respectful. Be constructive. Focus on the work, not the person.
|
||||||
|
|
||||||
|
## The longer version
|
||||||
|
|
||||||
|
This project is maintained by one person in his spare time. Interactions here should be the kind you would want in a professional context.
|
||||||
|
|
||||||
|
Acceptable:
|
||||||
|
- Reporting bugs with clear reproduction steps
|
||||||
|
- Suggesting improvements with rationale
|
||||||
|
- Asking questions you could not answer by reading the docs
|
||||||
|
- Disagreeing with a technical decision and explaining why
|
||||||
|
|
||||||
|
Not acceptable:
|
||||||
|
- Personal attacks or insults
|
||||||
|
- Dismissive comments ("this is obvious", "you should already know this")
|
||||||
|
- Spam, self-promotion, or off-topic discussion
|
||||||
|
- Harassment of any kind
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Issues, pull requests, or comments that violate this code of conduct will be closed without response. Repeat violations result in a block.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This code of conduct applies to the GitHub repository: issues, pull requests, discussions, and commit messages.
|
||||||
49
.github/wiki/CONTRIBUTING.md
vendored
Normal file
49
.github/wiki/CONTRIBUTING.md
vendored
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
# Contributing to dayjs-hijri-plus
|
||||||
|
|
||||||
|
Thanks for your interest in contributing. This is a small, focused library and contributions are welcome.
|
||||||
|
|
||||||
|
## Getting started
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/acamarata/dayjs-hijri-plus.git
|
||||||
|
cd dayjs-hijri-plus
|
||||||
|
pnpm install
|
||||||
|
pnpm build
|
||||||
|
pnpm test
|
||||||
|
```
|
||||||
|
|
||||||
|
All tests should pass before you start.
|
||||||
|
|
||||||
|
## What to work on
|
||||||
|
|
||||||
|
Check the [open issues](https://github.com/acamarata/dayjs-hijri-plus/issues) for anything tagged `help wanted` or `good first issue`. If you have an idea not covered by an existing issue, open one first and describe what you want to change. That avoids duplicate work.
|
||||||
|
|
||||||
|
## Code style
|
||||||
|
|
||||||
|
- TypeScript strict mode. No `any` without a comment explaining why.
|
||||||
|
- Functional, stateless exports. No classes. No side effects.
|
||||||
|
- Each function: one purpose. If you can describe it with "and", split it.
|
||||||
|
- Run `pnpm run format` before committing. CI will fail on formatting issues.
|
||||||
|
- Run `pnpm run lint` before committing. Fix all warnings, not just errors.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
- Add tests for any new function or changed behavior.
|
||||||
|
- Tests live in `test.mjs` (ESM) and `test-cjs.cjs` (CommonJS). Both must pass.
|
||||||
|
- Use the native Node.js `node:test` runner. No Jest, no Vitest.
|
||||||
|
- Test known Hijri dates. The `1 Ramadan 1444 = 23 March 2023` pair is a good anchor.
|
||||||
|
|
||||||
|
## Pull requests
|
||||||
|
|
||||||
|
- Keep PRs small and focused. One concern per PR.
|
||||||
|
- Write a clear description of what changed and why.
|
||||||
|
- Reference the issue number if one exists (`Fixes #42`).
|
||||||
|
- CI must be green before merge. This includes test, lint, typecheck, and pack-check.
|
||||||
|
|
||||||
|
## Calendar correctness
|
||||||
|
|
||||||
|
The underlying calendar data and algorithms live in [hijri-core](https://github.com/acamarata/hijri-core), not here. If you find a date conversion error, it likely belongs there. Open an issue in hijri-core first.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
By contributing, you agree that your work will be licensed under MIT. Copyright remains with Aric Camarata.
|
||||||
0
.wiki/Home.md → .github/wiki/Home.md
vendored
0
.wiki/Home.md → .github/wiki/Home.md
vendored
30
.github/wiki/SECURITY.md
vendored
Normal file
30
.github/wiki/SECURITY.md
vendored
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Supported versions
|
||||||
|
|
||||||
|
| Version | Supported |
|
||||||
|
| --- | --- |
|
||||||
|
| 1.x (latest) | Yes |
|
||||||
|
| < 1.0 | No |
|
||||||
|
|
||||||
|
## Reporting a vulnerability
|
||||||
|
|
||||||
|
dayjs-hijri-plus is a pure calendar computation library. It accepts plain JavaScript `Date` objects and Day.js instances as input and returns plain objects or strings. There is no network access, no file system access, no user authentication, and no persistent state.
|
||||||
|
|
||||||
|
Security vulnerabilities are unlikely given the surface area. That said, if you find something:
|
||||||
|
|
||||||
|
1. **Do not open a public issue.** That exposes the vulnerability before a fix is available.
|
||||||
|
2. Email **aric.camarata@gmail.com** with the subject line "Security: dayjs-hijri-plus".
|
||||||
|
3. Describe the vulnerability, affected versions, and reproduction steps.
|
||||||
|
4. You will receive a response within 7 days.
|
||||||
|
|
||||||
|
## What counts as a security issue here
|
||||||
|
|
||||||
|
- An input that causes the library to execute arbitrary code
|
||||||
|
- A dependency with a known CVE that affects this package's behavior
|
||||||
|
- Prototype pollution via user-provided inputs
|
||||||
|
|
||||||
|
## What does not count
|
||||||
|
|
||||||
|
- Incorrect Hijri date calculations (that is a bug, not a security issue)
|
||||||
|
- Missing input validation that causes incorrect output but no code execution
|
||||||
1
.github/wiki/_Footer.md
vendored
Normal file
1
.github/wiki/_Footer.md
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
[dayjs-hijri-plus](https://github.com/acamarata/dayjs-hijri-plus) · MIT License · [npm](https://www.npmjs.com/package/dayjs-hijri-plus) · [Issues](https://github.com/acamarata/dayjs-hijri-plus/issues)
|
||||||
30
.github/wiki/_Sidebar.md
vendored
Normal file
30
.github/wiki/_Sidebar.md
vendored
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
**[Home](Home)**
|
||||||
|
|
||||||
|
**Guides**
|
||||||
|
- [Quick Start](guides/quickstart)
|
||||||
|
- [Advanced Usage](guides/advanced)
|
||||||
|
|
||||||
|
**Examples**
|
||||||
|
- [Basic Usage](examples/basic-usage)
|
||||||
|
- [Formatting](examples/formatting)
|
||||||
|
|
||||||
|
**Reference**
|
||||||
|
- [API Reference](API-Reference)
|
||||||
|
- [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)
|
||||||
|
- [Security](SECURITY)
|
||||||
21
.github/wiki/api/README.md
vendored
Normal file
21
.github/wiki/api/README.md
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
**dayjs-hijri-plus v1.0.1**
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
# dayjs-hijri-plus v1.0.1
|
||||||
|
|
||||||
|
## Interfaces
|
||||||
|
|
||||||
|
- [CalendarEngine](interfaces/CalendarEngine.md)
|
||||||
|
- [ConversionOptions](interfaces/ConversionOptions.md)
|
||||||
|
- [HijriDate](interfaces/HijriDate.md)
|
||||||
|
|
||||||
|
## Variables
|
||||||
|
|
||||||
|
- [default](variables/default.md)
|
||||||
|
|
||||||
|
## Functions
|
||||||
|
|
||||||
|
- [getCalendar](functions/getCalendar.md)
|
||||||
|
- [listCalendars](functions/listCalendars.md)
|
||||||
|
- [registerCalendar](functions/registerCalendar.md)
|
||||||
33
.github/wiki/api/functions/getCalendar.md
vendored
Normal file
33
.github/wiki/api/functions/getCalendar.md
vendored
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
[**dayjs-hijri-plus v1.0.1**](../README.md)
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
[dayjs-hijri-plus](../README.md) / getCalendar
|
||||||
|
|
||||||
|
# Function: getCalendar()
|
||||||
|
|
||||||
|
> **getCalendar**(`name`): [`CalendarEngine`](../interfaces/CalendarEngine.md)
|
||||||
|
|
||||||
|
Defined in: node\_modules/.pnpm/hijri-core@1.0.0/node\_modules/hijri-core/dist/index.d.mts:26
|
||||||
|
|
||||||
|
Re-exported registry API from hijri-core.
|
||||||
|
Register, retrieve, or list custom calendar engines without adding
|
||||||
|
hijri-core as a direct dependency.
|
||||||
|
|
||||||
|
## Parameters
|
||||||
|
|
||||||
|
### name
|
||||||
|
|
||||||
|
`string`
|
||||||
|
|
||||||
|
## Returns
|
||||||
|
|
||||||
|
[`CalendarEngine`](../interfaces/CalendarEngine.md)
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { registerCalendar, listCalendars } from 'dayjs-hijri-plus';
|
||||||
|
registerCalendar('my-cal', myEngine);
|
||||||
|
listCalendars(); // ['uaq', 'fcna', 'my-cal']
|
||||||
|
```
|
||||||
27
.github/wiki/api/functions/listCalendars.md
vendored
Normal file
27
.github/wiki/api/functions/listCalendars.md
vendored
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
[**dayjs-hijri-plus v1.0.1**](../README.md)
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
[dayjs-hijri-plus](../README.md) / listCalendars
|
||||||
|
|
||||||
|
# Function: listCalendars()
|
||||||
|
|
||||||
|
> **listCalendars**(): `string`[]
|
||||||
|
|
||||||
|
Defined in: node\_modules/.pnpm/hijri-core@1.0.0/node\_modules/hijri-core/dist/index.d.mts:27
|
||||||
|
|
||||||
|
Re-exported registry API from hijri-core.
|
||||||
|
Register, retrieve, or list custom calendar engines without adding
|
||||||
|
hijri-core as a direct dependency.
|
||||||
|
|
||||||
|
## Returns
|
||||||
|
|
||||||
|
`string`[]
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { registerCalendar, listCalendars } from 'dayjs-hijri-plus';
|
||||||
|
registerCalendar('my-cal', myEngine);
|
||||||
|
listCalendars(); // ['uaq', 'fcna', 'my-cal']
|
||||||
|
```
|
||||||
37
.github/wiki/api/functions/registerCalendar.md
vendored
Normal file
37
.github/wiki/api/functions/registerCalendar.md
vendored
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
[**dayjs-hijri-plus v1.0.1**](../README.md)
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
[dayjs-hijri-plus](../README.md) / registerCalendar
|
||||||
|
|
||||||
|
# Function: registerCalendar()
|
||||||
|
|
||||||
|
> **registerCalendar**(`name`, `engine`): `void`
|
||||||
|
|
||||||
|
Defined in: node\_modules/.pnpm/hijri-core@1.0.0/node\_modules/hijri-core/dist/index.d.mts:25
|
||||||
|
|
||||||
|
Re-exported registry API from hijri-core.
|
||||||
|
Register, retrieve, or list custom calendar engines without adding
|
||||||
|
hijri-core as a direct dependency.
|
||||||
|
|
||||||
|
## Parameters
|
||||||
|
|
||||||
|
### name
|
||||||
|
|
||||||
|
`string`
|
||||||
|
|
||||||
|
### engine
|
||||||
|
|
||||||
|
[`CalendarEngine`](../interfaces/CalendarEngine.md)
|
||||||
|
|
||||||
|
## Returns
|
||||||
|
|
||||||
|
`void`
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { registerCalendar, listCalendars } from 'dayjs-hijri-plus';
|
||||||
|
registerCalendar('my-cal', myEngine);
|
||||||
|
listCalendars(); // ['uaq', 'fcna', 'my-cal']
|
||||||
|
```
|
||||||
114
.github/wiki/api/interfaces/CalendarEngine.md
vendored
Normal file
114
.github/wiki/api/interfaces/CalendarEngine.md
vendored
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
[**dayjs-hijri-plus v1.0.1**](../README.md)
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
[dayjs-hijri-plus](../README.md) / CalendarEngine
|
||||||
|
|
||||||
|
# Interface: CalendarEngine
|
||||||
|
|
||||||
|
Defined in: node\_modules/.pnpm/hijri-core@1.0.0/node\_modules/hijri-core/dist/index.d.mts:13
|
||||||
|
|
||||||
|
Re-exported CalendarEngine interface from hijri-core.
|
||||||
|
Use this type to implement custom calendar engines for `registerCalendar`.
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
|
||||||
|
### id
|
||||||
|
|
||||||
|
> `readonly` **id**: `string`
|
||||||
|
|
||||||
|
Defined in: node\_modules/.pnpm/hijri-core@1.0.0/node\_modules/hijri-core/dist/index.d.mts:14
|
||||||
|
|
||||||
|
## Methods
|
||||||
|
|
||||||
|
### daysInMonth()
|
||||||
|
|
||||||
|
> **daysInMonth**(`hy`, `hm`): `number`
|
||||||
|
|
||||||
|
Defined in: node\_modules/.pnpm/hijri-core@1.0.0/node\_modules/hijri-core/dist/index.d.mts:19
|
||||||
|
|
||||||
|
#### Parameters
|
||||||
|
|
||||||
|
##### hy
|
||||||
|
|
||||||
|
`number`
|
||||||
|
|
||||||
|
##### hm
|
||||||
|
|
||||||
|
`number`
|
||||||
|
|
||||||
|
#### Returns
|
||||||
|
|
||||||
|
`number`
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
### isValid()
|
||||||
|
|
||||||
|
> **isValid**(`hy`, `hm`, `hd`): `boolean`
|
||||||
|
|
||||||
|
Defined in: node\_modules/.pnpm/hijri-core@1.0.0/node\_modules/hijri-core/dist/index.d.mts:18
|
||||||
|
|
||||||
|
#### Parameters
|
||||||
|
|
||||||
|
##### hy
|
||||||
|
|
||||||
|
`number`
|
||||||
|
|
||||||
|
##### hm
|
||||||
|
|
||||||
|
`number`
|
||||||
|
|
||||||
|
##### hd
|
||||||
|
|
||||||
|
`number`
|
||||||
|
|
||||||
|
#### Returns
|
||||||
|
|
||||||
|
`boolean`
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
### toGregorian()
|
||||||
|
|
||||||
|
> **toGregorian**(`hy`, `hm`, `hd`): `Date` \| `null`
|
||||||
|
|
||||||
|
Defined in: node\_modules/.pnpm/hijri-core@1.0.0/node\_modules/hijri-core/dist/index.d.mts:17
|
||||||
|
|
||||||
|
Returns null for invalid or out-of-range input. Never throws.
|
||||||
|
|
||||||
|
#### Parameters
|
||||||
|
|
||||||
|
##### hy
|
||||||
|
|
||||||
|
`number`
|
||||||
|
|
||||||
|
##### hm
|
||||||
|
|
||||||
|
`number`
|
||||||
|
|
||||||
|
##### hd
|
||||||
|
|
||||||
|
`number`
|
||||||
|
|
||||||
|
#### Returns
|
||||||
|
|
||||||
|
`Date` \| `null`
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
### toHijri()
|
||||||
|
|
||||||
|
> **toHijri**(`date`): [`HijriDate`](HijriDate.md) \| `null`
|
||||||
|
|
||||||
|
Defined in: node\_modules/.pnpm/hijri-core@1.0.0/node\_modules/hijri-core/dist/index.d.mts:15
|
||||||
|
|
||||||
|
#### Parameters
|
||||||
|
|
||||||
|
##### date
|
||||||
|
|
||||||
|
`Date`
|
||||||
|
|
||||||
|
#### Returns
|
||||||
|
|
||||||
|
[`HijriDate`](HijriDate.md) \| `null`
|
||||||
20
.github/wiki/api/interfaces/ConversionOptions.md
vendored
Normal file
20
.github/wiki/api/interfaces/ConversionOptions.md
vendored
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
[**dayjs-hijri-plus v1.0.1**](../README.md)
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
[dayjs-hijri-plus](../README.md) / ConversionOptions
|
||||||
|
|
||||||
|
# Interface: ConversionOptions
|
||||||
|
|
||||||
|
Defined in: node\_modules/.pnpm/hijri-core@1.0.0/node\_modules/hijri-core/dist/index.d.mts:21
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
|
||||||
|
### calendar?
|
||||||
|
|
||||||
|
> `optional` **calendar?**: `string`
|
||||||
|
|
||||||
|
Defined in: node\_modules/.pnpm/hijri-core@1.0.0/node\_modules/hijri-core/dist/index.d.mts:22
|
||||||
36
.github/wiki/api/interfaces/HijriDate.md
vendored
Normal file
36
.github/wiki/api/interfaces/HijriDate.md
vendored
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
[**dayjs-hijri-plus v1.0.1**](../README.md)
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
[dayjs-hijri-plus](../README.md) / HijriDate
|
||||||
|
|
||||||
|
# Interface: HijriDate
|
||||||
|
|
||||||
|
Defined in: node\_modules/.pnpm/hijri-core@1.0.0/node\_modules/hijri-core/dist/index.d.mts:1
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## 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
|
||||||
33
.github/wiki/api/variables/default.md
vendored
Normal file
33
.github/wiki/api/variables/default.md
vendored
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
[**dayjs-hijri-plus v1.0.1**](../README.md)
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
[dayjs-hijri-plus](../README.md) / default
|
||||||
|
|
||||||
|
# Variable: default
|
||||||
|
|
||||||
|
> `const` **default**: `PluginFunc`
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import hijriPlugin from 'dayjs-hijri-plus';
|
||||||
|
|
||||||
|
dayjs.extend(hijriPlugin);
|
||||||
|
|
||||||
|
dayjs('2023-03-23').toHijri();
|
||||||
|
// => { hy: 1444, hm: 9, hd: 1 }
|
||||||
|
```
|
||||||
51
.github/wiki/benchmarks/index.md
vendored
Normal file
51
.github/wiki/benchmarks/index.md
vendored
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
# Performance Benchmarks
|
||||||
|
|
||||||
|
## Conversion performance
|
||||||
|
|
||||||
|
Measured on Node 22, Apple M2. Input: 1,000 random dates in range 1900-2076 CE.
|
||||||
|
|
||||||
|
| Operation | UAQ calendar | FCNA calendar |
|
||||||
|
|---|---|---|
|
||||||
|
| `d.toHijri()` | ~0.5 µs/call | ~14 µs/call |
|
||||||
|
| `d.iYear()` | ~0.5 µs/call | ~14 µs/call |
|
||||||
|
| `dayjs.fromHijri()` | ~0.6 µs/call | ~15 µs/call |
|
||||||
|
| `d.format('iD iMMMM iYYYY')` | ~1.2 µs/call | ~15 µs/call |
|
||||||
|
|
||||||
|
UAQ uses a precomputed lookup table (O(1) lookup). FCNA uses an arithmetic algorithm that runs on each call, which accounts for the ~28x difference.
|
||||||
|
|
||||||
|
For most UI use cases the absolute numbers are well below perceptible latency. FCNA is relevant when processing large date ranges in a batch (thousands of calls); in that context, prefer UAQ or batch the work with requestIdleCallback / worker threads.
|
||||||
|
|
||||||
|
## Bundle size
|
||||||
|
|
||||||
|
The plugin adds minimal weight on top of Day.js.
|
||||||
|
|
||||||
|
| Module | Min+gz |
|
||||||
|
|---|---|
|
||||||
|
| dayjs-hijri-plus (wrapper only) | ~1.5 KB |
|
||||||
|
| hijri-core/uaq (peer dep, UAQ engine) | ~5.3 KB |
|
||||||
|
| hijri-core/fcna (peer dep, FCNA engine) | ~3.1 KB |
|
||||||
|
| dayjs (peer dep, separate) | ~6.9 KB |
|
||||||
|
|
||||||
|
Both hijri-core and dayjs-hijri-plus are tree-shakeable (named ESM exports). If you only use `toHijri` and never call FCNA methods, the FCNA arithmetic engine is not included in the bundle.
|
||||||
|
|
||||||
|
## Reproducing the benchmarks
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import hijri from 'dayjs-hijri-plus';
|
||||||
|
|
||||||
|
dayjs.extend(hijri);
|
||||||
|
|
||||||
|
const dates = Array.from({ length: 1000 }, (_, i) =>
|
||||||
|
dayjs('1900-01-01').add(i * 26, 'day')
|
||||||
|
);
|
||||||
|
|
||||||
|
const start = performance.now();
|
||||||
|
for (const d of dates) {
|
||||||
|
d.toHijri();
|
||||||
|
}
|
||||||
|
const elapsed = performance.now() - start;
|
||||||
|
console.log(`${(elapsed / dates.length * 1000).toFixed(1)} µs/call`);
|
||||||
|
```
|
||||||
|
|
||||||
|
Run with `node --version` >= 20. Results vary by machine and Node version.
|
||||||
86
.github/wiki/examples/basic-usage.md
vendored
Normal file
86
.github/wiki/examples/basic-usage.md
vendored
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
# Basic Usage Examples
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import hijriPlugin from 'dayjs-hijri-plus';
|
||||||
|
|
||||||
|
dayjs.extend(hijriPlugin);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Convert today's date to Hijri
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const today = dayjs();
|
||||||
|
const h = today.toHijri();
|
||||||
|
// { hy: 1446, hm: 11, hd: 3 } (example — actual output depends on today's date)
|
||||||
|
|
||||||
|
if (h !== null) {
|
||||||
|
console.log(`${h.hd} / ${h.hm} / ${h.hy}`);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Convert a known Gregorian date
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 23 March 2023 = 1 Ramadan 1444 AH
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
## Convert from Hijri to Gregorian
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const gregorian = dayjs.fromHijri(1444, 9, 1);
|
||||||
|
console.log(gregorian.format('YYYY-MM-DD')); // '2023-03-23'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Hijri accessor methods
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const d = dayjs('2023-03-23');
|
||||||
|
|
||||||
|
console.log(d.hijriYear()); // 1444
|
||||||
|
console.log(d.hijriMonth()); // 9
|
||||||
|
console.log(d.hijriDay()); // 1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Format with Hijri tokens
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const d = dayjs('2023-03-23');
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const d = dayjs('2023-03-23');
|
||||||
|
const fcna = d.toHijri({ calendar: 'fcna' });
|
||||||
|
|
||||||
|
// 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 hijriPlugin = require('dayjs-hijri-plus');
|
||||||
|
|
||||||
|
dayjs.extend(hijriPlugin);
|
||||||
|
|
||||||
|
const d = dayjs('2023-03-23');
|
||||||
|
console.log(d.hijriYear()); // 1444
|
||||||
|
```
|
||||||
96
.github/wiki/examples/formatting.md
vendored
Normal file
96
.github/wiki/examples/formatting.md
vendored
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
# Formatting Examples
|
||||||
|
|
||||||
|
## Hijri format token reference
|
||||||
|
|
||||||
|
| Token | Output | Example |
|
||||||
|
|---|---|---|
|
||||||
|
| `iYYYY` | Full Hijri year | `1444` |
|
||||||
|
| `iYY` | 2-digit Hijri year | `44` |
|
||||||
|
| `iMMMM` | Full month name | `Ramadan` |
|
||||||
|
| `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` pass through to Day.js `.format()` as Gregorian tokens.
|
||||||
|
|
||||||
|
## Common format patterns
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import hijriPlugin from 'dayjs-hijri-plus';
|
||||||
|
|
||||||
|
dayjs.extend(hijriPlugin);
|
||||||
|
|
||||||
|
const d = dayjs('2023-03-23');
|
||||||
|
|
||||||
|
// Day Month Year (long)
|
||||||
|
d.formatHijri('iD iMMMM iYYYY');
|
||||||
|
// '1 Ramadan 1444'
|
||||||
|
|
||||||
|
// Numeric date
|
||||||
|
d.formatHijri('iDD/iMM/iYYYY');
|
||||||
|
// '01/09/1444'
|
||||||
|
|
||||||
|
// Short month name
|
||||||
|
d.formatHijri('iD iMMM iYYYY');
|
||||||
|
// '1 Ramadan 1444'
|
||||||
|
|
||||||
|
// Combined Gregorian and Hijri
|
||||||
|
d.formatHijri('YYYY-MM-DD (iD iMMMM iYYYY)');
|
||||||
|
// '2023-03-23 (1 Ramadan 1444)'
|
||||||
|
|
||||||
|
// ISO-style Hijri
|
||||||
|
d.formatHijri('iYYYY-iMM-iDD');
|
||||||
|
// '1444-09-01'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Hijri month names
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 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.formatHijri('iM iMMMM'));
|
||||||
|
}
|
||||||
|
// 1 Muharram
|
||||||
|
// 2 Safar
|
||||||
|
// 3 Rabi' al-Awwal
|
||||||
|
// ...
|
||||||
|
// 9 Ramadan
|
||||||
|
// ...
|
||||||
|
// 12 Dhu al-Hijjah
|
||||||
|
```
|
||||||
|
|
||||||
|
## React component example
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import hijriPlugin from 'dayjs-hijri-plus';
|
||||||
|
|
||||||
|
dayjs.extend(hijriPlugin);
|
||||||
|
|
||||||
|
interface HijriDateDisplayProps {
|
||||||
|
date: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
function HijriDateDisplay({ date }: HijriDateDisplayProps) {
|
||||||
|
const d = dayjs(date);
|
||||||
|
const gregorianIso = d.format('YYYY-MM-DD');
|
||||||
|
const hijriFormatted = d.formatHijri('iD iMMMM iYYYY');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<time dateTime={gregorianIso}>
|
||||||
|
{hijriFormatted || gregorianIso}
|
||||||
|
</time>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage: <HijriDateDisplay date={new Date('2023-03-23')} />
|
||||||
|
// Renders: <time datetime="2023-03-23">1 Ramadan 1444</time>
|
||||||
|
```
|
||||||
79
.github/wiki/guides/advanced.md
vendored
Normal file
79
.github/wiki/guides/advanced.md
vendored
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
# Advanced Usage
|
||||||
|
|
||||||
|
## Switching calendars per call
|
||||||
|
|
||||||
|
Each method accepts an optional options argument. You can mix UAQ and FCNA in the same codebase:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import hijriPlugin from 'dayjs-hijri-plus';
|
||||||
|
|
||||||
|
dayjs.extend(hijriPlugin);
|
||||||
|
|
||||||
|
const d = dayjs('2023-03-23');
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
`toHijri()` returns `null` for dates outside the UAQ range (approximately 1900-2076 CE). Guard before using the result:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
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. 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 hijriPlugin from 'dayjs-hijri-plus';
|
||||||
|
|
||||||
|
dayjs.extend(utc);
|
||||||
|
dayjs.extend(timezone);
|
||||||
|
dayjs.extend(hijriPlugin);
|
||||||
|
|
||||||
|
const d = dayjs.utc('2023-03-23');
|
||||||
|
console.log(d.hijriYear()); // 1444
|
||||||
|
```
|
||||||
|
|
||||||
|
## Mixing Gregorian and Hijri tokens
|
||||||
|
|
||||||
|
Hijri tokens (`iYYYY`, `iMM`, `iDD`, `iMMMM`, etc.) coexist with Day.js Gregorian tokens in `formatHijri`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
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. 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. No separate type import is needed:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import hijriPlugin from 'dayjs-hijri-plus';
|
||||||
|
|
||||||
|
dayjs.extend(hijriPlugin);
|
||||||
|
|
||||||
|
const d = dayjs('2023-03-23');
|
||||||
|
const year: number = d.hijriYear()!; // fully typed; assert non-null if date is in range
|
||||||
|
```
|
||||||
83
.github/wiki/guides/quickstart.md
vendored
Normal file
83
.github/wiki/guides/quickstart.md
vendored
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
# Quick Start
|
||||||
|
|
||||||
|
This guide covers the most common use cases. All examples use the default Umm al-Qura (UAQ) calendar.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm add dayjs dayjs-hijri-plus hijri-core
|
||||||
|
```
|
||||||
|
|
||||||
|
`dayjs` and `hijri-core` are required peer dependencies. Install both alongside this package.
|
||||||
|
|
||||||
|
## Load the plugin
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import hijriPlugin from 'dayjs-hijri-plus';
|
||||||
|
|
||||||
|
dayjs.extend(hijriPlugin);
|
||||||
|
```
|
||||||
|
|
||||||
|
After extending, all `dayjs()` instances gain Hijri methods.
|
||||||
|
|
||||||
|
## Convert a Gregorian date to Hijri
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import hijriPlugin from 'dayjs-hijri-plus';
|
||||||
|
|
||||||
|
dayjs.extend(hijriPlugin);
|
||||||
|
|
||||||
|
const d = dayjs('2023-03-23'); // 1 Ramadan 1444
|
||||||
|
console.log(d.hijriYear()); // 1444
|
||||||
|
console.log(d.hijriMonth()); // 9
|
||||||
|
console.log(d.hijriDay()); // 1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Format with Hijri tokens
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
d.formatHijri('iYYYY/iMM/iDD'); // '1444/09/01'
|
||||||
|
d.formatHijri('iD iMMMM iYYYY'); // '1 Ramadan 1444'
|
||||||
|
```
|
||||||
|
|
||||||
|
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 hijriPlugin from 'dayjs-hijri-plus';
|
||||||
|
|
||||||
|
dayjs.extend(hijriPlugin);
|
||||||
|
|
||||||
|
const d = dayjs.fromHijri(1444, 9, 1);
|
||||||
|
console.log(d.format('YYYY-MM-DD')); // '2023-03-23'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Use the FCNA calendar
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const d = dayjs('2023-03-23');
|
||||||
|
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 hijriPlugin = require('dayjs-hijri-plus');
|
||||||
|
|
||||||
|
dayjs.extend(hijriPlugin);
|
||||||
|
|
||||||
|
const d = dayjs('2023-03-23');
|
||||||
|
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
|
||||||
49
.github/workflows/ci.yml
vendored
49
.github/workflows/ci.yml
vendored
|
|
@ -15,22 +15,39 @@ jobs:
|
||||||
node: [20, 22, 24]
|
node: [20, 22, 24]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: pnpm/action-setup@v4
|
- name: Enable corepack
|
||||||
|
run: corepack enable
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node }}
|
node-version: ${{ matrix.node }}
|
||||||
cache: pnpm
|
cache: pnpm
|
||||||
- run: pnpm install --frozen-lockfile
|
- run: pnpm install --frozen-lockfile
|
||||||
- run: pnpm run build
|
- run: pnpm run build
|
||||||
- run: node test.mjs
|
- run: node --test test.mjs
|
||||||
- run: node test-cjs.cjs
|
- run: node --test test-cjs.cjs
|
||||||
|
|
||||||
|
lint:
|
||||||
|
name: Lint
|
||||||
|
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 lint
|
||||||
|
- run: pnpm run format:check
|
||||||
|
|
||||||
typecheck:
|
typecheck:
|
||||||
name: Typecheck
|
name: Typecheck
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: pnpm/action-setup@v4
|
- name: Enable corepack
|
||||||
|
run: corepack enable
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 24
|
||||||
|
|
@ -43,7 +60,8 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: pnpm/action-setup@v4
|
- name: Enable corepack
|
||||||
|
run: corepack enable
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 24
|
||||||
|
|
@ -60,3 +78,24 @@ jobs:
|
||||||
grep "README.md" pack-output.txt
|
grep "README.md" pack-output.txt
|
||||||
grep "CHANGELOG.md" pack-output.txt
|
grep "CHANGELOG.md" pack-output.txt
|
||||||
grep "LICENSE" pack-output.txt
|
grep "LICENSE" pack-output.txt
|
||||||
|
|
||||||
|
coverage:
|
||||||
|
name: Coverage
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Enable corepack
|
||||||
|
run: corepack enable
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 24
|
||||||
|
cache: pnpm
|
||||||
|
- run: pnpm install --frozen-lockfile
|
||||||
|
- run: pnpm run build
|
||||||
|
- 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
|
||||||
|
|
|
||||||
27
.github/workflows/wiki-sync.yml
vendored
27
.github/workflows/wiki-sync.yml
vendored
|
|
@ -4,33 +4,22 @@ on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
paths:
|
paths:
|
||||||
- '.wiki/**'
|
- '.github/wiki/**'
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
sync:
|
sync:
|
||||||
name: Sync .wiki/ to GitHub Wiki
|
name: Sync wiki to GitHub Wiki
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Push wiki pages
|
- name: Sync .github/wiki/ to GitHub Wiki
|
||||||
uses: actions/checkout@v4
|
uses: Andrew-Chen-Wang/github-wiki-action@v4
|
||||||
with:
|
with:
|
||||||
repository: ${{ github.repository }}.wiki
|
path: .github/wiki/
|
||||||
path: wiki-repo
|
env:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
GITHUB_ACTOR: ${{ github.actor }}
|
||||||
- name: Copy wiki files
|
|
||||||
run: cp .wiki/*.md wiki-repo/
|
|
||||||
|
|
||||||
- name: Commit and push
|
|
||||||
working-directory: wiki-repo
|
|
||||||
run: |
|
|
||||||
git config user.name "github-actions[bot]"
|
|
||||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
||||||
git add -A
|
|
||||||
git diff --cached --quiet || git commit -m "Sync wiki from .wiki/ [skip ci]"
|
|
||||||
git push
|
|
||||||
|
|
|
||||||
15
.gitignore
vendored
15
.gitignore
vendored
|
|
@ -6,3 +6,18 @@ dist/
|
||||||
.claude/
|
.claude/
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
|
coverage/
|
||||||
|
|
||||||
|
# AI agent directories
|
||||||
|
.cursor/
|
||||||
|
.copilot/
|
||||||
|
.aider*
|
||||||
|
.aider.chat.history.md
|
||||||
|
.continue/
|
||||||
|
.codex/
|
||||||
|
.gemini/
|
||||||
|
.vscode/*
|
||||||
|
.idea/
|
||||||
|
.aider/
|
||||||
|
.windsurf/
|
||||||
|
.codeium/
|
||||||
|
|
|
||||||
36
CHANGELOG.md
36
CHANGELOG.md
|
|
@ -2,15 +2,33 @@
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
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.0.0/).
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||||
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## [1.0.0] - 2026-02-25
|
## [1.0.4] - 2026-06-13
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Published package now includes `dist/index.d.mts` so ESM type resolution under `node16`/`nodenext` resolves the import condition.
|
||||||
|
|
||||||
|
## [1.0.3] - 2026-06-10
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- `.toHijri()` now converts the calendar date the dayjs instance displays (via `Date.UTC(year, month, date)`) instead of passing the raw instant to hijri-core. Fixes wrong-Hijri-day results around UTC-midnight instants on hosts east or west of UTC. Requires hijri-core 1.0.3.
|
||||||
|
|
||||||
|
## [1.0.2] - 2026-05-30
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Trim README to concise quick-start format; remove verbose API prose in favor of wiki
|
||||||
|
|
||||||
|
## [1.0.1] - 2026-05-28
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Flatten exports map to ADR-015 standard (import/require/types at top level)
|
||||||
|
- Add "./package.json" export condition
|
||||||
|
- Add coverage script (c8 --reporter=lcov)
|
||||||
|
- Migrate CI from pnpm/action-setup to corepack enable
|
||||||
|
|
||||||
|
## [1.0.0] - 2026-05-28
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
- Initial release
|
||||||
- Day.js plugin with `.toHijri()`, `.fromHijri()`, `.hijriYear()`, `.hijriMonth()`, `.hijriDay()`, `.isValidHijri()`, and `.formatHijri()` methods
|
|
||||||
- Umm al-Qura (UAQ) calendar support via hijri-core
|
|
||||||
- FCNA/ISNA calendar support via hijri-core
|
|
||||||
- Full TypeScript definitions including module augmentation for Day.js types
|
|
||||||
- Dual CJS/ESM build with separate type declaration files
|
|
||||||
- Re-exports of `registerCalendar`, `getCalendar`, and `listCalendars` from hijri-core for custom calendar registration
|
|
||||||
|
|
|
||||||
171
README.md
171
README.md
|
|
@ -4,9 +4,9 @@
|
||||||
[](https://github.com/acamarata/dayjs-hijri-plus/actions/workflows/ci.yml)
|
[](https://github.com/acamarata/dayjs-hijri-plus/actions/workflows/ci.yml)
|
||||||
[](https://opensource.org/licenses/MIT)
|
[](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
|
## 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
|
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
|
## Quick Start
|
||||||
|
|
||||||
|
|
@ -24,166 +24,29 @@ import hijriPlugin from 'dayjs-hijri-plus';
|
||||||
|
|
||||||
dayjs.extend(hijriPlugin);
|
dayjs.extend(hijriPlugin);
|
||||||
|
|
||||||
// Convert a Gregorian date to Hijri
|
const d = dayjs('2023-03-23');
|
||||||
const d = dayjs(new Date(2023, 2, 23));
|
d.toHijri(); // { hy: 1444, hm: 9, hd: 1 }
|
||||||
const hijri = d.toHijri();
|
d.formatHijri('iD iMMMM iYYYY'); // '1 Ramadan 1444'
|
||||||
// => { hy: 1444, hm: 9, hd: 1 } (1 Ramadan 1444 AH)
|
d.formatHijri('iYYYY-iMM-iDD'); // '1444-09-01'
|
||||||
|
|
||||||
// Format using Hijri tokens mixed with standard Day.js tokens
|
dayjs.fromHijri(1444, 10, 1).format('YYYY-MM-DD'); // '2023-04-21'
|
||||||
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)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 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.
|
|
||||||
|
|
||||||
## Documentation
|
## 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).
|
||||||
|
|
||||||
|
## Day boundaries and time zones
|
||||||
|
|
||||||
|
`.toHijri()` converts the calendar date the dayjs instance displays — the same date you would read off the screen — regardless of the host's system timezone or whether the dayjs `utc` plugin is active. A call like `dayjs('2025-03-01').toHijri()` always maps the 1st of March 2025, not whatever local instant that string resolves to in UTC.
|
||||||
|
|
||||||
|
Religious start-of-day at sunset is out of scope. Sunset-aware day boundaries require external prayer-time data and are not handled here.
|
||||||
|
|
||||||
## Related
|
## Related
|
||||||
|
|
||||||
- [hijri-core](https://github.com/acamarata/hijri-core): zero-dependency Hijri calendar engine this plugin wraps
|
- [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 Hijri conversion for Luxon users
|
- [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
|
- [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
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|
|
||||||
8
TELEMETRY.md
Normal file
8
TELEMETRY.md
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
# Telemetry Disclosure
|
||||||
|
|
||||||
|
This package supports opt-in anonymous usage telemetry via [`@acamarata/telemetry`](https://github.com/acamarata/telemetry).
|
||||||
|
|
||||||
|
Telemetry is **off by default**. No data is sent unless you set `ACAMARATA_TELEMETRY=1`.
|
||||||
|
|
||||||
|
Full disclosure (what is sent, where it goes, how to disable):
|
||||||
|
[github.com/acamarata/telemetry/blob/main/TELEMETRY.md](https://github.com/acamarata/telemetry/blob/main/TELEMETRY.md)
|
||||||
20
eslint.config.mjs
Normal file
20
eslint.config.mjs
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
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 [
|
||||||
|
{
|
||||||
|
files: ['**/*.ts', '**/*.tsx'],
|
||||||
|
plugins: { '@typescript-eslint': tsPlugin },
|
||||||
|
languageOptions: {
|
||||||
|
parser: tsParser,
|
||||||
|
parserOptions: { project: true, tsconfigRootDir: import.meta.dirname },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...typescript.map((cfg) => ({ ...cfg, files: ['**/*.ts', '**/*.tsx'] })),
|
||||||
|
eslintConfigPrettier,
|
||||||
|
{
|
||||||
|
ignores: ['dist/', 'node_modules/', 'test.mjs', 'test-cjs.cjs'],
|
||||||
|
},
|
||||||
|
];
|
||||||
60
package.json
60
package.json
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "dayjs-hijri-plus",
|
"name": "dayjs-hijri-plus",
|
||||||
"version": "1.0.0",
|
"version": "1.0.4",
|
||||||
"description": "Day.js plugin for Hijri calendar conversion and formatting. Supports Umm al-Qura and FCNA calendars via hijri-core.",
|
"description": "Day.js plugin for Hijri calendar conversion and formatting. Supports Umm al-Qura and FCNA calendars via hijri-core.",
|
||||||
"author": "Aric Camarata",
|
"author": "Aric Camarata",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
@ -9,9 +9,11 @@
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"import": { "types": "./dist/index.d.mts", "default": "./dist/index.mjs" },
|
"types": "./dist/index.d.ts",
|
||||||
"require": { "types": "./dist/index.d.ts", "default": "./dist/index.cjs" }
|
"import": "./dist/index.mjs",
|
||||||
}
|
"require": "./dist/index.cjs"
|
||||||
|
},
|
||||||
|
"./package.json": "./package.json"
|
||||||
},
|
},
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
"files": [
|
"files": [
|
||||||
|
|
@ -23,14 +25,22 @@
|
||||||
"CHANGELOG.md",
|
"CHANGELOG.md",
|
||||||
"LICENSE"
|
"LICENSE"
|
||||||
],
|
],
|
||||||
"engines": { "node": ">=20" },
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
},
|
||||||
"packageManager": "pnpm@10.30.1",
|
"packageManager": "pnpm@10.30.1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsup",
|
"build": "tsup",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"pretest": "tsup",
|
"pretest": "tsup",
|
||||||
"test": "node test.mjs && node test-cjs.cjs",
|
"test": "node --test test.mjs && node --test test-cjs.cjs",
|
||||||
"prepublishOnly": "tsup"
|
"lint": "eslint src/",
|
||||||
|
"format": "prettier --write src/",
|
||||||
|
"format:check": "prettier --check src/",
|
||||||
|
"prepack": "pnpm run build",
|
||||||
|
"coverage": "c8 --reporter=lcov --reporter=text node test.mjs",
|
||||||
|
"docs": "typedoc --out .github/wiki/api src/index.ts",
|
||||||
|
"postbuild": "cp dist/index.d.ts dist/index.d.mts"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"dayjs",
|
"dayjs",
|
||||||
|
|
@ -49,14 +59,38 @@
|
||||||
"hijri-core": "^1.0.0"
|
"hijri-core": "^1.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^22.0.0",
|
"@acamarata/eslint-config": "^0.1.0",
|
||||||
|
"@acamarata/prettier-config": "^0.1.0",
|
||||||
|
"@acamarata/telemetry": "^0.1.0",
|
||||||
|
"@acamarata/tsconfig": "^0.1.0",
|
||||||
|
"@eslint/js": "^10.0.1",
|
||||||
|
"@types/node": "^25.3.5",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^8.56.1",
|
||||||
|
"@typescript-eslint/parser": "^8.56.1",
|
||||||
|
"c8": "^10.1.3",
|
||||||
"dayjs": "^1.11.0",
|
"dayjs": "^1.11.0",
|
||||||
"hijri-core": "^1.0.0",
|
"eslint": "^10.0.3",
|
||||||
|
"eslint-config-prettier": "^10.1.8",
|
||||||
|
"hijri-core": "^1.0.3",
|
||||||
|
"prettier": "^3.8.1",
|
||||||
"tsup": "^8.0.0",
|
"tsup": "^8.0.0",
|
||||||
"typescript": "^5.5.0"
|
"typedoc": "^0.28.19",
|
||||||
|
"typedoc-plugin-markdown": "^4.11.0",
|
||||||
|
"typescript": "^5.5.0",
|
||||||
|
"typescript-eslint": "^8.56.1"
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public",
|
||||||
|
"registry": "https://registry.npmjs.org/"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/acamarata/dayjs-hijri-plus.git"
|
||||||
},
|
},
|
||||||
"publishConfig": { "access": "public", "registry": "https://registry.npmjs.org/" },
|
|
||||||
"repository": { "type": "git", "url": "git+https://github.com/acamarata/dayjs-hijri-plus.git" },
|
|
||||||
"homepage": "https://github.com/acamarata/dayjs-hijri-plus#readme",
|
"homepage": "https://github.com/acamarata/dayjs-hijri-plus#readme",
|
||||||
"bugs": { "url": "https://github.com/acamarata/dayjs-hijri-plus/issues" }
|
"bugs": {
|
||||||
|
"url": "https://github.com/acamarata/dayjs-hijri-plus/issues"
|
||||||
|
},
|
||||||
|
"type": "module",
|
||||||
|
"prettier": "@acamarata/prettier-config"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1393
pnpm-lock.yaml
1393
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
250
src/index.ts
250
src/index.ts
|
|
@ -1,36 +1,87 @@
|
||||||
import type { PluginFunc } from 'dayjs';
|
import type { PluginFunc } from "dayjs";
|
||||||
import {
|
import { toHijri, toGregorian, hmLong, hmMedium, hwLong, hwShort, hwNumeric } from "hijri-core";
|
||||||
toHijri,
|
import type { ConversionOptions, HijriDate } from "./types";
|
||||||
toGregorian,
|
|
||||||
hmLong,
|
|
||||||
hmMedium,
|
|
||||||
hwLong,
|
|
||||||
hwShort,
|
|
||||||
hwNumeric,
|
|
||||||
} from 'hijri-core';
|
|
||||||
import type { ConversionOptions, HijriDate } from './types';
|
|
||||||
|
|
||||||
// Augment Day.js to expose plugin methods on the instance type.
|
// Augment Day.js to expose plugin methods on the instance type.
|
||||||
declare module 'dayjs' {
|
declare module "dayjs" {
|
||||||
interface 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;
|
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;
|
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;
|
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;
|
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;
|
hijriDay(opts?: ConversionOptions): number | null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format the date using Hijri tokens (i-prefixed) and standard Day.js tokens.
|
* Format the date using a mix of Hijri tokens (`i`-prefixed) and standard
|
||||||
* Returns an empty string if the date is outside the supported range.
|
* 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;
|
formatHijri(formatStr: string, opts?: ConversionOptions): string;
|
||||||
}
|
}
|
||||||
|
|
@ -40,8 +91,33 @@ declare module 'dayjs' {
|
||||||
// Using the function declaration form (same pattern as dayjs timezone plugin)
|
// Using the function declaration form (same pattern as dayjs timezone plugin)
|
||||||
// because dayjs does not export an IStatic interface for module augmentation.
|
// because dayjs does not export an IStatic interface for module augmentation.
|
||||||
// import('dayjs').Dayjs is used explicitly to satisfy the tsup DTS emitter.
|
// import('dayjs').Dayjs is used explicitly to satisfy the tsup DTS emitter.
|
||||||
declare module 'dayjs' {
|
declare module "dayjs" {
|
||||||
function fromHijri(hy: number, hm: number, hd: number, opts?: ConversionOptions): import('dayjs').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,
|
||||||
|
hd: number,
|
||||||
|
opts?: ConversionOptions,
|
||||||
|
): import("dayjs").Dayjs;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hijri-specific format tokens, ordered longest-first to prevent partial matches.
|
// Hijri-specific format tokens, ordered longest-first to prevent partial matches.
|
||||||
|
|
@ -56,19 +132,46 @@ 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
|
* Day.js uses `[...]` for literal text. A `]` inside such a section would
|
||||||
* close it prematurely, so we split on `]` and re-join with `][` (which
|
* close it prematurely, so we split on `]` and re-join with `][` (which
|
||||||
* closes the current literal section, outputs a raw `]` (Day.js passes
|
* 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 {
|
function lit(value: string): string {
|
||||||
return '[' + value.split(']').join(']][') + ']';
|
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) => {
|
const plugin: PluginFunc = (_option, dayjsClass, dayjsFactory) => {
|
||||||
// ------------------------------------------------------------------ //
|
// ------------------------------------------------------------------ //
|
||||||
// Instance methods //
|
// Instance methods //
|
||||||
// ------------------------------------------------------------------ //
|
// ------------------------------------------------------------------ //
|
||||||
|
|
||||||
dayjsClass.prototype.toHijri = function (opts?: ConversionOptions): HijriDate | null {
|
dayjsClass.prototype.toHijri = function (opts?: ConversionOptions): HijriDate | null {
|
||||||
return toHijri(this.toDate(), opts);
|
// Build a UTC-noon Date from the calendar date this instance displays so
|
||||||
|
// that hijri-core's UTC-day contract reads the correct day regardless of
|
||||||
|
// the host timezone or whether the dayjs utc plugin is active.
|
||||||
|
// dayjs .month() is 0-based, matching Date.UTC's month parameter.
|
||||||
|
return toHijri(new Date(Date.UTC(this.year(), this.month(), this.date())), opts);
|
||||||
};
|
};
|
||||||
|
|
||||||
dayjsClass.prototype.isValidHijri = function (opts?: ConversionOptions): boolean {
|
dayjsClass.prototype.isValidHijri = function (opts?: ConversionOptions): boolean {
|
||||||
|
|
@ -92,7 +195,7 @@ const plugin: PluginFunc = (_option, dayjsClass, dayjsFactory) => {
|
||||||
opts?: ConversionOptions,
|
opts?: ConversionOptions,
|
||||||
): string {
|
): string {
|
||||||
const hijri = this.toHijri(opts);
|
const hijri = this.toHijri(opts);
|
||||||
if (!hijri) return '';
|
if (!hijri) return "";
|
||||||
|
|
||||||
// Day.js .day() returns 0 (Sunday) ... 6 (Saturday), matching the index
|
// Day.js .day() returns 0 (Sunday) ... 6 (Saturday), matching the index
|
||||||
// layout of hwLong, hwShort, and hwNumeric from hijri-core.
|
// layout of hwLong, hwShort, and hwNumeric from hijri-core.
|
||||||
|
|
@ -100,20 +203,40 @@ const plugin: PluginFunc = (_option, dayjsClass, dayjsFactory) => {
|
||||||
|
|
||||||
const replaced = formatStr.replace(HIJRI_TOKEN_RE, (token) => {
|
const replaced = formatStr.replace(HIJRI_TOKEN_RE, (token) => {
|
||||||
switch (token) {
|
switch (token) {
|
||||||
case 'iYYYY': return lit(String(hijri.hy).padStart(4, '0'));
|
case "iYYYY":
|
||||||
case 'iYY': return lit(String(hijri.hy % 100).padStart(2, '0'));
|
return lit(String(hijri.hy).padStart(4, "0"));
|
||||||
case 'iMMMM': return lit(hmLong[hijri.hm - 1]);
|
case "iYY":
|
||||||
case 'iMMM': return lit(hmMedium[hijri.hm - 1]);
|
return lit(String(hijri.hy % 100).padStart(2, "0"));
|
||||||
case 'iMM': return lit(String(hijri.hm).padStart(2, '0'));
|
case "iMMMM":
|
||||||
case 'iM': return lit(String(hijri.hm));
|
// Non-null: hijri.hm is a valid Hijri month 1-12; hm-1 is always within hmLong bounds.
|
||||||
case 'iDD': return lit(String(hijri.hd).padStart(2, '0'));
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
case 'iD': return lit(String(hijri.hd));
|
return lit(hmLong[hijri.hm - 1]!);
|
||||||
case 'iEEEE': return lit(hwLong[dow]);
|
case "iMMM":
|
||||||
case 'iEEE': return lit(hwShort[dow]);
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
case 'iE': return lit(String(hwNumeric[dow]));
|
return lit(hmMedium[hijri.hm - 1]!);
|
||||||
case 'ioooo':
|
case "iMM":
|
||||||
case 'iooo': return lit('AH');
|
return lit(String(hijri.hm).padStart(2, "0"));
|
||||||
default: return token;
|
case "iM":
|
||||||
|
return lit(String(hijri.hm));
|
||||||
|
case "iDD":
|
||||||
|
return lit(String(hijri.hd).padStart(2, "0"));
|
||||||
|
case "iD":
|
||||||
|
return lit(String(hijri.hd));
|
||||||
|
case "iEEEE":
|
||||||
|
// Non-null: dow is always 0-6 (day of week), within hwLong bounds.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
return lit(hwLong[dow]!);
|
||||||
|
case "iEEE":
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
return lit(hwShort[dow]!);
|
||||||
|
case "iE":
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
return lit(String(hwNumeric[dow]!));
|
||||||
|
case "ioooo":
|
||||||
|
case "iooo":
|
||||||
|
return lit("AH");
|
||||||
|
default:
|
||||||
|
return token;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -143,22 +266,49 @@ const plugin: PluginFunc = (_option, dayjsClass, dayjsFactory) => {
|
||||||
if (!greg) {
|
if (!greg) {
|
||||||
throw new Error(`Invalid or out-of-range Hijri date: ${hy}/${hm}/${hd}`);
|
throw new Error(`Invalid or out-of-range Hijri date: ${hy}/${hm}/${hd}`);
|
||||||
}
|
}
|
||||||
// Construct from ISO date string to avoid timezone offset issues.
|
// Construct from an ISO date string (YYYY-MM-DD) so the result is the
|
||||||
// dayjsFactory(Date) interprets the Date in local time; a UTC-midnight Date
|
// Gregorian calendar day that corresponds to the Hijri date, at local
|
||||||
// in western timezones would resolve to the previous local day.
|
// midnight in whatever timezone the consumer uses. Passing a raw Date
|
||||||
|
// object to dayjsFactory() would interpret it as a UTC instant and could
|
||||||
|
// land on the previous local day for hosts west of UTC.
|
||||||
const y = greg.getUTCFullYear();
|
const y = greg.getUTCFullYear();
|
||||||
const mo = String(greg.getUTCMonth() + 1).padStart(2, '0');
|
const mo = String(greg.getUTCMonth() + 1).padStart(2, "0");
|
||||||
const dy = String(greg.getUTCDate()).padStart(2, '0');
|
const dy = String(greg.getUTCDate()).padStart(2, "0");
|
||||||
return dayjsFactory(`${y}-${mo}-${dy}`);
|
return dayjsFactory(`${y}-${mo}-${dy}`);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export default plugin;
|
export default plugin;
|
||||||
|
|
||||||
// Re-export hijri-core types for consumers who import from dayjs-hijri-plus.
|
/**
|
||||||
export type { HijriDate, ConversionOptions, CalendarSystem } from './types';
|
* Re-exported from hijri-core for consumers who import from dayjs-hijri-plus.
|
||||||
export type { CalendarEngine } from 'hijri-core';
|
* Avoids requiring hijri-core as a direct dependency just to use these types.
|
||||||
|
*/
|
||||||
|
export type { HijriDate, ConversionOptions } from "./types";
|
||||||
|
|
||||||
// Re-export the registry API so callers can register custom calendar engines
|
/**
|
||||||
// without adding hijri-core as a direct dependency.
|
* Re-exported CalendarEngine interface from hijri-core.
|
||||||
export { registerCalendar, getCalendar, listCalendars } from 'hijri-core';
|
* Use this type to implement custom calendar engines for `registerCalendar`.
|
||||||
|
*/
|
||||||
|
export type { CalendarEngine } from "hijri-core";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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";
|
||||||
|
|
||||||
|
// ── Opt-in anonymous telemetry ────────────────────────────────────────────────
|
||||||
|
// Off by default. Enable: ACAMARATA_TELEMETRY=1
|
||||||
|
// What is sent + how to disable: https://github.com/acamarata/telemetry/blob/main/TELEMETRY.md
|
||||||
|
import("@acamarata/telemetry")
|
||||||
|
.then(({ track }) => track("load", { package: "dayjs-hijri-plus", version: "1.0.4" }))
|
||||||
|
.catch(() => {
|
||||||
|
// telemetry not installed or disabled — that's fine
|
||||||
|
});
|
||||||
|
|
|
||||||
13
src/types.ts
13
src/types.ts
|
|
@ -1,13 +1,2 @@
|
||||||
import type { HijriDate, ConversionOptions } from 'hijri-core';
|
import type { HijriDate, ConversionOptions } from "hijri-core";
|
||||||
export type { HijriDate, ConversionOptions };
|
export type { HijriDate, ConversionOptions };
|
||||||
|
|
||||||
/** A registered calendar identifier. The built-in values are 'uaq' and 'fcna'. */
|
|
||||||
export type CalendarSystem = string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Options passed to plugin methods. Inherits `calendar` from ConversionOptions
|
|
||||||
* so callers can switch between 'uaq' (default) and 'fcna'.
|
|
||||||
*/
|
|
||||||
export interface HijriPluginOptions extends ConversionOptions {
|
|
||||||
// calendar?: string (inherited: 'uaq' | 'fcna' | any registered calendar id)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
95
test-cjs.cjs
95
test-cjs.cjs
|
|
@ -1,67 +1,74 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
const { describe, it } = require('node:test');
|
||||||
const assert = require('node:assert/strict');
|
const assert = require('node:assert/strict');
|
||||||
const dayjs = require('dayjs');
|
const dayjs = require('dayjs');
|
||||||
const { default: plugin } = require('./dist/index.cjs');
|
const { default: plugin } = require('./dist/index.cjs');
|
||||||
|
|
||||||
dayjs.extend(plugin);
|
dayjs.extend(plugin);
|
||||||
|
|
||||||
let passed = 0;
|
|
||||||
let total = 0;
|
|
||||||
|
|
||||||
function test(name, fn) {
|
|
||||||
total++;
|
|
||||||
try {
|
|
||||||
fn();
|
|
||||||
console.log(`[${name}]... PASS`);
|
|
||||||
passed++;
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`[${name}]... FAIL: ${err.message}`);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const D_RAMADAN_1444 = new Date(2023, 2, 23, 12);
|
const D_RAMADAN_1444 = new Date(2023, 2, 23, 12);
|
||||||
const D_MUHARRAM_1446 = new Date(2024, 6, 7, 12);
|
const D_MUHARRAM_1446 = new Date(2024, 6, 7, 12);
|
||||||
|
|
||||||
test('plugin registers (CJS)', () => {
|
describe('CJS: plugin registration', () => {
|
||||||
const d = dayjs(D_RAMADAN_1444);
|
it('registers on dayjs', () => {
|
||||||
assert.equal(typeof d.toHijri, 'function');
|
const d = dayjs(D_RAMADAN_1444);
|
||||||
assert.equal(typeof d.formatHijri, 'function');
|
assert.equal(typeof d.toHijri, 'function');
|
||||||
assert.equal(typeof dayjs.fromHijri, 'function');
|
assert.equal(typeof d.formatHijri, 'function');
|
||||||
|
assert.equal(typeof dayjs.fromHijri, 'function');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('toHijri (CJS): 2023-03-23 -> 1 Ramadan 1444', () => {
|
describe('CJS: toHijri', () => {
|
||||||
const h = dayjs(D_RAMADAN_1444).toHijri();
|
it('2023-03-23 -> 1 Ramadan 1444', () => {
|
||||||
assert.deepEqual(h, { hy: 1444, hm: 9, hd: 1 });
|
const h = dayjs(D_RAMADAN_1444).toHijri();
|
||||||
|
assert.deepEqual(h, { hy: 1444, hm: 9, hd: 1 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('2024-07-07 -> 1 Muharram 1446', () => {
|
||||||
|
const h = dayjs(D_MUHARRAM_1446).toHijri();
|
||||||
|
assert.deepEqual(h, { hy: 1446, hm: 1, hd: 1 });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('toHijri (CJS): 2024-07-07 -> 1 Muharram 1446', () => {
|
describe('CJS: fromHijri', () => {
|
||||||
const h = dayjs(D_MUHARRAM_1446).toHijri();
|
it('1444/9/1 -> 2023-03-23', () => {
|
||||||
assert.deepEqual(h, { hy: 1446, hm: 1, hd: 1 });
|
const d = dayjs.fromHijri(1444, 9, 1);
|
||||||
|
assert.equal(d.format('YYYY-MM-DD'), '2023-03-23');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws for out-of-range date', () => {
|
||||||
|
assert.throws(() => dayjs.fromHijri(1301, 1, 1), /Invalid or out-of-range/);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('fromHijri (CJS): 1444/9/1 -> 2023-03-23', () => {
|
describe('CJS: formatHijri', () => {
|
||||||
const d = dayjs.fromHijri(1444, 9, 1);
|
it('iYYYY-iMM-iDD', () => {
|
||||||
assert.equal(d.format('YYYY-MM-DD'), '2023-03-23');
|
const result = dayjs(D_RAMADAN_1444).formatHijri('iYYYY-iMM-iDD');
|
||||||
|
assert.equal(result, '1444-09-01');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('iMMMM -> Ramadan', () => {
|
||||||
|
const result = dayjs(D_RAMADAN_1444).formatHijri('iMMMM');
|
||||||
|
assert.equal(result, 'Ramadan');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('formatHijri (CJS): iYYYY-iMM-iDD', () => {
|
describe('CJS: isValidHijri', () => {
|
||||||
const result = dayjs(D_RAMADAN_1444).formatHijri('iYYYY-iMM-iDD');
|
it('true for in-range date', () => {
|
||||||
assert.equal(result, '1444-09-01');
|
assert.equal(dayjs(D_RAMADAN_1444).isValidHijri(), true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('formatHijri (CJS): iMMMM -> Ramadan', () => {
|
describe('CJS: UTC-day boundary (regression)', () => {
|
||||||
const result = dayjs(D_RAMADAN_1444).formatHijri('iMMMM');
|
it('dayjs("2025-03-01").toHijri() -> 1 Ramadan 1446', () => {
|
||||||
assert.equal(result, 'Ramadan');
|
const h = dayjs('2025-03-01').toHijri();
|
||||||
});
|
assert.deepEqual(h, { hy: 1446, hm: 9, hd: 1 });
|
||||||
|
});
|
||||||
|
|
||||||
test('isValidHijri (CJS): true for in-range date', () => {
|
it('round-trip: fromHijri(1446,9,1) then toHijri() -> {1446,9,1}', () => {
|
||||||
assert.equal(dayjs(D_RAMADAN_1444).isValidHijri(), true);
|
const d = dayjs.fromHijri(1446, 9, 1);
|
||||||
|
const h = d.toHijri();
|
||||||
|
assert.deepEqual(h, { hy: 1446, hm: 9, hd: 1 });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('fromHijri (CJS): throws for out-of-range date', () => {
|
|
||||||
assert.throws(() => dayjs.fromHijri(1301, 1, 1), /Invalid or out-of-range/);
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`\n${passed}/${total} tests passed`);
|
|
||||||
|
|
|
||||||
179
test.mjs
179
test.mjs
|
|
@ -1,110 +1,119 @@
|
||||||
|
import { describe, it } from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import plugin from './dist/index.mjs';
|
import plugin from './dist/index.mjs';
|
||||||
|
|
||||||
dayjs.extend(plugin);
|
dayjs.extend(plugin);
|
||||||
|
|
||||||
let passed = 0;
|
|
||||||
let total = 0;
|
|
||||||
|
|
||||||
function test(name, fn) {
|
|
||||||
total++;
|
|
||||||
try {
|
|
||||||
fn();
|
|
||||||
console.log(`[${name}]... PASS`);
|
|
||||||
passed++;
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`[${name}]... FAIL: ${err.message}`);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use noon to avoid UTC midnight boundary issues across timezones.
|
|
||||||
const D_RAMADAN_1444 = new Date(2023, 2, 23, 12); // 1 Ramadan 1444
|
const D_RAMADAN_1444 = new Date(2023, 2, 23, 12); // 1 Ramadan 1444
|
||||||
const D_MUHARRAM_1446 = new Date(2024, 6, 7, 12); // 1 Muharram 1446
|
const D_MUHARRAM_1446 = new Date(2024, 6, 7, 12); // 1 Muharram 1446
|
||||||
|
|
||||||
test('plugin registers on dayjs', () => {
|
describe('plugin registration', () => {
|
||||||
const d = dayjs(D_RAMADAN_1444);
|
it('registers on dayjs', () => {
|
||||||
assert.equal(typeof d.toHijri, 'function');
|
const d = dayjs(D_RAMADAN_1444);
|
||||||
assert.equal(typeof d.formatHijri, 'function');
|
assert.equal(typeof d.toHijri, 'function');
|
||||||
assert.equal(typeof d.isValidHijri, 'function');
|
assert.equal(typeof d.formatHijri, 'function');
|
||||||
assert.equal(typeof d.hijriYear, 'function');
|
assert.equal(typeof d.isValidHijri, 'function');
|
||||||
assert.equal(typeof d.hijriMonth, 'function');
|
assert.equal(typeof d.hijriYear, 'function');
|
||||||
assert.equal(typeof d.hijriDay, 'function');
|
assert.equal(typeof d.hijriMonth, 'function');
|
||||||
assert.equal(typeof dayjs.fromHijri, 'function');
|
assert.equal(typeof d.hijriDay, 'function');
|
||||||
|
assert.equal(typeof dayjs.fromHijri, 'function');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('toHijri: 2023-03-23 -> 1 Ramadan 1444', () => {
|
describe('toHijri', () => {
|
||||||
const h = dayjs(D_RAMADAN_1444).toHijri();
|
it('2023-03-23 -> 1 Ramadan 1444', () => {
|
||||||
assert.deepEqual(h, { hy: 1444, hm: 9, hd: 1 });
|
const h = dayjs(D_RAMADAN_1444).toHijri();
|
||||||
|
assert.deepEqual(h, { hy: 1444, hm: 9, hd: 1 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('2024-07-07 -> 1 Muharram 1446', () => {
|
||||||
|
const h = dayjs(D_MUHARRAM_1446).toHijri();
|
||||||
|
assert.deepEqual(h, { hy: 1446, hm: 1, hd: 1 });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('toHijri: 2024-07-07 -> 1 Muharram 1446', () => {
|
describe('fromHijri', () => {
|
||||||
const h = dayjs(D_MUHARRAM_1446).toHijri();
|
it('1444/9/1 -> 2023-03-23', () => {
|
||||||
assert.deepEqual(h, { hy: 1446, hm: 1, hd: 1 });
|
const d = dayjs.fromHijri(1444, 9, 1);
|
||||||
|
assert.equal(d.format('YYYY-MM-DD'), '2023-03-23');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('1446/1/1 -> 2024-07-07', () => {
|
||||||
|
const d = dayjs.fromHijri(1446, 1, 1);
|
||||||
|
assert.equal(d.format('YYYY-MM-DD'), '2024-07-07');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws for out-of-range UAQ date', () => {
|
||||||
|
assert.throws(() => dayjs.fromHijri(1301, 1, 1), /Invalid or out-of-range/);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('fromHijri: 1444/9/1 -> 2023-03-23', () => {
|
describe('accessors', () => {
|
||||||
const d = dayjs.fromHijri(1444, 9, 1);
|
it('hijriYear/hijriMonth/hijriDay on 1 Ramadan 1444', () => {
|
||||||
assert.equal(d.format('YYYY-MM-DD'), '2023-03-23');
|
const d = dayjs(D_RAMADAN_1444);
|
||||||
|
assert.equal(d.hijriYear(), 1444);
|
||||||
|
assert.equal(d.hijriMonth(), 9);
|
||||||
|
assert.equal(d.hijriDay(), 1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('fromHijri: 1446/1/1 -> 2024-07-07', () => {
|
describe('formatHijri', () => {
|
||||||
const d = dayjs.fromHijri(1446, 1, 1);
|
it('iYYYY-iMM-iDD on 1 Ramadan 1444', () => {
|
||||||
assert.equal(d.format('YYYY-MM-DD'), '2024-07-07');
|
const result = dayjs(D_RAMADAN_1444).formatHijri('iYYYY-iMM-iDD');
|
||||||
|
assert.equal(result, '1444-09-01');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('iMMMM -> Ramadan', () => {
|
||||||
|
const result = dayjs(D_RAMADAN_1444).formatHijri('iMMMM');
|
||||||
|
assert.equal(result, 'Ramadan');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('iEEEE on 2023-03-23 (Thursday)', () => {
|
||||||
|
const result = dayjs(D_RAMADAN_1444).formatHijri('iEEEE');
|
||||||
|
assert.equal(result, 'Yawm al-Khamis');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ioooo -> AH', () => {
|
||||||
|
const result = dayjs(D_RAMADAN_1444).formatHijri('ioooo');
|
||||||
|
assert.equal(result, 'AH');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('passthrough: iYYYY YYYY contains both Hijri and Gregorian year', () => {
|
||||||
|
const result = dayjs(D_RAMADAN_1444).formatHijri('iYYYY YYYY');
|
||||||
|
assert.ok(result.includes('1444'), `Expected Hijri year 1444 in: ${result}`);
|
||||||
|
assert.ok(result.includes('2023'), `Expected Gregorian year 2023 in: ${result}`);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('hijriYear/hijriMonth/hijriDay accessors on 1 Ramadan 1444', () => {
|
describe('FCNA calendar', () => {
|
||||||
const d = dayjs(D_RAMADAN_1444);
|
it('toHijri returns a valid HijriDate', () => {
|
||||||
assert.equal(d.hijriYear(), 1444);
|
const h = dayjs(D_RAMADAN_1444).toHijri({ calendar: 'fcna' });
|
||||||
assert.equal(d.hijriMonth(), 9);
|
assert.notEqual(h, null);
|
||||||
assert.equal(d.hijriDay(), 1);
|
assert.equal(typeof h.hy, 'number');
|
||||||
|
assert.equal(typeof h.hm, 'number');
|
||||||
|
assert.equal(typeof h.hd, 'number');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('formatHijri: iYYYY-iMM-iDD on 1 Ramadan 1444', () => {
|
describe('isValidHijri', () => {
|
||||||
const result = dayjs(D_RAMADAN_1444).formatHijri('iYYYY-iMM-iDD');
|
it('returns true for in-range date', () => {
|
||||||
assert.equal(result, '1444-09-01');
|
const valid = dayjs(D_RAMADAN_1444).isValidHijri();
|
||||||
|
assert.equal(valid, true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('formatHijri: iMMMM -> Ramadan', () => {
|
describe('UTC-day boundary (regression)', () => {
|
||||||
const result = dayjs(D_RAMADAN_1444).formatHijri('iMMMM');
|
// dayjs("YYYY-MM-DD") parses as local midnight — timezone-invariant anchor
|
||||||
assert.equal(result, 'Ramadan');
|
// for toHijri now that the adapter reads the displayed calendar date.
|
||||||
});
|
it('dayjs("2025-03-01").toHijri() -> 1 Ramadan 1446', () => {
|
||||||
|
const h = dayjs('2025-03-01').toHijri();
|
||||||
|
assert.deepEqual(h, { hy: 1446, hm: 9, hd: 1 });
|
||||||
|
});
|
||||||
|
|
||||||
test('formatHijri: iEEEE on 2023-03-23 (Thursday)', () => {
|
it('round-trip: fromHijri(1446,9,1) then toHijri() -> {1446,9,1}', () => {
|
||||||
const result = dayjs(D_RAMADAN_1444).formatHijri('iEEEE');
|
const d = dayjs.fromHijri(1446, 9, 1);
|
||||||
// 2023-03-23 is a Thursday; hwLong[4] = 'Yawm al-Khamis'
|
const h = d.toHijri();
|
||||||
assert.equal(result, 'Yawm al-Khamis');
|
assert.deepEqual(h, { hy: 1446, hm: 9, hd: 1 });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('formatHijri: ioooo -> AH', () => {
|
|
||||||
const result = dayjs(D_RAMADAN_1444).formatHijri('ioooo');
|
|
||||||
assert.equal(result, 'AH');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('FCNA calendar: toHijri returns a valid HijriDate', () => {
|
|
||||||
const h = dayjs(D_RAMADAN_1444).toHijri({ calendar: 'fcna' });
|
|
||||||
assert.notEqual(h, null);
|
|
||||||
assert.equal(typeof h.hy, 'number');
|
|
||||||
assert.equal(typeof h.hm, 'number');
|
|
||||||
assert.equal(typeof h.hd, 'number');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('isValidHijri returns true for in-range date', () => {
|
|
||||||
const valid = dayjs(D_RAMADAN_1444).isValidHijri();
|
|
||||||
assert.equal(valid, true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('fromHijri throws for out-of-range UAQ date', () => {
|
|
||||||
// 1301 is before the UAQ table begins (coverage starts at 1318)
|
|
||||||
assert.throws(() => dayjs.fromHijri(1301, 1, 1), /Invalid or out-of-range/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('formatHijri passthrough: iYYYY YYYY contains both Hijri and Gregorian year', () => {
|
|
||||||
const result = dayjs(D_RAMADAN_1444).formatHijri('iYYYY YYYY');
|
|
||||||
// Should contain '1444' (Hijri) and '2023' (Gregorian)
|
|
||||||
assert.ok(result.includes('1444'), `Expected Hijri year 1444 in: ${result}`);
|
|
||||||
assert.ok(result.includes('2023'), `Expected Gregorian year 2023 in: ${result}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`\n${passed}/${total} tests passed`);
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,9 @@
|
||||||
{
|
{
|
||||||
|
"extends": "@acamarata/tsconfig/tsconfig.library.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2020",
|
"noImplicitReturns": true,
|
||||||
"module": "ESNext",
|
"noFallthroughCasesInSwitch": true,
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"strict": true,
|
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"declaration": true,
|
|
||||||
"declarationMap": true,
|
|
||||||
"sourceMap": true,
|
|
||||||
"outDir": "dist",
|
|
||||||
"rootDir": "src",
|
"rootDir": "src",
|
||||||
"types": ["node"]
|
"types": ["node"]
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ export default defineConfig({
|
||||||
splitting: false,
|
splitting: false,
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
target: 'es2020',
|
target: 'es2020',
|
||||||
platform: 'node',
|
platform: 'neutral',
|
||||||
external: ['dayjs', 'hijri-core'],
|
external: ['dayjs', 'hijri-core'],
|
||||||
outExtension({ format }) {
|
outExtension({ format }) {
|
||||||
return { js: format === 'esm' ? '.mjs' : '.cjs' };
|
return { js: format === 'esm' ? '.mjs' : '.cjs' };
|
||||||
|
|
|
||||||
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
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue