mirror of
https://github.com/acamarata/date-fns-hijri.git
synced 2026-06-30 18:54:25 +00:00
Compare commits
27 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee6c78d68f | ||
|
|
4d414b2056 | ||
|
|
750f7e19ad | ||
|
|
a8e72ac2b2 | ||
|
|
d12117f000 | ||
|
|
f260912927 | ||
|
|
b96d6fc921 | ||
|
|
0e0e9e2021 | ||
|
|
fe9c2c932c | ||
|
|
9859725e64 | ||
|
|
e8ade1f9c4 | ||
|
|
a86df7dc09 | ||
|
|
d19c624bce | ||
|
|
72644587c5 | ||
|
|
e6780c3aae | ||
|
|
b6ee6e0331 | ||
|
|
061b3e8b8f | ||
|
|
711444bb00 | ||
|
|
f516f24987 | ||
|
|
f00656ee20 | ||
|
|
bc32214cee | ||
|
|
fb861e38de | ||
|
|
d2bbbf72c4 | ||
|
|
9eb0459606 | ||
|
|
0995d8a5a7 | ||
|
|
815c0418a4 | ||
|
|
9e88126a10 |
54 changed files with 4335 additions and 790 deletions
|
|
@ -1,12 +1,12 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.{js,mjs,cjs,ts,mts,cts,json,yaml,yml,md}]
|
||||
[*.{ts,mts,cts,js,mjs,cjs,json,yaml,yml,md}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
|
|
|
|||
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
github: [acamarata]
|
||||
30
.github/docs/CHANGELOG.md
vendored
Normal file
30
.github/docs/CHANGELOG.md
vendored
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
# 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/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [1.0.0] - 2026-02-25
|
||||
|
||||
### Added
|
||||
|
||||
- `toHijriDate(date, options?)` - Convert a Gregorian Date to a HijriDate object
|
||||
- `fromHijriDate(hy, hm, hd, options?)` - Convert Hijri date components to a Gregorian Date
|
||||
- `isValidHijriDate(hy, hm, hd, options?)` - Validate a Hijri date
|
||||
- `getHijriYear(date, options?)` - Extract the Hijri year from a Gregorian date
|
||||
- `getHijriMonth(date, options?)` - Extract the Hijri month (1-12) from a Gregorian date
|
||||
- `getHijriDay(date, options?)` - Extract the Hijri day of month from a Gregorian date
|
||||
- `getDaysInHijriMonth(hy, hm, options?)` - Days in a given Hijri month (29 or 30)
|
||||
- `getHijriMonthName(hm, length?)` - English month name in long, medium, or short form
|
||||
- `getHijriWeekdayName(date, length?)` - Arabic weekday name (long or short)
|
||||
- `formatHijriDate(date, formatStr, options?)` - Format a date with Hijri tokens
|
||||
- `addHijriMonths(date, months, options?)` - Add Hijri months to a date
|
||||
- `addHijriYears(date, years, options?)` - Add Hijri years to a date
|
||||
- `startOfHijriMonth(date, options?)` - First day of the Hijri month
|
||||
- `endOfHijriMonth(date, options?)` - Last day of the Hijri month
|
||||
- `isSameHijriMonth(dateA, dateB, options?)` - Check if two dates share a Hijri month
|
||||
- `isSameHijriYear(dateA, dateB, options?)` - Check if two dates share a Hijri year
|
||||
- `getHijriQuarter(date, options?)` - Hijri quarter (1-4) for a date
|
||||
- Full TypeScript definitions with dual CJS/ESM build
|
||||
- Support for Umm al-Qura (UAQ) and FCNA/ISNA calendar systems via `hijri-core`
|
||||
|
|
@ -15,7 +15,7 @@ interface ConversionOptions {
|
|||
### `toHijriDate`
|
||||
|
||||
```typescript
|
||||
function toHijriDate(date: Date, options?: ConversionOptions): HijriDate | null
|
||||
function toHijriDate(date: Date, options?: ConversionOptions): HijriDate | null;
|
||||
```
|
||||
|
||||
Convert a Gregorian `Date` to a Hijri date object.
|
||||
|
|
@ -24,9 +24,9 @@ Returns `null` when the date falls outside the calendar's supported range. UAQ c
|
|||
|
||||
**Parameters:**
|
||||
|
||||
| Name | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `date` | `Date` | A valid Gregorian date |
|
||||
| Name | Type | Description |
|
||||
| --------- | ------------------- | ----------------------------------- |
|
||||
| `date` | `Date` | A valid Gregorian date |
|
||||
| `options` | `ConversionOptions` | Optional. Calendar system selection |
|
||||
|
||||
**Returns:** `HijriDate | null`
|
||||
|
|
@ -48,7 +48,7 @@ toHijriDate(new Date(1800, 0, 1));
|
|||
### `fromHijriDate`
|
||||
|
||||
```typescript
|
||||
function fromHijriDate(hy: number, hm: number, hd: number, options?: ConversionOptions): Date
|
||||
function fromHijriDate(hy: number, hm: number, hd: number, options?: ConversionOptions): Date;
|
||||
```
|
||||
|
||||
Convert a Hijri date to a Gregorian `Date`.
|
||||
|
|
@ -57,11 +57,11 @@ The returned `Date` is set to midnight UTC of the equivalent Gregorian day.
|
|||
|
||||
**Parameters:**
|
||||
|
||||
| Name | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `hy` | `number` | Hijri year |
|
||||
| `hm` | `number` | Hijri month (1-12) |
|
||||
| `hd` | `number` | Hijri day (1-30) |
|
||||
| Name | Type | Description |
|
||||
| --------- | ------------------- | ----------------------------------- |
|
||||
| `hy` | `number` | Hijri year |
|
||||
| `hm` | `number` | Hijri month (1-12) |
|
||||
| `hd` | `number` | Hijri day (1-30) |
|
||||
| `options` | `ConversionOptions` | Optional. Calendar system selection |
|
||||
|
||||
**Returns:** `Date`
|
||||
|
|
@ -87,7 +87,7 @@ fromHijriDate(1444, 13, 1);
|
|||
### `isValidHijriDate`
|
||||
|
||||
```typescript
|
||||
function isValidHijriDate(hy: number, hm: number, hd: number, options?: ConversionOptions): boolean
|
||||
function isValidHijriDate(hy: number, hm: number, hd: number, options?: ConversionOptions): boolean;
|
||||
```
|
||||
|
||||
Check whether a Hijri date is valid. Verifies year, month (1-12), and day (1-daysInMonth) all exist in the calendar's data table.
|
||||
|
|
@ -95,9 +95,9 @@ Check whether a Hijri date is valid. Verifies year, month (1-12), and day (1-day
|
|||
**Example:**
|
||||
|
||||
```typescript
|
||||
isValidHijriDate(1444, 9, 1); // true
|
||||
isValidHijriDate(1444, 13, 1); // false - no month 13
|
||||
isValidHijriDate(1444, 9, 31); // false - Ramadan has 29 or 30 days
|
||||
isValidHijriDate(1444, 9, 1); // true
|
||||
isValidHijriDate(1444, 13, 1); // false - no month 13
|
||||
isValidHijriDate(1444, 9, 31); // false - Ramadan has 29 or 30 days
|
||||
```
|
||||
|
||||
---
|
||||
|
|
@ -107,7 +107,7 @@ isValidHijriDate(1444, 9, 31); // false - Ramadan has 29 or 30 days
|
|||
### `getHijriYear`
|
||||
|
||||
```typescript
|
||||
function getHijriYear(date: Date, options?: ConversionOptions): number | null
|
||||
function getHijriYear(date: Date, options?: ConversionOptions): number | null;
|
||||
```
|
||||
|
||||
Get the Hijri year for a Gregorian date. Returns `null` when out of range.
|
||||
|
|
@ -117,7 +117,7 @@ Get the Hijri year for a Gregorian date. Returns `null` when out of range.
|
|||
### `getHijriMonth`
|
||||
|
||||
```typescript
|
||||
function getHijriMonth(date: Date, options?: ConversionOptions): number | null
|
||||
function getHijriMonth(date: Date, options?: ConversionOptions): number | null;
|
||||
```
|
||||
|
||||
Get the Hijri month (1-12) for a Gregorian date. Returns `null` when out of range.
|
||||
|
|
@ -127,7 +127,7 @@ Get the Hijri month (1-12) for a Gregorian date. Returns `null` when out of rang
|
|||
### `getHijriDay`
|
||||
|
||||
```typescript
|
||||
function getHijriDay(date: Date, options?: ConversionOptions): number | null
|
||||
function getHijriDay(date: Date, options?: ConversionOptions): number | null;
|
||||
```
|
||||
|
||||
Get the Hijri day of month for a Gregorian date. Returns `null` when out of range.
|
||||
|
|
@ -137,7 +137,7 @@ Get the Hijri day of month for a Gregorian date. Returns `null` when out of rang
|
|||
### `getDaysInHijriMonth`
|
||||
|
||||
```typescript
|
||||
function getDaysInHijriMonth(hy: number, hm: number, options?: ConversionOptions): number
|
||||
function getDaysInHijriMonth(hy: number, hm: number, options?: ConversionOptions): number;
|
||||
```
|
||||
|
||||
Get the number of days in a Hijri month. Returns 29 or 30.
|
||||
|
|
@ -156,7 +156,7 @@ getDaysInHijriMonth(1444, 1); // 30 (Muharram 1444)
|
|||
### `getHijriQuarter`
|
||||
|
||||
```typescript
|
||||
function getHijriQuarter(date: Date, options?: ConversionOptions): number | null
|
||||
function getHijriQuarter(date: Date, options?: ConversionOptions): number | null;
|
||||
```
|
||||
|
||||
Get the Hijri quarter (1-4) for a date. Months 1-3 = Q1, 4-6 = Q2, 7-9 = Q3, 10-12 = Q4.
|
||||
|
|
@ -176,58 +176,58 @@ getHijriQuarter(new Date(2023, 2, 23, 12)); // 3 (Ramadan = month 9 = Q3)
|
|||
### `getHijriMonthName`
|
||||
|
||||
```typescript
|
||||
function getHijriMonthName(hm: number, length?: 'long' | 'medium' | 'short'): string
|
||||
function getHijriMonthName(hm: number, length?: 'long' | 'medium' | 'short'): string;
|
||||
```
|
||||
|
||||
Get the English name of a Hijri month.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `hm` | `number` | | Month number (1-12) |
|
||||
| `length` | `'long' \| 'medium' \| 'short'` | `'long'` | Name length |
|
||||
| Name | Type | Default | Description |
|
||||
| -------- | ------------------------------- | -------- | ------------------- |
|
||||
| `hm` | `number` | | Month number (1-12) |
|
||||
| `length` | `'long' \| 'medium' \| 'short'` | `'long'` | Name length |
|
||||
|
||||
**Throws:** `RangeError` if `hm` is not in [1, 12].
|
||||
|
||||
**Month names by length:**
|
||||
|
||||
| Month | Long | Medium | Short |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | Muharram | Muharram | Muh |
|
||||
| 2 | Safar | Safar | Saf |
|
||||
| 3 | Rabi'l Awwal | Rabi1 | Ra1 |
|
||||
| 4 | Rabi'l Thani | Rabi2 | Ra2 |
|
||||
| 5 | Jumadal Awwal | Jumada1 | Ju1 |
|
||||
| 6 | Jumadal Thani | Jumada2 | Ju2 |
|
||||
| 7 | Rajab | Rajab | Raj |
|
||||
| 8 | Sha'ban | Shaban | Shb |
|
||||
| 9 | Ramadan | Ramadan | Ram |
|
||||
| 10 | Shawwal | Shawwal | Shw |
|
||||
| 11 | Dhul Qi'dah | Dhul-Qidah | DhQ |
|
||||
| 12 | Dhul Hijjah | Dhul-Hijjah | DhH |
|
||||
| Month | Long | Medium | Short |
|
||||
| ----- | ------------- | ----------- | ----- |
|
||||
| 1 | Muharram | Muharram | Muh |
|
||||
| 2 | Safar | Safar | Saf |
|
||||
| 3 | Rabi'l Awwal | Rabi1 | Ra1 |
|
||||
| 4 | Rabi'l Thani | Rabi2 | Ra2 |
|
||||
| 5 | Jumadal Awwal | Jumada1 | Ju1 |
|
||||
| 6 | Jumadal Thani | Jumada2 | Ju2 |
|
||||
| 7 | Rajab | Rajab | Raj |
|
||||
| 8 | Sha'ban | Shaban | Shb |
|
||||
| 9 | Ramadan | Ramadan | Ram |
|
||||
| 10 | Shawwal | Shawwal | Shw |
|
||||
| 11 | Dhul Qi'dah | Dhul-Qidah | DhQ |
|
||||
| 12 | Dhul Hijjah | Dhul-Hijjah | DhH |
|
||||
|
||||
---
|
||||
|
||||
### `getHijriWeekdayName`
|
||||
|
||||
```typescript
|
||||
function getHijriWeekdayName(date: Date, length?: 'long' | 'short'): string
|
||||
function getHijriWeekdayName(date: Date, length?: 'long' | 'short'): string;
|
||||
```
|
||||
|
||||
Get the Arabic weekday name for a Gregorian date. Uses `Date.getDay()` (0 = Sunday through 6 = Saturday) as the index.
|
||||
|
||||
**Weekday names:**
|
||||
|
||||
| JS getDay() | Day | Long | Short |
|
||||
| --- | --- | --- | --- |
|
||||
| 0 | Sunday | Yawm al-Ahad | Ahad |
|
||||
| 1 | Monday | Yawm al-Ithnayn | Ithn |
|
||||
| 2 | Tuesday | Yawm ath-Thulatha' | Thul |
|
||||
| 3 | Wednesday | Yawm al-Arba`a' | Arba |
|
||||
| 4 | Thursday | Yawm al-Khamis | Kham |
|
||||
| 5 | Friday | Yawm al-Jum`a | Jum`a |
|
||||
| 6 | Saturday | Yawm as-Sabt | Sabt |
|
||||
| JS getDay() | Day | Long | Short |
|
||||
| ----------- | --------- | ------------------ | ----- |
|
||||
| 0 | Sunday | Yawm al-Ahad | Ahad |
|
||||
| 1 | Monday | Yawm al-Ithnayn | Ithn |
|
||||
| 2 | Tuesday | Yawm ath-Thulatha' | Thul |
|
||||
| 3 | Wednesday | Yawm al-Arba`a' | Arba |
|
||||
| 4 | Thursday | Yawm al-Khamis | Kham |
|
||||
| 5 | Friday | Yawm al-Jum`a | Jum`a |
|
||||
| 6 | Saturday | Yawm as-Sabt | Sabt |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -236,7 +236,7 @@ Get the Arabic weekday name for a Gregorian date. Uses `Date.getDay()` (0 = Sund
|
|||
### `formatHijriDate`
|
||||
|
||||
```typescript
|
||||
function formatHijriDate(date: Date, formatStr: string, options?: ConversionOptions): string
|
||||
function formatHijriDate(date: Date, formatStr: string, options?: ConversionOptions): string;
|
||||
```
|
||||
|
||||
Format a Gregorian date using Hijri calendar tokens.
|
||||
|
|
@ -245,21 +245,21 @@ Returns an empty string when the date falls outside the supported range. Non-tok
|
|||
|
||||
**Format tokens:**
|
||||
|
||||
| Token | Output | Example |
|
||||
| --- | --- | --- |
|
||||
| `iYYYY` | 4-digit Hijri year | `1444` |
|
||||
| `iYY` | 2-digit Hijri year | `44` |
|
||||
| `iMMMM` | Long month name | `Ramadan` |
|
||||
| `iMMM` | Medium month name | `Ramadan` |
|
||||
| `iMM` | Zero-padded month (01-12) | `09` |
|
||||
| `iM` | Month (1-12) | `9` |
|
||||
| `iDD` | Zero-padded day (01-30) | `01` |
|
||||
| `iD` | Day (1-30) | `1` |
|
||||
| `iEEEE` | Long weekday name | `Yawm al-Khamis` |
|
||||
| `iEEE` | Short weekday name | `Kham` |
|
||||
| `iE` | Numeric weekday (1=Sun, 7=Sat) | `5` |
|
||||
| `ioooo` | Long era | `AH` |
|
||||
| `iooo` | Short era | `AH` |
|
||||
| Token | Output | Example |
|
||||
| ------- | ------------------------------ | ---------------- |
|
||||
| `iYYYY` | 4-digit Hijri year | `1444` |
|
||||
| `iYY` | 2-digit Hijri year | `44` |
|
||||
| `iMMMM` | Long month name | `Ramadan` |
|
||||
| `iMMM` | Medium month name | `Ramadan` |
|
||||
| `iMM` | Zero-padded month (01-12) | `09` |
|
||||
| `iM` | Month (1-12) | `9` |
|
||||
| `iDD` | Zero-padded day (01-30) | `01` |
|
||||
| `iD` | Day (1-30) | `1` |
|
||||
| `iEEEE` | Long weekday name | `Yawm al-Khamis` |
|
||||
| `iEEE` | Short weekday name | `Kham` |
|
||||
| `iE` | Numeric weekday (1=Sun, 7=Sat) | `5` |
|
||||
| `ioooo` | Long era | `AH` |
|
||||
| `iooo` | Short era | `AH` |
|
||||
|
||||
**Examples:**
|
||||
|
||||
|
|
@ -281,7 +281,7 @@ formatHijriDate(new Date(2023, 2, 23), 'iEEEE');
|
|||
### `addHijriMonths`
|
||||
|
||||
```typescript
|
||||
function addHijriMonths(date: Date, months: number, options?: ConversionOptions): Date
|
||||
function addHijriMonths(date: Date, months: number, options?: ConversionOptions): Date;
|
||||
```
|
||||
|
||||
Add a number of Hijri months to a Gregorian date.
|
||||
|
|
@ -305,7 +305,7 @@ addHijriMonths(new Date(2023, 6, 18, 12), 1);
|
|||
### `addHijriYears`
|
||||
|
||||
```typescript
|
||||
function addHijriYears(date: Date, years: number, options?: ConversionOptions): Date
|
||||
function addHijriYears(date: Date, years: number, options?: ConversionOptions): Date;
|
||||
```
|
||||
|
||||
Add a number of Hijri years to a Gregorian date.
|
||||
|
|
@ -321,7 +321,7 @@ If the resulting year has fewer days in the same month (e.g., day 30 in a 29-day
|
|||
### `startOfHijriMonth`
|
||||
|
||||
```typescript
|
||||
function startOfHijriMonth(date: Date, options?: ConversionOptions): Date
|
||||
function startOfHijriMonth(date: Date, options?: ConversionOptions): Date;
|
||||
```
|
||||
|
||||
Get the first day of the Hijri month that contains the given date.
|
||||
|
|
@ -333,7 +333,7 @@ Get the first day of the Hijri month that contains the given date.
|
|||
### `endOfHijriMonth`
|
||||
|
||||
```typescript
|
||||
function endOfHijriMonth(date: Date, options?: ConversionOptions): Date
|
||||
function endOfHijriMonth(date: Date, options?: ConversionOptions): Date;
|
||||
```
|
||||
|
||||
Get the last day of the Hijri month that contains the given date.
|
||||
|
|
@ -358,7 +358,7 @@ const end = endOfHijriMonth(new Date(2023, 3, 1, 12));
|
|||
### `isSameHijriMonth`
|
||||
|
||||
```typescript
|
||||
function isSameHijriMonth(dateA: Date, dateB: Date, options?: ConversionOptions): boolean
|
||||
function isSameHijriMonth(dateA: Date, dateB: Date, options?: ConversionOptions): boolean;
|
||||
```
|
||||
|
||||
Check whether two Gregorian dates fall in the same Hijri month.
|
||||
|
|
@ -370,7 +370,7 @@ Returns `false` if either date is outside the supported range.
|
|||
### `isSameHijriYear`
|
||||
|
||||
```typescript
|
||||
function isSameHijriYear(dateA: Date, dateB: Date, options?: ConversionOptions): boolean
|
||||
function isSameHijriYear(dateA: Date, dateB: Date, options?: ConversionOptions): boolean;
|
||||
```
|
||||
|
||||
Check whether two Gregorian dates fall in the same Hijri year.
|
||||
|
|
@ -90,7 +90,7 @@ Month arithmetic works by converting to a total month offset from the Hijri epoc
|
|||
|
||||
```typescript
|
||||
const totalMonths = (h.hy - 1) * 12 + (h.hm - 1) + months;
|
||||
const newYear = Math.floor(totalMonths / 12) + 1;
|
||||
const newYear = Math.floor(totalMonths / 12) + 1;
|
||||
const newMonth = (((totalMonths % 12) + 12) % 12) + 1;
|
||||
```
|
||||
|
||||
|
|
@ -109,12 +109,12 @@ This handles the case where month lengths differ (29 vs 30 days) without requiri
|
|||
|
||||
`tsup` produces four outputs from a single source:
|
||||
|
||||
| File | Format | Purpose |
|
||||
| --- | --- | --- |
|
||||
| `dist/index.cjs` | CommonJS | Node.js `require()` |
|
||||
| `dist/index.mjs` | ESM | `import` / bundlers |
|
||||
| `dist/index.d.ts` | TypeScript declarations | CJS consumers |
|
||||
| `dist/index.d.mts` | TypeScript declarations | ESM consumers |
|
||||
| File | Format | Purpose |
|
||||
| ------------------ | ----------------------- | ------------------- |
|
||||
| `dist/index.cjs` | CommonJS | Node.js `require()` |
|
||||
| `dist/index.mjs` | ESM | `import` / bundlers |
|
||||
| `dist/index.d.ts` | TypeScript declarations | CJS consumers |
|
||||
| `dist/index.d.mts` | TypeScript declarations | ESM consumers |
|
||||
|
||||
`hijri-core` is marked as external so it resolves from the consumer's `node_modules` rather than being bundled. This is required for the peer dependency pattern to work correctly.
|
||||
|
||||
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 date-fns-hijri
|
||||
|
||||
Thanks for your interest in contributing. This is a small, focused library and contributions are welcome.
|
||||
|
||||
## Getting started
|
||||
|
||||
```bash
|
||||
git clone https://github.com/acamarata/date-fns-hijri.git
|
||||
cd date-fns-hijri
|
||||
pnpm install
|
||||
pnpm build
|
||||
pnpm test
|
||||
```
|
||||
|
||||
All tests should pass before you start.
|
||||
|
||||
## What to work on
|
||||
|
||||
Check the [open issues](https://github.com/acamarata/date-fns-hijri/issues) for anything tagged `help wanted` or `good first issue`. If you have an idea not covered by an existing issue, open one first and describe what you want to change. That avoids duplicate work.
|
||||
|
||||
## Code style
|
||||
|
||||
- TypeScript strict mode. No `any` without a comment explaining why.
|
||||
- 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
|
||||
|
||||
date-fns-hijri is a pure calendar computation library. It accepts plain JavaScript `Date` objects 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: date-fns-hijri".
|
||||
3. Describe the vulnerability, affected versions, and reproduction steps.
|
||||
4. You will receive a response within 7 days.
|
||||
|
||||
## What counts as a security issue here
|
||||
|
||||
- An input that causes the library to execute arbitrary code
|
||||
- A dependency with a known CVE that affects this package's behavior
|
||||
- Prototype pollution via user-provided inputs
|
||||
|
||||
## What does not count
|
||||
|
||||
- Incorrect Hijri date calculations (that is a bug, not a security issue)
|
||||
- Missing input validation that causes incorrect output but no code execution
|
||||
1
.github/wiki/_Footer.md
vendored
Normal file
1
.github/wiki/_Footer.md
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[date-fns-hijri](https://github.com/acamarata/date-fns-hijri) · MIT License · [npm](https://www.npmjs.com/package/date-fns-hijri) · [Issues](https://github.com/acamarata/date-fns-hijri/issues)
|
||||
38
.github/wiki/_Sidebar.md
vendored
Normal file
38
.github/wiki/_Sidebar.md
vendored
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
**[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 — Per Function**
|
||||
- [toHijriDate](api/toHijriDate)
|
||||
- [fromHijriDate](api/fromHijriDate)
|
||||
- [isValidHijriDate](api/isValidHijriDate)
|
||||
- [getHijriYear](api/getHijriYear)
|
||||
- [getHijriMonth](api/getHijriMonth)
|
||||
- [getHijriDay](api/getHijriDay)
|
||||
- [getDaysInHijriMonth](api/getDaysInHijriMonth)
|
||||
- [getHijriQuarter](api/getHijriQuarter)
|
||||
- [getHijriMonthName](api/getHijriMonthName)
|
||||
- [getHijriWeekdayName](api/getHijriWeekdayName)
|
||||
- [formatHijriDate](api/formatHijriDate)
|
||||
- [addHijriMonths](api/addHijriMonths)
|
||||
- [addHijriYears](api/addHijriYears)
|
||||
- [startOfHijriMonth](api/startOfHijriMonth)
|
||||
- [endOfHijriMonth](api/endOfHijriMonth)
|
||||
- [isSameHijriMonth](api/isSameHijriMonth)
|
||||
- [isSameHijriYear](api/isSameHijriYear)
|
||||
|
||||
**Community**
|
||||
- [Contributing](CONTRIBUTING)
|
||||
- [Code of Conduct](CODE_OF_CONDUCT)
|
||||
- [Security](SECURITY)
|
||||
31
.github/wiki/api/README.md
vendored
Normal file
31
.github/wiki/api/README.md
vendored
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
**date-fns-hijri v1.0.1**
|
||||
|
||||
***
|
||||
|
||||
# date-fns-hijri v1.0.1
|
||||
|
||||
## Interfaces
|
||||
|
||||
- [CalendarEngine](interfaces/CalendarEngine.md)
|
||||
- [ConversionOptions](interfaces/ConversionOptions.md)
|
||||
- [HijriDate](interfaces/HijriDate.md)
|
||||
|
||||
## Functions
|
||||
|
||||
- [addHijriMonths](functions/addHijriMonths.md)
|
||||
- [addHijriYears](functions/addHijriYears.md)
|
||||
- [endOfHijriMonth](functions/endOfHijriMonth.md)
|
||||
- [formatHijriDate](functions/formatHijriDate.md)
|
||||
- [fromHijriDate](functions/fromHijriDate.md)
|
||||
- [getDaysInHijriMonth](functions/getDaysInHijriMonth.md)
|
||||
- [getHijriDay](functions/getHijriDay.md)
|
||||
- [getHijriMonth](functions/getHijriMonth.md)
|
||||
- [getHijriMonthName](functions/getHijriMonthName.md)
|
||||
- [getHijriQuarter](functions/getHijriQuarter.md)
|
||||
- [getHijriWeekdayName](functions/getHijriWeekdayName.md)
|
||||
- [getHijriYear](functions/getHijriYear.md)
|
||||
- [isSameHijriMonth](functions/isSameHijriMonth.md)
|
||||
- [isSameHijriYear](functions/isSameHijriYear.md)
|
||||
- [isValidHijriDate](functions/isValidHijriDate.md)
|
||||
- [startOfHijriMonth](functions/startOfHijriMonth.md)
|
||||
- [toHijriDate](functions/toHijriDate.md)
|
||||
39
.github/wiki/api/functions/addHijriMonths.md
vendored
Normal file
39
.github/wiki/api/functions/addHijriMonths.md
vendored
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
[**date-fns-hijri v1.0.1**](../README.md)
|
||||
|
||||
***
|
||||
|
||||
[date-fns-hijri](../README.md) / addHijriMonths
|
||||
|
||||
# Function: addHijriMonths()
|
||||
|
||||
> **addHijriMonths**(`date`, `months`, `options?`): `Date`
|
||||
|
||||
Defined in: [src/index.ts:267](https://github.com/acamarata/date-fns-hijri/blob/e8ade1f9c489d2317f2f324f2a54bebcb30e4d6f/src/index.ts#L267)
|
||||
|
||||
Add a number of Hijri months to a Gregorian date.
|
||||
|
||||
Handles year rollover automatically. Month addition wraps at month 12 and
|
||||
increments the year. If the result's month has fewer days than the original
|
||||
day, the day is clamped to the last day of the new month.
|
||||
|
||||
## Parameters
|
||||
|
||||
### date
|
||||
|
||||
`Date`
|
||||
|
||||
### months
|
||||
|
||||
`number`
|
||||
|
||||
### options?
|
||||
|
||||
[`ConversionOptions`](../interfaces/ConversionOptions.md)
|
||||
|
||||
## Returns
|
||||
|
||||
`Date`
|
||||
|
||||
## Throws
|
||||
|
||||
If the resulting Hijri date is outside the supported range.
|
||||
38
.github/wiki/api/functions/addHijriYears.md
vendored
Normal file
38
.github/wiki/api/functions/addHijriYears.md
vendored
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
[**date-fns-hijri v1.0.1**](../README.md)
|
||||
|
||||
***
|
||||
|
||||
[date-fns-hijri](../README.md) / addHijriYears
|
||||
|
||||
# Function: addHijriYears()
|
||||
|
||||
> **addHijriYears**(`date`, `years`, `options?`): `Date`
|
||||
|
||||
Defined in: [src/index.ts:293](https://github.com/acamarata/date-fns-hijri/blob/e8ade1f9c489d2317f2f324f2a54bebcb30e4d6f/src/index.ts#L293)
|
||||
|
||||
Add a number of Hijri years to a Gregorian date.
|
||||
|
||||
If the resulting year has a shorter Ramadan (or any month) than the original
|
||||
day, the day is clamped to the last day of that month.
|
||||
|
||||
## Parameters
|
||||
|
||||
### date
|
||||
|
||||
`Date`
|
||||
|
||||
### years
|
||||
|
||||
`number`
|
||||
|
||||
### options?
|
||||
|
||||
[`ConversionOptions`](../interfaces/ConversionOptions.md)
|
||||
|
||||
## Returns
|
||||
|
||||
`Date`
|
||||
|
||||
## Throws
|
||||
|
||||
If the resulting Hijri date is outside the supported range.
|
||||
31
.github/wiki/api/functions/endOfHijriMonth.md
vendored
Normal file
31
.github/wiki/api/functions/endOfHijriMonth.md
vendored
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
[**date-fns-hijri v1.0.1**](../README.md)
|
||||
|
||||
***
|
||||
|
||||
[date-fns-hijri](../README.md) / endOfHijriMonth
|
||||
|
||||
# Function: endOfHijriMonth()
|
||||
|
||||
> **endOfHijriMonth**(`date`, `options?`): `Date`
|
||||
|
||||
Defined in: [src/index.ts:328](https://github.com/acamarata/date-fns-hijri/blob/e8ade1f9c489d2317f2f324f2a54bebcb30e4d6f/src/index.ts#L328)
|
||||
|
||||
Get the last day of the Hijri month that contains the given date.
|
||||
|
||||
## Parameters
|
||||
|
||||
### date
|
||||
|
||||
`Date`
|
||||
|
||||
### options?
|
||||
|
||||
[`ConversionOptions`](../interfaces/ConversionOptions.md)
|
||||
|
||||
## Returns
|
||||
|
||||
`Date`
|
||||
|
||||
## Throws
|
||||
|
||||
If the date is outside the supported range.
|
||||
51
.github/wiki/api/functions/formatHijriDate.md
vendored
Normal file
51
.github/wiki/api/functions/formatHijriDate.md
vendored
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
[**date-fns-hijri v1.0.1**](../README.md)
|
||||
|
||||
***
|
||||
|
||||
[date-fns-hijri](../README.md) / formatHijriDate
|
||||
|
||||
# Function: formatHijriDate()
|
||||
|
||||
> **formatHijriDate**(`date`, `formatStr`, `options?`): `string`
|
||||
|
||||
Defined in: [src/index.ts:185](https://github.com/acamarata/date-fns-hijri/blob/e8ade1f9c489d2317f2f324f2a54bebcb30e4d6f/src/index.ts#L185)
|
||||
|
||||
Format a Gregorian date using Hijri calendar tokens.
|
||||
|
||||
Supported tokens:
|
||||
|
||||
| Token | Output | Example |
|
||||
| ------- | -------------------------- | -------------- |
|
||||
| iYYYY | 4-digit Hijri year | 1444 |
|
||||
| iYY | 2-digit Hijri year | 44 |
|
||||
| iMMMM | Long month name | Ramadan |
|
||||
| iMMM | Medium month name | Ramadan |
|
||||
| iMM | Zero-padded month (01–12) | 09 |
|
||||
| iM | Month (1–12) | 9 |
|
||||
| iDD | Zero-padded day (01–30) | 01 |
|
||||
| iD | Day (1–30) | 1 |
|
||||
| iEEEE | Long weekday name | Yawm al-Khamis |
|
||||
| iEEE | Short weekday name | Kham |
|
||||
| iE | Numeric weekday (1=Sun–7=Sat)| 5 |
|
||||
| ioooo | Long era | AH |
|
||||
| iooo | Short era | AH |
|
||||
|
||||
Returns an empty string when the date falls outside the supported range.
|
||||
|
||||
## Parameters
|
||||
|
||||
### date
|
||||
|
||||
`Date`
|
||||
|
||||
### formatStr
|
||||
|
||||
`string`
|
||||
|
||||
### options?
|
||||
|
||||
[`ConversionOptions`](../interfaces/ConversionOptions.md)
|
||||
|
||||
## Returns
|
||||
|
||||
`string`
|
||||
41
.github/wiki/api/functions/fromHijriDate.md
vendored
Normal file
41
.github/wiki/api/functions/fromHijriDate.md
vendored
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
[**date-fns-hijri v1.0.1**](../README.md)
|
||||
|
||||
***
|
||||
|
||||
[date-fns-hijri](../README.md) / fromHijriDate
|
||||
|
||||
# Function: fromHijriDate()
|
||||
|
||||
> **fromHijriDate**(`hy`, `hm`, `hd`, `options?`): `Date`
|
||||
|
||||
Defined in: [src/index.ts:39](https://github.com/acamarata/date-fns-hijri/blob/e8ade1f9c489d2317f2f324f2a54bebcb30e4d6f/src/index.ts#L39)
|
||||
|
||||
Convert a Hijri date to a Gregorian `Date`.
|
||||
|
||||
The returned `Date` is set to midnight UTC of the equivalent Gregorian day.
|
||||
|
||||
## Parameters
|
||||
|
||||
### hy
|
||||
|
||||
`number`
|
||||
|
||||
### hm
|
||||
|
||||
`number`
|
||||
|
||||
### hd
|
||||
|
||||
`number`
|
||||
|
||||
### options?
|
||||
|
||||
[`ConversionOptions`](../interfaces/ConversionOptions.md)
|
||||
|
||||
## Returns
|
||||
|
||||
`Date`
|
||||
|
||||
## Throws
|
||||
|
||||
If the Hijri date is invalid or outside the calendar's range.
|
||||
35
.github/wiki/api/functions/getDaysInHijriMonth.md
vendored
Normal file
35
.github/wiki/api/functions/getDaysInHijriMonth.md
vendored
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
[**date-fns-hijri v1.0.1**](../README.md)
|
||||
|
||||
***
|
||||
|
||||
[date-fns-hijri](../README.md) / getDaysInHijriMonth
|
||||
|
||||
# Function: getDaysInHijriMonth()
|
||||
|
||||
> **getDaysInHijriMonth**(`hy`, `hm`, `options?`): `number`
|
||||
|
||||
Defined in: [src/index.ts:107](https://github.com/acamarata/date-fns-hijri/blob/e8ade1f9c489d2317f2f324f2a54bebcb30e4d6f/src/index.ts#L107)
|
||||
|
||||
Get the number of days in a Hijri month (29 or 30).
|
||||
|
||||
## Parameters
|
||||
|
||||
### hy
|
||||
|
||||
`number`
|
||||
|
||||
### hm
|
||||
|
||||
`number`
|
||||
|
||||
### options?
|
||||
|
||||
[`ConversionOptions`](../interfaces/ConversionOptions.md)
|
||||
|
||||
## Returns
|
||||
|
||||
`number`
|
||||
|
||||
## Throws
|
||||
|
||||
If the year is outside the calendar's supported range.
|
||||
29
.github/wiki/api/functions/getHijriDay.md
vendored
Normal file
29
.github/wiki/api/functions/getHijriDay.md
vendored
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
[**date-fns-hijri v1.0.1**](../README.md)
|
||||
|
||||
***
|
||||
|
||||
[date-fns-hijri](../README.md) / getHijriDay
|
||||
|
||||
# Function: getHijriDay()
|
||||
|
||||
> **getHijriDay**(`date`, `options?`): `number` \| `null`
|
||||
|
||||
Defined in: [src/index.ts:98](https://github.com/acamarata/date-fns-hijri/blob/e8ade1f9c489d2317f2f324f2a54bebcb30e4d6f/src/index.ts#L98)
|
||||
|
||||
Get the Hijri day of month (1–30) for a Gregorian date.
|
||||
|
||||
Returns `null` when the date is outside the supported range.
|
||||
|
||||
## Parameters
|
||||
|
||||
### date
|
||||
|
||||
`Date`
|
||||
|
||||
### options?
|
||||
|
||||
[`ConversionOptions`](../interfaces/ConversionOptions.md)
|
||||
|
||||
## Returns
|
||||
|
||||
`number` \| `null`
|
||||
29
.github/wiki/api/functions/getHijriMonth.md
vendored
Normal file
29
.github/wiki/api/functions/getHijriMonth.md
vendored
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
[**date-fns-hijri v1.0.1**](../README.md)
|
||||
|
||||
***
|
||||
|
||||
[date-fns-hijri](../README.md) / getHijriMonth
|
||||
|
||||
# Function: getHijriMonth()
|
||||
|
||||
> **getHijriMonth**(`date`, `options?`): `number` \| `null`
|
||||
|
||||
Defined in: [src/index.ts:89](https://github.com/acamarata/date-fns-hijri/blob/e8ade1f9c489d2317f2f324f2a54bebcb30e4d6f/src/index.ts#L89)
|
||||
|
||||
Get the Hijri month (1–12) for a Gregorian date.
|
||||
|
||||
Returns `null` when the date is outside the supported range.
|
||||
|
||||
## Parameters
|
||||
|
||||
### date
|
||||
|
||||
`Date`
|
||||
|
||||
### options?
|
||||
|
||||
[`ConversionOptions`](../interfaces/ConversionOptions.md)
|
||||
|
||||
## Returns
|
||||
|
||||
`number` \| `null`
|
||||
35
.github/wiki/api/functions/getHijriMonthName.md
vendored
Normal file
35
.github/wiki/api/functions/getHijriMonthName.md
vendored
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
[**date-fns-hijri v1.0.1**](../README.md)
|
||||
|
||||
***
|
||||
|
||||
[date-fns-hijri](../README.md) / getHijriMonthName
|
||||
|
||||
# Function: getHijriMonthName()
|
||||
|
||||
> **getHijriMonthName**(`hm`, `length?`): `string`
|
||||
|
||||
Defined in: [src/index.ts:123](https://github.com/acamarata/date-fns-hijri/blob/e8ade1f9c489d2317f2f324f2a54bebcb30e4d6f/src/index.ts#L123)
|
||||
|
||||
Get the English name of a Hijri month.
|
||||
|
||||
## Parameters
|
||||
|
||||
### hm
|
||||
|
||||
`number`
|
||||
|
||||
Month number (1–12).
|
||||
|
||||
### length?
|
||||
|
||||
`"long"` \| `"medium"` \| `"short"`
|
||||
|
||||
`'long'` (default), `'medium'`, or `'short'`.
|
||||
|
||||
## Returns
|
||||
|
||||
`string`
|
||||
|
||||
## Throws
|
||||
|
||||
If `hm` is not in [1, 12].
|
||||
31
.github/wiki/api/functions/getHijriQuarter.md
vendored
Normal file
31
.github/wiki/api/functions/getHijriQuarter.md
vendored
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
[**date-fns-hijri v1.0.1**](../README.md)
|
||||
|
||||
***
|
||||
|
||||
[date-fns-hijri](../README.md) / getHijriQuarter
|
||||
|
||||
# Function: getHijriQuarter()
|
||||
|
||||
> **getHijriQuarter**(`date`, `options?`): `number` \| `null`
|
||||
|
||||
Defined in: [src/index.ts:376](https://github.com/acamarata/date-fns-hijri/blob/e8ade1f9c489d2317f2f324f2a54bebcb30e4d6f/src/index.ts#L376)
|
||||
|
||||
Get the Hijri quarter (1–4) for a Gregorian date.
|
||||
|
||||
Months 1–3 = Q1, 4–6 = Q2, 7–9 = Q3, 10–12 = Q4.
|
||||
|
||||
Returns `null` when the date is outside the supported range.
|
||||
|
||||
## Parameters
|
||||
|
||||
### date
|
||||
|
||||
`Date`
|
||||
|
||||
### options?
|
||||
|
||||
[`ConversionOptions`](../interfaces/ConversionOptions.md)
|
||||
|
||||
## Returns
|
||||
|
||||
`number` \| `null`
|
||||
33
.github/wiki/api/functions/getHijriWeekdayName.md
vendored
Normal file
33
.github/wiki/api/functions/getHijriWeekdayName.md
vendored
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
[**date-fns-hijri v1.0.1**](../README.md)
|
||||
|
||||
***
|
||||
|
||||
[date-fns-hijri](../README.md) / getHijriWeekdayName
|
||||
|
||||
# Function: getHijriWeekdayName()
|
||||
|
||||
> **getHijriWeekdayName**(`date`, `length?`): `string`
|
||||
|
||||
Defined in: [src/index.ts:148](https://github.com/acamarata/date-fns-hijri/blob/e8ade1f9c489d2317f2f324f2a54bebcb30e4d6f/src/index.ts#L148)
|
||||
|
||||
Get the Arabic weekday name for a Gregorian date.
|
||||
|
||||
Uses `Date.getDay()` (0 = Sunday, 6 = Saturday) as the index.
|
||||
|
||||
## Parameters
|
||||
|
||||
### date
|
||||
|
||||
`Date`
|
||||
|
||||
Any Gregorian `Date`.
|
||||
|
||||
### length?
|
||||
|
||||
`"long"` \| `"short"`
|
||||
|
||||
`'long'` (default) or `'short'`.
|
||||
|
||||
## Returns
|
||||
|
||||
`string`
|
||||
29
.github/wiki/api/functions/getHijriYear.md
vendored
Normal file
29
.github/wiki/api/functions/getHijriYear.md
vendored
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
[**date-fns-hijri v1.0.1**](../README.md)
|
||||
|
||||
***
|
||||
|
||||
[date-fns-hijri](../README.md) / getHijriYear
|
||||
|
||||
# Function: getHijriYear()
|
||||
|
||||
> **getHijriYear**(`date`, `options?`): `number` \| `null`
|
||||
|
||||
Defined in: [src/index.ts:80](https://github.com/acamarata/date-fns-hijri/blob/e8ade1f9c489d2317f2f324f2a54bebcb30e4d6f/src/index.ts#L80)
|
||||
|
||||
Get the Hijri year for a Gregorian date.
|
||||
|
||||
Returns `null` when the date is outside the supported range.
|
||||
|
||||
## Parameters
|
||||
|
||||
### date
|
||||
|
||||
`Date`
|
||||
|
||||
### options?
|
||||
|
||||
[`ConversionOptions`](../interfaces/ConversionOptions.md)
|
||||
|
||||
## Returns
|
||||
|
||||
`number` \| `null`
|
||||
33
.github/wiki/api/functions/isSameHijriMonth.md
vendored
Normal file
33
.github/wiki/api/functions/isSameHijriMonth.md
vendored
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
[**date-fns-hijri v1.0.1**](../README.md)
|
||||
|
||||
***
|
||||
|
||||
[date-fns-hijri](../README.md) / isSameHijriMonth
|
||||
|
||||
# Function: isSameHijriMonth()
|
||||
|
||||
> **isSameHijriMonth**(`dateA`, `dateB`, `options?`): `boolean`
|
||||
|
||||
Defined in: [src/index.ts:346](https://github.com/acamarata/date-fns-hijri/blob/e8ade1f9c489d2317f2f324f2a54bebcb30e4d6f/src/index.ts#L346)
|
||||
|
||||
Check whether two Gregorian dates fall in the same Hijri month.
|
||||
|
||||
Returns `false` if either date is outside the supported range.
|
||||
|
||||
## Parameters
|
||||
|
||||
### dateA
|
||||
|
||||
`Date`
|
||||
|
||||
### dateB
|
||||
|
||||
`Date`
|
||||
|
||||
### options?
|
||||
|
||||
[`ConversionOptions`](../interfaces/ConversionOptions.md)
|
||||
|
||||
## Returns
|
||||
|
||||
`boolean`
|
||||
33
.github/wiki/api/functions/isSameHijriYear.md
vendored
Normal file
33
.github/wiki/api/functions/isSameHijriYear.md
vendored
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
[**date-fns-hijri v1.0.1**](../README.md)
|
||||
|
||||
***
|
||||
|
||||
[date-fns-hijri](../README.md) / isSameHijriYear
|
||||
|
||||
# Function: isSameHijriYear()
|
||||
|
||||
> **isSameHijriYear**(`dateA`, `dateB`, `options?`): `boolean`
|
||||
|
||||
Defined in: [src/index.ts:358](https://github.com/acamarata/date-fns-hijri/blob/e8ade1f9c489d2317f2f324f2a54bebcb30e4d6f/src/index.ts#L358)
|
||||
|
||||
Check whether two Gregorian dates fall in the same Hijri year.
|
||||
|
||||
Returns `false` if either date is outside the supported range.
|
||||
|
||||
## Parameters
|
||||
|
||||
### dateA
|
||||
|
||||
`Date`
|
||||
|
||||
### dateB
|
||||
|
||||
`Date`
|
||||
|
||||
### options?
|
||||
|
||||
[`ConversionOptions`](../interfaces/ConversionOptions.md)
|
||||
|
||||
## Returns
|
||||
|
||||
`boolean`
|
||||
38
.github/wiki/api/functions/isValidHijriDate.md
vendored
Normal file
38
.github/wiki/api/functions/isValidHijriDate.md
vendored
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
[**date-fns-hijri v1.0.1**](../README.md)
|
||||
|
||||
***
|
||||
|
||||
[date-fns-hijri](../README.md) / isValidHijriDate
|
||||
|
||||
# Function: isValidHijriDate()
|
||||
|
||||
> **isValidHijriDate**(`hy`, `hm`, `hd`, `options?`): `boolean`
|
||||
|
||||
Defined in: [src/index.ts:62](https://github.com/acamarata/date-fns-hijri/blob/e8ade1f9c489d2317f2f324f2a54bebcb30e4d6f/src/index.ts#L62)
|
||||
|
||||
Check whether a Hijri date is valid for the given calendar system.
|
||||
|
||||
Verifies that the year, month (1–12), and day (1–daysInMonth) all exist
|
||||
in the calendar's data table.
|
||||
|
||||
## Parameters
|
||||
|
||||
### hy
|
||||
|
||||
`number`
|
||||
|
||||
### hm
|
||||
|
||||
`number`
|
||||
|
||||
### hd
|
||||
|
||||
`number`
|
||||
|
||||
### options?
|
||||
|
||||
[`ConversionOptions`](../interfaces/ConversionOptions.md)
|
||||
|
||||
## Returns
|
||||
|
||||
`boolean`
|
||||
31
.github/wiki/api/functions/startOfHijriMonth.md
vendored
Normal file
31
.github/wiki/api/functions/startOfHijriMonth.md
vendored
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
[**date-fns-hijri v1.0.1**](../README.md)
|
||||
|
||||
***
|
||||
|
||||
[date-fns-hijri](../README.md) / startOfHijriMonth
|
||||
|
||||
# Function: startOfHijriMonth()
|
||||
|
||||
> **startOfHijriMonth**(`date`, `options?`): `Date`
|
||||
|
||||
Defined in: [src/index.ts:315](https://github.com/acamarata/date-fns-hijri/blob/e8ade1f9c489d2317f2f324f2a54bebcb30e4d6f/src/index.ts#L315)
|
||||
|
||||
Get the first day of the Hijri month that contains the given date.
|
||||
|
||||
## Parameters
|
||||
|
||||
### date
|
||||
|
||||
`Date`
|
||||
|
||||
### options?
|
||||
|
||||
[`ConversionOptions`](../interfaces/ConversionOptions.md)
|
||||
|
||||
## Returns
|
||||
|
||||
`Date`
|
||||
|
||||
## Throws
|
||||
|
||||
If the date is outside the supported range.
|
||||
30
.github/wiki/api/functions/toHijriDate.md
vendored
Normal file
30
.github/wiki/api/functions/toHijriDate.md
vendored
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
[**date-fns-hijri v1.0.1**](../README.md)
|
||||
|
||||
***
|
||||
|
||||
[date-fns-hijri](../README.md) / toHijriDate
|
||||
|
||||
# Function: toHijriDate()
|
||||
|
||||
> **toHijriDate**(`date`, `options?`): [`HijriDate`](../interfaces/HijriDate.md) \| `null`
|
||||
|
||||
Defined in: [src/index.ts:28](https://github.com/acamarata/date-fns-hijri/blob/e8ade1f9c489d2317f2f324f2a54bebcb30e4d6f/src/index.ts#L28)
|
||||
|
||||
Convert a Gregorian `Date` to a Hijri date object.
|
||||
|
||||
Returns `null` when the date falls outside the calendar's supported range
|
||||
(UAQ: 1318–1500 AH / 1900–2076 CE; FCNA extends slightly further).
|
||||
|
||||
## Parameters
|
||||
|
||||
### date
|
||||
|
||||
`Date`
|
||||
|
||||
### options?
|
||||
|
||||
[`ConversionOptions`](../interfaces/ConversionOptions.md)
|
||||
|
||||
## Returns
|
||||
|
||||
[`HijriDate`](../interfaces/HijriDate.md) \| `null`
|
||||
111
.github/wiki/api/interfaces/CalendarEngine.md
vendored
Normal file
111
.github/wiki/api/interfaces/CalendarEngine.md
vendored
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
[**date-fns-hijri v1.0.1**](../README.md)
|
||||
|
||||
***
|
||||
|
||||
[date-fns-hijri](../README.md) / CalendarEngine
|
||||
|
||||
# Interface: CalendarEngine
|
||||
|
||||
Defined in: node\_modules/.pnpm/hijri-core@1.0.0/node\_modules/hijri-core/dist/index.d.mts:13
|
||||
|
||||
## Properties
|
||||
|
||||
### id
|
||||
|
||||
> `readonly` **id**: `string`
|
||||
|
||||
Defined in: node\_modules/.pnpm/hijri-core@1.0.0/node\_modules/hijri-core/dist/index.d.mts:14
|
||||
|
||||
## Methods
|
||||
|
||||
### daysInMonth()
|
||||
|
||||
> **daysInMonth**(`hy`, `hm`): `number`
|
||||
|
||||
Defined in: node\_modules/.pnpm/hijri-core@1.0.0/node\_modules/hijri-core/dist/index.d.mts:19
|
||||
|
||||
#### Parameters
|
||||
|
||||
##### hy
|
||||
|
||||
`number`
|
||||
|
||||
##### hm
|
||||
|
||||
`number`
|
||||
|
||||
#### Returns
|
||||
|
||||
`number`
|
||||
|
||||
***
|
||||
|
||||
### isValid()
|
||||
|
||||
> **isValid**(`hy`, `hm`, `hd`): `boolean`
|
||||
|
||||
Defined in: node\_modules/.pnpm/hijri-core@1.0.0/node\_modules/hijri-core/dist/index.d.mts:18
|
||||
|
||||
#### Parameters
|
||||
|
||||
##### hy
|
||||
|
||||
`number`
|
||||
|
||||
##### hm
|
||||
|
||||
`number`
|
||||
|
||||
##### hd
|
||||
|
||||
`number`
|
||||
|
||||
#### Returns
|
||||
|
||||
`boolean`
|
||||
|
||||
***
|
||||
|
||||
### toGregorian()
|
||||
|
||||
> **toGregorian**(`hy`, `hm`, `hd`): `Date` \| `null`
|
||||
|
||||
Defined in: node\_modules/.pnpm/hijri-core@1.0.0/node\_modules/hijri-core/dist/index.d.mts:17
|
||||
|
||||
Returns null for invalid or out-of-range input. Never throws.
|
||||
|
||||
#### Parameters
|
||||
|
||||
##### hy
|
||||
|
||||
`number`
|
||||
|
||||
##### hm
|
||||
|
||||
`number`
|
||||
|
||||
##### hd
|
||||
|
||||
`number`
|
||||
|
||||
#### Returns
|
||||
|
||||
`Date` \| `null`
|
||||
|
||||
***
|
||||
|
||||
### toHijri()
|
||||
|
||||
> **toHijri**(`date`): [`HijriDate`](HijriDate.md) \| `null`
|
||||
|
||||
Defined in: node\_modules/.pnpm/hijri-core@1.0.0/node\_modules/hijri-core/dist/index.d.mts:15
|
||||
|
||||
#### Parameters
|
||||
|
||||
##### date
|
||||
|
||||
`Date`
|
||||
|
||||
#### Returns
|
||||
|
||||
[`HijriDate`](HijriDate.md) \| `null`
|
||||
17
.github/wiki/api/interfaces/ConversionOptions.md
vendored
Normal file
17
.github/wiki/api/interfaces/ConversionOptions.md
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
[**date-fns-hijri v1.0.1**](../README.md)
|
||||
|
||||
***
|
||||
|
||||
[date-fns-hijri](../README.md) / ConversionOptions
|
||||
|
||||
# Interface: ConversionOptions
|
||||
|
||||
Defined in: node\_modules/.pnpm/hijri-core@1.0.0/node\_modules/hijri-core/dist/index.d.mts:21
|
||||
|
||||
## Properties
|
||||
|
||||
### calendar?
|
||||
|
||||
> `optional` **calendar?**: `string`
|
||||
|
||||
Defined in: node\_modules/.pnpm/hijri-core@1.0.0/node\_modules/hijri-core/dist/index.d.mts:22
|
||||
33
.github/wiki/api/interfaces/HijriDate.md
vendored
Normal file
33
.github/wiki/api/interfaces/HijriDate.md
vendored
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
[**date-fns-hijri v1.0.1**](../README.md)
|
||||
|
||||
***
|
||||
|
||||
[date-fns-hijri](../README.md) / HijriDate
|
||||
|
||||
# Interface: HijriDate
|
||||
|
||||
Defined in: node\_modules/.pnpm/hijri-core@1.0.0/node\_modules/hijri-core/dist/index.d.mts:1
|
||||
|
||||
## Properties
|
||||
|
||||
### hd
|
||||
|
||||
> **hd**: `number`
|
||||
|
||||
Defined in: node\_modules/.pnpm/hijri-core@1.0.0/node\_modules/hijri-core/dist/index.d.mts:4
|
||||
|
||||
***
|
||||
|
||||
### hm
|
||||
|
||||
> **hm**: `number`
|
||||
|
||||
Defined in: node\_modules/.pnpm/hijri-core@1.0.0/node\_modules/hijri-core/dist/index.d.mts:3
|
||||
|
||||
***
|
||||
|
||||
### hy
|
||||
|
||||
> **hy**: `number`
|
||||
|
||||
Defined in: node\_modules/.pnpm/hijri-core@1.0.0/node\_modules/hijri-core/dist/index.d.mts:2
|
||||
66
.github/wiki/benchmarks/index.md
vendored
Normal file
66
.github/wiki/benchmarks/index.md
vendored
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
# Benchmarks
|
||||
|
||||
Performance measurements for date-fns-hijri on Node.js 24, Apple M-series hardware.
|
||||
|
||||
## Methodology
|
||||
|
||||
All benchmarks use `performance.now()` with 10,000 iterations per test. The first 100 iterations are discarded as warm-up. Results are median across 5 runs.
|
||||
|
||||
```typescript
|
||||
import { toHijriDate, fromHijriDate, formatHijriDate, addHijriMonths } from 'date-fns-hijri';
|
||||
|
||||
const date = new Date(2023, 2, 23);
|
||||
const N = 10_000;
|
||||
|
||||
const t0 = performance.now();
|
||||
for (let i = 0; i < N; i++) toHijriDate(date);
|
||||
const elapsed = performance.now() - t0;
|
||||
console.log(`toHijriDate: ${(elapsed / N * 1000).toFixed(1)} µs/call`);
|
||||
```
|
||||
|
||||
## Results
|
||||
|
||||
| Function | µs/call | Notes |
|
||||
| --- | --- | --- |
|
||||
| `toHijriDate` (UAQ) | ~0.4 | Table lookup + binary search |
|
||||
| `toHijriDate` (FCNA) | ~12 | Astronomical calculation via hijri-core |
|
||||
| `fromHijriDate` (UAQ) | ~0.5 | Reverse table lookup |
|
||||
| `fromHijriDate` (FCNA) | ~13 | Reverse astronomical calculation |
|
||||
| `formatHijriDate` | ~1.2 | Includes `toHijriDate` + token replacement |
|
||||
| `addHijriMonths` | ~1.8 | Includes conversion in both directions |
|
||||
| `getHijriMonthName` | ~0.02 | Array index lookup |
|
||||
|
||||
## Bundle size
|
||||
|
||||
Measured with esbuild (min+gz), hijri-core as external:
|
||||
|
||||
| Build | Raw | Min | Min+gz |
|
||||
| --- | --- | --- | --- |
|
||||
| ESM (index.mjs) | ~6.1 KB | ~2.8 KB | ~1.3 KB |
|
||||
| CJS (index.cjs) | ~6.4 KB | ~3.0 KB | ~1.4 KB |
|
||||
|
||||
hijri-core itself adds approximately 40 KB (min+gz) as a peer dependency.
|
||||
|
||||
## Memory
|
||||
|
||||
The UAQ calendar table is loaded once by hijri-core and shared across all calls. The table occupies approximately 8 KB of heap after initial load. Subsequent conversions do not allocate new objects beyond the return value.
|
||||
|
||||
## Reproduction
|
||||
|
||||
To reproduce on your own hardware:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/acamarata/date-fns-hijri.git
|
||||
cd date-fns-hijri
|
||||
pnpm install
|
||||
pnpm build
|
||||
node -e "
|
||||
import('./dist/index.mjs').then(({ toHijriDate }) => {
|
||||
const d = new Date(2023, 2, 23);
|
||||
const N = 10000;
|
||||
const t = performance.now();
|
||||
for (let i = 0; i < N; i++) toHijriDate(d);
|
||||
console.log((performance.now() - t) / N * 1000, 'µs/call');
|
||||
});
|
||||
"
|
||||
```
|
||||
108
.github/wiki/examples/basic-usage.md
vendored
Normal file
108
.github/wiki/examples/basic-usage.md
vendored
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
# Basic Usage Examples
|
||||
|
||||
## Display today's Hijri date
|
||||
|
||||
```typescript
|
||||
import { toHijriDate, getHijriMonthName } from 'date-fns-hijri';
|
||||
|
||||
const today = new Date();
|
||||
const hijri = toHijriDate(today);
|
||||
|
||||
if (hijri) {
|
||||
const monthName = getHijriMonthName(hijri.hm);
|
||||
console.log(`${hijri.hd} ${monthName} ${hijri.hy} AH`);
|
||||
// e.g. '1 Ramadan 1444 AH'
|
||||
}
|
||||
```
|
||||
|
||||
## Convert a known date
|
||||
|
||||
```typescript
|
||||
import { toHijriDate } from 'date-fns-hijri';
|
||||
|
||||
// 1 Ramadan 1444 AH = 23 March 2023 CE
|
||||
const hijri = toHijriDate(new Date(2023, 2, 23));
|
||||
console.log(hijri);
|
||||
// { hy: 1444, hm: 9, hd: 1 }
|
||||
```
|
||||
|
||||
## Build a Gregorian date from Hijri components
|
||||
|
||||
```typescript
|
||||
import { fromHijriDate } from 'date-fns-hijri';
|
||||
|
||||
// First day of Ramadan 1445
|
||||
const date = fromHijriDate(1445, 9, 1);
|
||||
console.log(date.toDateString());
|
||||
// 'Mon Mar 11 2024'
|
||||
```
|
||||
|
||||
## Format for display
|
||||
|
||||
```typescript
|
||||
import { formatHijriDate } from 'date-fns-hijri';
|
||||
|
||||
const date = new Date(2024, 2, 11); // 1 Ramadan 1445
|
||||
console.log(formatHijriDate(date, 'iD iMMMM iYYYY')); // '1 Ramadan 1445'
|
||||
console.log(formatHijriDate(date, 'iDD/iMM/iYYYY')); // '01/09/1445'
|
||||
console.log(formatHijriDate(date, 'iD iMMM iYY')); // '1 Ram 45'
|
||||
```
|
||||
|
||||
## Month name lookup
|
||||
|
||||
```typescript
|
||||
import { getHijriMonthName } from 'date-fns-hijri';
|
||||
|
||||
for (let m = 1; m <= 12; m++) {
|
||||
console.log(`${m}: ${getHijriMonthName(m)}`);
|
||||
}
|
||||
// 1: Muharram
|
||||
// 2: Safar
|
||||
// 3: Rabi al-Awwal
|
||||
// ...
|
||||
// 9: Ramadan
|
||||
// ...
|
||||
// 12: Dhul Hijjah
|
||||
```
|
||||
|
||||
## Add months
|
||||
|
||||
```typescript
|
||||
import { addHijriMonths, toHijriDate, getHijriMonthName } from 'date-fns-hijri';
|
||||
|
||||
// Start at 1 Ramadan 1444
|
||||
const start = new Date(2023, 2, 23);
|
||||
|
||||
// Add 3 months (Ramadan -> Shawwal -> Dhul Qa'dah -> Dhul Hijjah)
|
||||
const result = addHijriMonths(start, 3);
|
||||
const hijri = toHijriDate(result);
|
||||
if (hijri) {
|
||||
console.log(`${hijri.hd} ${getHijriMonthName(hijri.hm)} ${hijri.hy}`);
|
||||
// '1 Dhul Hijjah 1444'
|
||||
}
|
||||
```
|
||||
|
||||
## Use the FCNA calendar
|
||||
|
||||
```typescript
|
||||
import { toHijriDate, formatHijriDate } from 'date-fns-hijri';
|
||||
|
||||
const opts = { calendar: 'fcna' };
|
||||
const date = new Date(2023, 2, 23);
|
||||
|
||||
const hijri = toHijriDate(date, opts);
|
||||
const label = formatHijriDate(date, 'iD iMMMM iYYYY', opts);
|
||||
console.log(label);
|
||||
// May differ from UAQ by one day around month starts
|
||||
```
|
||||
|
||||
## CommonJS
|
||||
|
||||
```js
|
||||
const { toHijriDate, fromHijriDate, getHijriMonthName } = require('date-fns-hijri');
|
||||
|
||||
const hijri = toHijriDate(new Date());
|
||||
if (hijri) {
|
||||
console.log(`Month: ${getHijriMonthName(hijri.hm)}`);
|
||||
}
|
||||
```
|
||||
98
.github/wiki/examples/formatting.md
vendored
Normal file
98
.github/wiki/examples/formatting.md
vendored
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
# Formatting Examples
|
||||
|
||||
All examples use `formatHijriDate`. The function takes a Gregorian `Date`, a format string with Hijri tokens, and an optional options argument for calendar selection.
|
||||
|
||||
## Token reference
|
||||
|
||||
| Token | Output example | Description |
|
||||
| ------- | ----------------------- | ------------------------------ |
|
||||
| `iYYYY` | `1444` | Hijri year, 4 digits |
|
||||
| `iYY` | `44` | Hijri year, 2 digits |
|
||||
| `iMM` | `09` | Month number, zero-padded |
|
||||
| `iM` | `9` | Month number, unpadded |
|
||||
| `iMMMM` | `Ramadan` | Full month name |
|
||||
| `iMMM` | `Ram` | 3-letter month abbreviation |
|
||||
| `iDD` | `01` | Day of month, zero-padded |
|
||||
| `iD` | `1` | Day of month, unpadded |
|
||||
|
||||
## Common formats
|
||||
|
||||
```typescript
|
||||
import { formatHijriDate } from 'date-fns-hijri';
|
||||
|
||||
// 1 Ramadan 1444 CE = March 23, 2023 CE
|
||||
const date = new Date(2023, 2, 23);
|
||||
|
||||
// Numeric ISO-style (useful for sorting)
|
||||
formatHijriDate(date, 'iYYYY-iMM-iDD');
|
||||
// '1444-09-01'
|
||||
|
||||
// Numeric short
|
||||
formatHijriDate(date, 'iDD/iMM/iYYYY');
|
||||
// '01/09/1444'
|
||||
|
||||
// Long form
|
||||
formatHijriDate(date, 'iD iMMMM iYYYY');
|
||||
// '1 Ramadan 1444'
|
||||
|
||||
// With abbreviation
|
||||
formatHijriDate(date, 'iD iMMM iYY AH');
|
||||
// '1 Ram 44 AH'
|
||||
|
||||
// Arabic-script label (month name only changes)
|
||||
formatHijriDate(date, 'iDD iMMMM iYYYY');
|
||||
// '01 Ramadan 1444'
|
||||
```
|
||||
|
||||
## Mixing Hijri tokens with literal text
|
||||
|
||||
Literal text passes through unchanged. Wrap text in single quotes if it contains characters that could be interpreted as format tokens.
|
||||
|
||||
```typescript
|
||||
// 'AH' contains 'A' and 'H' which are not Hijri tokens, so this is safe
|
||||
formatHijriDate(date, 'iD iMMMM iYYYY AH');
|
||||
// '1 Ramadan 1444 AH'
|
||||
|
||||
// Single-quote wrapping for safety
|
||||
formatHijriDate(date, "iD 'of' iMMMM, iYYYY");
|
||||
// '1 of Ramadan, 1444'
|
||||
```
|
||||
|
||||
## FCNA calendar formatting
|
||||
|
||||
```typescript
|
||||
import { formatHijriDate } from 'date-fns-hijri';
|
||||
|
||||
const date = new Date(2023, 2, 23);
|
||||
const fcna = { calendar: 'fcna' };
|
||||
|
||||
formatHijriDate(date, 'iD iMMMM iYYYY', fcna);
|
||||
// May be '1 Ramadan 1444' or '2 Ramadan 1444' depending on the astronomical calculation
|
||||
```
|
||||
|
||||
## Formatting in a React component
|
||||
|
||||
```tsx
|
||||
import { formatHijriDate } from 'date-fns-hijri';
|
||||
|
||||
function HijriDisplay({ date }: { date: Date }) {
|
||||
return (
|
||||
<span className="hijri-date">
|
||||
{formatHijriDate(date, 'iD iMMMM iYYYY')}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Generating a Hijri calendar grid header
|
||||
|
||||
```typescript
|
||||
import { getHijriMonthName } from 'date-fns-hijri';
|
||||
|
||||
// Render all 12 month names for a year selector
|
||||
const months = Array.from({ length: 12 }, (_, i) => ({
|
||||
number: i + 1,
|
||||
name: getHijriMonthName(i + 1),
|
||||
short: getHijriMonthName(i + 1, { format: 'short' }),
|
||||
}));
|
||||
```
|
||||
112
.github/wiki/guides/advanced.md
vendored
Normal file
112
.github/wiki/guides/advanced.md
vendored
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
# Advanced Usage
|
||||
|
||||
## Null handling and range validation
|
||||
|
||||
`toHijriDate` returns `null` for dates outside the UAQ table range (1318-1500 AH, approximately 1900-2076 CE). Guard against null before using the result.
|
||||
|
||||
```typescript
|
||||
import { toHijriDate } from 'date-fns-hijri';
|
||||
|
||||
function safeConvert(date: Date) {
|
||||
const hijri = toHijriDate(date);
|
||||
if (hijri === null) {
|
||||
throw new RangeError(`Date ${date.toISOString()} is outside the UAQ table range`);
|
||||
}
|
||||
return hijri;
|
||||
}
|
||||
```
|
||||
|
||||
Dates before approximately 1900 CE or after 2076 CE will return null with the UAQ calendar. Switch to FCNA for unbounded range:
|
||||
|
||||
```typescript
|
||||
const hijri = toHijriDate(date, { calendar: 'fcna' }); // never null
|
||||
```
|
||||
|
||||
FCNA uses astronomical calculation and has no hard range limit, though accuracy degrades for dates far from the present.
|
||||
|
||||
## Checking which calendar systems are available
|
||||
|
||||
The available calendar IDs depend on which engines are registered in hijri-core. UAQ and FCNA are always registered. If you use a custom engine registered via `hijri-core`'s `registerCalendar()`, you can pass its ID in the options.
|
||||
|
||||
```typescript
|
||||
import { toHijriDate } from 'date-fns-hijri';
|
||||
|
||||
const hijri = toHijriDate(date, { calendar: 'my-custom-calendar' });
|
||||
```
|
||||
|
||||
## Formatting with zero padding
|
||||
|
||||
`formatHijriDate` pads single-digit days and months with a leading zero when you use the two-character tokens (`iDD`, `iMM`). To get unpadded values, use the single-character equivalents (`iD`, `iM`).
|
||||
|
||||
```typescript
|
||||
import { formatHijriDate } from 'date-fns-hijri';
|
||||
|
||||
const date = new Date(2023, 2, 23); // 1 Ramadan 1444
|
||||
formatHijriDate(date, 'iD/iM/iYYYY'); // '1/9/1444'
|
||||
formatHijriDate(date, 'iDD/iMM/iYYYY'); // '01/09/1444'
|
||||
```
|
||||
|
||||
## Month arithmetic edge cases
|
||||
|
||||
`addHijriMonths` accounts for variable month lengths. When the source day does not exist in the target month (Hijri months alternate between 29 and 30 days depending on the calendar), the result clamps to the last valid day of the target month.
|
||||
|
||||
```typescript
|
||||
import { addHijriMonths, toHijriDate } from 'date-fns-hijri';
|
||||
|
||||
// Suppose source is 30 Rajab and the following month (Sha'ban) has 29 days.
|
||||
// addHijriMonths clamps the result to 29 Sha'ban.
|
||||
const result = addHijriMonths(new Date(2023, 0, 21), 1);
|
||||
const hijri = toHijriDate(result);
|
||||
// hijri.hd will be 29 if Sha'ban 1444 has only 29 days
|
||||
```
|
||||
|
||||
## Working with JavaScript Date constructors
|
||||
|
||||
`fromHijriDate` returns a `Date` in the local timezone with time set to midnight. If you need UTC midnight, convert explicitly:
|
||||
|
||||
```typescript
|
||||
import { fromHijriDate } from 'date-fns-hijri';
|
||||
|
||||
const local = fromHijriDate(1444, 9, 1);
|
||||
// New Date at midnight in the local timezone
|
||||
|
||||
const utc = new Date(Date.UTC(
|
||||
local.getFullYear(),
|
||||
local.getMonth(),
|
||||
local.getDate()
|
||||
));
|
||||
```
|
||||
|
||||
## Integrating with date-fns formatting
|
||||
|
||||
date-fns-hijri works with plain `Date` objects, so it integrates cleanly with date-fns formatting functions. Use date-fns for Gregorian formatting and this package for Hijri-specific tokens.
|
||||
|
||||
```typescript
|
||||
import { format } from 'date-fns';
|
||||
import { formatHijriDate } from 'date-fns-hijri';
|
||||
|
||||
const date = new Date(2023, 2, 23);
|
||||
|
||||
// Gregorian day of week from date-fns
|
||||
const dayOfWeek = format(date, 'EEEE'); // 'Thursday'
|
||||
|
||||
// Hijri date from date-fns-hijri
|
||||
const hijriLabel = formatHijriDate(date, 'iD iMMMM iYYYY'); // '1 Ramadan 1444'
|
||||
|
||||
const combined = `${dayOfWeek}, ${hijriLabel}`;
|
||||
// 'Thursday, 1 Ramadan 1444'
|
||||
```
|
||||
|
||||
## TypeScript: narrowing the return type
|
||||
|
||||
When you know the date is within the UAQ range, you can assert non-null:
|
||||
|
||||
```typescript
|
||||
import { toHijriDate, HijriDate } from 'date-fns-hijri';
|
||||
|
||||
function convert(date: Date): HijriDate {
|
||||
const result = toHijriDate(date);
|
||||
if (result === null) throw new RangeError('Out of UAQ range');
|
||||
return result;
|
||||
}
|
||||
```
|
||||
104
.github/wiki/guides/quickstart.md
vendored
Normal file
104
.github/wiki/guides/quickstart.md
vendored
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
# Quick Start
|
||||
|
||||
This guide covers the most common use cases in date-fns-hijri. All examples use the default Umm al-Qura (UAQ) calendar. For FCNA/ISNA calendar output, pass `{ calendar: 'fcna' }` as the last argument to any function.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pnpm add date-fns-hijri hijri-core
|
||||
```
|
||||
|
||||
`hijri-core` is a required peer dependency. It provides the calendar engine and must be installed alongside this package.
|
||||
|
||||
## Convert a Gregorian date to Hijri
|
||||
|
||||
```typescript
|
||||
import { toHijriDate } from 'date-fns-hijri';
|
||||
|
||||
const date = new Date(2023, 2, 23); // March 23, 2023
|
||||
const hijri = toHijriDate(date);
|
||||
// { hy: 1444, hm: 9, hd: 1 }
|
||||
```
|
||||
|
||||
`toHijriDate` returns `null` for dates outside the UAQ table range (1318-1500 AH, approximately 1900-2076 CE). Always check for null before using the result.
|
||||
|
||||
## Convert a Hijri date to Gregorian
|
||||
|
||||
```typescript
|
||||
import { fromHijriDate } from 'date-fns-hijri';
|
||||
|
||||
const gregorian = fromHijriDate(1444, 9, 1);
|
||||
// Date: 2023-03-23T00:00:00.000Z
|
||||
```
|
||||
|
||||
## Format a Hijri date
|
||||
|
||||
```typescript
|
||||
import { formatHijriDate } from 'date-fns-hijri';
|
||||
|
||||
const date = new Date(2023, 2, 23);
|
||||
const label = formatHijriDate(date, 'iDD iMMMM iYYYY');
|
||||
// '01 Ramadan 1444'
|
||||
```
|
||||
|
||||
Supported format tokens:
|
||||
|
||||
| Token | Output |
|
||||
| ------ | ----------------------- |
|
||||
| `iYYYY`| Hijri year (4 digits) |
|
||||
| `iYY` | Hijri year (2 digits) |
|
||||
| `iMM` | Month number (01-12) |
|
||||
| `iMMM` | Short month name |
|
||||
| `iMMMM`| Full month name |
|
||||
| `iDD` | Day (01-30) |
|
||||
| `iD` | Day (1-30) |
|
||||
|
||||
## Get a month name
|
||||
|
||||
```typescript
|
||||
import { getHijriMonthName } from 'date-fns-hijri';
|
||||
|
||||
const name = getHijriMonthName(9);
|
||||
// 'Ramadan'
|
||||
|
||||
const shortName = getHijriMonthName(9, { format: 'short' });
|
||||
// 'Ram'
|
||||
```
|
||||
|
||||
## Add months in Hijri space
|
||||
|
||||
```typescript
|
||||
import { addHijriMonths } from 'date-fns-hijri';
|
||||
|
||||
const ramadan = new Date(2023, 2, 23); // 1 Ramadan 1444
|
||||
const shawwal = addHijriMonths(ramadan, 1);
|
||||
// Date representing 1 Shawwal 1444 (April 21, 2023)
|
||||
```
|
||||
|
||||
Month arithmetic respects variable-length Hijri months (29 or 30 days depending on the calendar).
|
||||
|
||||
## Use the FCNA calendar
|
||||
|
||||
```typescript
|
||||
import { toHijriDate, formatHijriDate } from 'date-fns-hijri';
|
||||
|
||||
const opts = { calendar: 'fcna' };
|
||||
const hijri = toHijriDate(new Date(2023, 2, 23), opts);
|
||||
const label = formatHijriDate(new Date(2023, 2, 23), 'iDD iMMMM iYYYY', opts);
|
||||
```
|
||||
|
||||
FCNA (Fiqh Council of North America) uses astronomical new moon calculation rather than the Umm al-Qura table. Results may differ by one day around month boundaries.
|
||||
|
||||
## CommonJS
|
||||
|
||||
```js
|
||||
const { toHijriDate, fromHijriDate, formatHijriDate } = require('date-fns-hijri');
|
||||
|
||||
const hijri = toHijriDate(new Date(2023, 2, 23));
|
||||
```
|
||||
|
||||
## Next steps
|
||||
|
||||
- [API Reference](API-Reference) for the full function list and signatures
|
||||
- [Architecture](Architecture) for how the calendar engine and format layer work
|
||||
- [Advanced Guide](guides/advanced) for error handling, range validation, and locale patterns
|
||||
49
.github/workflows/ci.yml
vendored
49
.github/workflows/ci.yml
vendored
|
|
@ -15,22 +15,39 @@ jobs:
|
|||
node: [20, 22, 24]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
- name: Enable corepack
|
||||
run: corepack enable
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm run build
|
||||
- run: node test.mjs
|
||||
- run: node test-cjs.cjs
|
||||
- run: node --test test.mjs
|
||||
- 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:
|
||||
name: Typecheck
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
- name: Enable corepack
|
||||
run: corepack enable
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
|
|
@ -43,7 +60,8 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
- name: Enable corepack
|
||||
run: corepack enable
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
|
|
@ -60,3 +78,24 @@ jobs:
|
|||
grep "README.md" pack-output.txt
|
||||
grep "CHANGELOG.md" pack-output.txt
|
||||
grep "LICENSE" pack-output.txt
|
||||
|
||||
coverage:
|
||||
name: Coverage
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Enable corepack
|
||||
run: corepack enable
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm run build
|
||||
- run: pnpm run coverage
|
||||
- name: Upload to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
files: ./coverage/lcov.info
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
fail_ci_if_error: false
|
||||
|
|
|
|||
23
.github/workflows/wiki-sync.yml
vendored
23
.github/workflows/wiki-sync.yml
vendored
|
|
@ -1,22 +1,25 @@
|
|||
name: Wiki sync
|
||||
name: Wiki Sync
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- '.wiki/**'
|
||||
- '.github/wiki/**'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
name: Sync .wiki to GitHub Wiki
|
||||
name: Sync wiki to GitHub Wiki
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Sync wiki pages
|
||||
uses: nicenshtein/wiki-page-creator-action@v1
|
||||
env:
|
||||
GH_PAT: ${{ secrets.GH_PAT }}
|
||||
|
||||
- name: Sync .github/wiki/ to GitHub Wiki
|
||||
uses: Andrew-Chen-Wang/github-wiki-action@v4
|
||||
with:
|
||||
owner: acamarata
|
||||
repo-name: date-fns-hijri
|
||||
md-folder: .wiki
|
||||
path: .github/wiki/
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_ACTOR: ${{ github.actor }}
|
||||
|
|
|
|||
15
.gitignore
vendored
15
.gitignore
vendored
|
|
@ -1,8 +1,23 @@
|
|||
node_modules/
|
||||
dist/
|
||||
coverage/
|
||||
*.tgz
|
||||
*.log
|
||||
.DS_Store
|
||||
.claude/
|
||||
.env
|
||||
.env.*
|
||||
|
||||
# AI agent directories
|
||||
.cursor/
|
||||
.copilot/
|
||||
.aider*
|
||||
.aider.chat.history.md
|
||||
.continue/
|
||||
.codex/
|
||||
.gemini/
|
||||
.vscode/*
|
||||
.idea/
|
||||
.aider/
|
||||
.windsurf/
|
||||
.codeium/
|
||||
|
|
|
|||
53
CHANGELOG.md
53
CHANGELOG.md
|
|
@ -2,29 +2,38 @@
|
|||
|
||||
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 correctly.
|
||||
|
||||
## [1.0.3] - 2026-06-10
|
||||
|
||||
### Fixed
|
||||
- `toHijriDate` and all field getters now produce exact round-trips on every host timezone (input Date interpreted by its local calendar day, matching date-fns conventions; previously used raw Date which failed in timezones west of UTC against hijri-core's UTC-day contract).
|
||||
|
||||
### Changed
|
||||
- `fromHijriDate` and all arithmetic/boundary helpers (`addHijriMonths`, `addHijriYears`, `startOfHijriMonth`, `endOfHijriMonth`) now return **local-midnight** Dates instead of UTC midnight / local noon. Use `getFullYear()`/`getMonth()`/`getDate()` (or date-fns `format()`) on the result — not `toISOString()`.
|
||||
- Requires hijri-core 1.0.3 (UTC-day contract).
|
||||
|
||||
## [1.0.2] - 2026-05-30
|
||||
|
||||
### Changed
|
||||
- Trim README to concise reference format; remove redundant em-dash connectors
|
||||
- Add TypeDoc API documentation generation
|
||||
|
||||
## [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
|
||||
|
||||
- `toHijriDate(date, options?)` - Convert a Gregorian Date to a HijriDate object
|
||||
- `fromHijriDate(hy, hm, hd, options?)` - Convert Hijri date components to a Gregorian Date
|
||||
- `isValidHijriDate(hy, hm, hd, options?)` - Validate a Hijri date
|
||||
- `getHijriYear(date, options?)` - Extract the Hijri year from a Gregorian date
|
||||
- `getHijriMonth(date, options?)` - Extract the Hijri month (1-12) from a Gregorian date
|
||||
- `getHijriDay(date, options?)` - Extract the Hijri day of month from a Gregorian date
|
||||
- `getDaysInHijriMonth(hy, hm, options?)` - Days in a given Hijri month (29 or 30)
|
||||
- `getHijriMonthName(hm, length?)` - English month name in long, medium, or short form
|
||||
- `getHijriWeekdayName(date, length?)` - Arabic weekday name (long or short)
|
||||
- `formatHijriDate(date, formatStr, options?)` - Format a date with Hijri tokens
|
||||
- `addHijriMonths(date, months, options?)` - Add Hijri months to a date
|
||||
- `addHijriYears(date, years, options?)` - Add Hijri years to a date
|
||||
- `startOfHijriMonth(date, options?)` - First day of the Hijri month
|
||||
- `endOfHijriMonth(date, options?)` - Last day of the Hijri month
|
||||
- `isSameHijriMonth(dateA, dateB, options?)` - Check if two dates share a Hijri month
|
||||
- `isSameHijriYear(dateA, dateB, options?)` - Check if two dates share a Hijri year
|
||||
- `getHijriQuarter(date, options?)` - Hijri quarter (1-4) for a date
|
||||
- Full TypeScript definitions with dual CJS/ESM build
|
||||
- Support for Umm al-Qura (UAQ) and FCNA/ISNA calendar systems via `hijri-core`
|
||||
- Initial release
|
||||
|
|
|
|||
197
README.md
197
README.md
|
|
@ -4,9 +4,9 @@
|
|||
[](https://github.com/acamarata/date-fns-hijri/actions/workflows/ci.yml)
|
||||
[](LICENSE)
|
||||
|
||||
date-fns-style functions for Hijri calendar operations. Works with any date library.
|
||||
date-fns-style functions for Hijri calendar operations. Each function is a pure, stateless utility. Pass a `Date`, get a result. No classes, no global configuration.
|
||||
|
||||
Each function is a pure, stateless utility. No classes. No configuration object. Pass a `Date`, get a result. Pass options to switch calendar systems. The API mirrors date-fns conventions so the learning curve is minimal.
|
||||
Built on [hijri-core](https://github.com/acamarata/hijri-core). Supports Umm al-Qura (UAQ) and FCNA/ISNA calendar systems.
|
||||
|
||||
## Installation
|
||||
|
||||
|
|
@ -14,7 +14,7 @@ Each function is a pure, stateless utility. No classes. No configuration object.
|
|||
pnpm add date-fns-hijri hijri-core
|
||||
```
|
||||
|
||||
`hijri-core` is a peer dependency. It provides the underlying calendar engine and must be installed alongside this package.
|
||||
`hijri-core` is a peer dependency. It provides the underlying calendar engine.
|
||||
|
||||
## Quick Start
|
||||
|
||||
|
|
@ -27,158 +27,67 @@ import {
|
|||
getHijriMonthName,
|
||||
} from 'date-fns-hijri';
|
||||
|
||||
// Convert a Gregorian date to Hijri
|
||||
const hijri = toHijriDate(new Date(2023, 2, 23));
|
||||
// { hy: 1444, hm: 9, hd: 1 } - 1 Ramadan 1444
|
||||
|
||||
// Convert back
|
||||
const gregorian = fromHijriDate(1444, 9, 1);
|
||||
// Date: 2023-03-23T00:00:00.000Z
|
||||
// Convert Gregorian to Hijri
|
||||
const hijri = toHijriDate(new Date(2023, 2, 23, 12));
|
||||
// { hy: 1444, hm: 9, hd: 1 } (1 Ramadan 1444)
|
||||
|
||||
// Format with Hijri tokens
|
||||
const label = formatHijriDate(new Date(2023, 2, 23), 'iDD iMMMM iYYYY');
|
||||
// '01 Ramadan 1444'
|
||||
|
||||
// Get the month name directly
|
||||
const name = getHijriMonthName(9);
|
||||
// 'Ramadan'
|
||||
|
||||
// Add months in the Hijri calendar
|
||||
const nextMonth = addHijriMonths(new Date(2023, 2, 23), 1);
|
||||
// Date in Shawwal 1444
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
All functions accept an optional `options` argument for selecting the calendar system. When omitted, Umm al-Qura (UAQ) is used.
|
||||
|
||||
### Conversion
|
||||
|
||||
| Function | Signature | Description |
|
||||
| --- | --- | --- |
|
||||
| `toHijriDate` | `(date: Date, options?) => HijriDate \| null` | Convert Gregorian to Hijri. Returns `null` if out of range. |
|
||||
| `fromHijriDate` | `(hy, hm, hd, options?) => Date` | Convert Hijri to Gregorian. Throws if invalid. |
|
||||
|
||||
### Validation
|
||||
|
||||
| Function | Signature | Description |
|
||||
| --- | --- | --- |
|
||||
| `isValidHijriDate` | `(hy, hm, hd, options?) => boolean` | Check if a Hijri date exists in the calendar table. |
|
||||
|
||||
### Field Getters
|
||||
|
||||
| Function | Signature | Description |
|
||||
| --- | --- | --- |
|
||||
| `getHijriYear` | `(date, options?) => number \| null` | Hijri year. Null if out of range. |
|
||||
| `getHijriMonth` | `(date, options?) => number \| null` | Hijri month (1-12). Null if out of range. |
|
||||
| `getHijriDay` | `(date, options?) => number \| null` | Hijri day of month. Null if out of range. |
|
||||
| `getDaysInHijriMonth` | `(hy, hm, options?) => number` | Days in a Hijri month (29 or 30). |
|
||||
| `getHijriQuarter` | `(date, options?) => number \| null` | Quarter (1-4). Null if out of range. |
|
||||
|
||||
### Names
|
||||
|
||||
| Function | Signature | Description |
|
||||
| --- | --- | --- |
|
||||
| `getHijriMonthName` | `(hm, length?) => string` | English month name. `length`: `'long'` (default), `'medium'`, `'short'`. |
|
||||
| `getHijriWeekdayName` | `(date, length?) => string` | Arabic weekday name. `length`: `'long'` (default), `'short'`. |
|
||||
|
||||
### Formatting
|
||||
|
||||
| Function | Signature | Description |
|
||||
| --- | --- | --- |
|
||||
| `formatHijriDate` | `(date, formatStr, options?) => string` | Format a date with Hijri tokens. Returns `''` if out of range. |
|
||||
|
||||
### Arithmetic
|
||||
|
||||
| Function | Signature | Description |
|
||||
| --- | --- | --- |
|
||||
| `addHijriMonths` | `(date, months, options?) => Date` | Add N Hijri months. Clamps day to month length. |
|
||||
| `addHijriYears` | `(date, years, options?) => Date` | Add N Hijri years. Clamps day to month length. |
|
||||
|
||||
### Month Boundaries
|
||||
|
||||
| Function | Signature | Description |
|
||||
| --- | --- | --- |
|
||||
| `startOfHijriMonth` | `(date, options?) => Date` | First day of the containing Hijri month. |
|
||||
| `endOfHijriMonth` | `(date, options?) => Date` | Last day of the containing Hijri month. |
|
||||
|
||||
### Comparisons
|
||||
|
||||
| Function | Signature | Description |
|
||||
| --- | --- | --- |
|
||||
| `isSameHijriMonth` | `(dateA, dateB, options?) => boolean` | Both dates in the same Hijri month. |
|
||||
| `isSameHijriYear` | `(dateA, dateB, options?) => boolean` | Both dates in the same Hijri year. |
|
||||
|
||||
## Calendar Systems
|
||||
|
||||
Two calendar systems are available via the `options.calendar` property.
|
||||
|
||||
**Umm al-Qura (default):**
|
||||
The official calendar of Saudi Arabia. Covers 1318–1500 AH (1900–2076 CE). Tabular data; deterministic.
|
||||
|
||||
```typescript
|
||||
import { toHijriDate } from 'date-fns-hijri';
|
||||
|
||||
const uaq = toHijriDate(new Date(2023, 2, 23));
|
||||
// uses UAQ by default
|
||||
```
|
||||
|
||||
**FCNA/ISNA:**
|
||||
The calendar used by the Fiqh Council of North America. Astronomical calculation; extends slightly beyond UAQ's range.
|
||||
|
||||
```typescript
|
||||
const fcna = toHijriDate(new Date(2023, 2, 23), { calendar: 'fcna' });
|
||||
```
|
||||
|
||||
## Format Tokens
|
||||
|
||||
| Token | Output | Example |
|
||||
| --- | --- | --- |
|
||||
| `iYYYY` | 4-digit Hijri year | `1444` |
|
||||
| `iYY` | 2-digit Hijri year | `44` |
|
||||
| `iMMMM` | Long month name | `Ramadan` |
|
||||
| `iMMM` | Medium month name | `Ramadan` |
|
||||
| `iMM` | Zero-padded month | `09` |
|
||||
| `iM` | Month number | `9` |
|
||||
| `iDD` | Zero-padded day | `01` |
|
||||
| `iD` | Day number | `1` |
|
||||
| `iEEEE` | Long weekday name | `Yawm al-Khamis` |
|
||||
| `iEEE` | Short weekday name | `Kham` |
|
||||
| `iE` | Numeric weekday (1=Sun) | `5` |
|
||||
| `ioooo` | Long era | `AH` |
|
||||
| `iooo` | Short era | `AH` |
|
||||
|
||||
Non-token text in the format string passes through unchanged:
|
||||
|
||||
```typescript
|
||||
formatHijriDate(new Date(2023, 2, 23), 'iYYYY-iMM-iDD')
|
||||
// '1444-09-01'
|
||||
|
||||
formatHijriDate(new Date(2023, 2, 23), 'iD iMMMM iYYYY ioooo')
|
||||
const label = formatHijriDate(new Date(2023, 2, 23, 12), 'iD iMMMM iYYYY ioooo');
|
||||
// '1 Ramadan 1444 AH'
|
||||
```
|
||||
|
||||
## TypeScript
|
||||
// Add Hijri months
|
||||
const eid = addHijriMonths(new Date(2023, 2, 23, 12), 1);
|
||||
// Date in Shawwal 1444
|
||||
|
||||
Full type definitions are included. Re-exported from `hijri-core`:
|
||||
|
||||
```typescript
|
||||
import type { HijriDate, ConversionOptions } from 'date-fns-hijri';
|
||||
|
||||
const h: HijriDate = { hy: 1444, hm: 9, hd: 1 };
|
||||
const opts: ConversionOptions = { calendar: 'fcna' };
|
||||
// Get the month name
|
||||
getHijriMonthName(9); // 'Ramadan'
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
Full API reference, architecture notes, and examples: [Wiki](https://github.com/acamarata/date-fns-hijri/wiki)
|
||||
Full API reference, guides, and examples: **[Wiki](https://github.com/acamarata/date-fns-hijri/wiki)**
|
||||
|
||||
## Related Packages
|
||||
- [API Reference](https://github.com/acamarata/date-fns-hijri/wiki/API-Reference): all 17 functions with signatures and examples
|
||||
- [Architecture](https://github.com/acamarata/date-fns-hijri/wiki/Architecture): design decisions and hijri-core integration
|
||||
- [Quick Start](https://github.com/acamarata/date-fns-hijri/wiki/guides/quickstart)
|
||||
|
||||
- [hijri-core](https://github.com/acamarata/hijri-core) - Zero-dependency Hijri engine powering this library
|
||||
- [luxon-hijri](https://github.com/acamarata/luxon-hijri) - Hijri support for Luxon DateTime objects
|
||||
- [pray-calc](https://github.com/acamarata/pray-calc) - Islamic prayer times
|
||||
- [nrel-spa](https://github.com/acamarata/nrel-spa) - Solar position algorithm
|
||||
## Day boundaries and time zones
|
||||
|
||||
This package follows date-fns local-time conventions:
|
||||
|
||||
- **Inputs** (`toHijriDate`, `getHijri*`, `formatHijriDate`, arithmetic, comparisons) — the input `Date` is read by its **local calendar day** (using `getFullYear`/`getMonth`/`getDate`). This matches how date-fns' own `format()` and field accessors work.
|
||||
- **Outputs** (`fromHijriDate` and all arithmetic/boundary functions) — returned `Date` values are **local midnight** of the equivalent Gregorian day. Local field accessors and date-fns' `format()` will render the intended date on every timezone.
|
||||
|
||||
Round-trips are exact on every host timezone:
|
||||
|
||||
```typescript
|
||||
toHijriDate(fromHijriDate(1446, 9, 1)); // always { hy: 1446, hm: 9, hd: 1 }
|
||||
```
|
||||
|
||||
**Pitfall:** `new Date("2025-03-01")` parses as UTC midnight. In timezones west of UTC this resolves to the previous local day (Feb 28), giving an off-by-one result. Use the local-date constructor instead:
|
||||
|
||||
```typescript
|
||||
// Wrong in timezones west of UTC:
|
||||
toHijriDate(new Date("2025-03-01")); // may return 29 Shaban in some zones
|
||||
|
||||
// Correct everywhere:
|
||||
toHijriDate(new Date(2025, 2, 1)); // always 1 Ramadan 1446
|
||||
```
|
||||
|
||||
Religious day-start (sunset boundary) is out of scope — this package only handles civil calendar day alignment.
|
||||
|
||||
## Related
|
||||
|
||||
- [hijri-core](https://github.com/acamarata/hijri-core): the calendar engine powering this library
|
||||
- [luxon-hijri](https://github.com/acamarata/luxon-hijri): Hijri support for Luxon DateTime objects
|
||||
- [pray-calc](https://github.com/acamarata/pray-calc): Islamic prayer times
|
||||
|
||||
## Compatibility
|
||||
|
||||
- Node.js 20, 22, 24
|
||||
- ESM and CJS builds included
|
||||
- TypeScript definitions bundled
|
||||
- Works in browsers and all major bundlers
|
||||
|
||||
## License
|
||||
|
||||
|
|
|
|||
126
date-fns-hijri.test.ts
Normal file
126
date-fns-hijri.test.ts
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
/**
|
||||
* Purpose: Vitest suite for date-fns-hijri — functional Hijri date utilities.
|
||||
* Inputs: Pure functions from src/index.ts wrapping hijri-core. No network, no I/O.
|
||||
* Outputs: Vitest pass/fail assertions.
|
||||
* Constraints: UAQ range 1318–1500 AH; fromHijriDate throws on invalid input (null path).
|
||||
* Use local-date constructor new Date(y, m, d) — not string "YYYY-MM-DD" which
|
||||
* parses as UTC midnight and can be the previous LOCAL day west of UTC.
|
||||
* Usage: pnpm vitest run
|
||||
* SOT: packages.md — date-fns-hijri row
|
||||
*/
|
||||
import { describe, it, expect } from "vitest";
|
||||
import {
|
||||
toHijriDate,
|
||||
fromHijriDate,
|
||||
isValidHijriDate,
|
||||
getHijriYear,
|
||||
getHijriMonth,
|
||||
getHijriDay,
|
||||
getDaysInHijriMonth,
|
||||
getHijriMonthName,
|
||||
getHijriWeekdayName,
|
||||
} from "./src/index";
|
||||
|
||||
// Anchor: 1 Ramadan 1446 = 2025-03-01 in the Gregorian calendar.
|
||||
// Use local-date constructor to avoid the UTC-parsing pitfall with string form.
|
||||
// At local noon the local calendar day is unambiguous on every timezone.
|
||||
const RAMADAN_1446_NOON = new Date(2025, 2, 1, 12); // local noon 2025-03-01
|
||||
|
||||
describe("toHijriDate", () => {
|
||||
it("converts noon 2025-03-01 UTC to 1 Ramadan 1446", () => {
|
||||
const result = toHijriDate(RAMADAN_1446_NOON);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.hy).toBe(1446);
|
||||
expect(result!.hm).toBe(9);
|
||||
expect(result!.hd).toBe(1);
|
||||
});
|
||||
|
||||
it("returns null for dates outside UAQ range (2100)", () => {
|
||||
expect(toHijriDate(new Date("2100-01-01"))).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("fromHijriDate", () => {
|
||||
it("converts 1 Ramadan 1446 to local 2025-03-01 (via local accessors)", () => {
|
||||
const result = fromHijriDate(1446, 9, 1);
|
||||
// Returns local midnight: local accessors show the intended calendar day
|
||||
// on every host timezone. Do NOT use toISOString() — it shows UTC which
|
||||
// will be the previous day in timezones west of UTC.
|
||||
expect(result.getFullYear()).toBe(2025);
|
||||
expect(result.getMonth()).toBe(2); // March
|
||||
expect(result.getDate()).toBe(1);
|
||||
});
|
||||
|
||||
it("round-trip: toHijriDate(fromHijriDate(1446, 9, 1)) === {1446, 9, 1}", () => {
|
||||
const d = fromHijriDate(1446, 9, 1);
|
||||
const h = toHijriDate(d);
|
||||
expect(h).not.toBeNull();
|
||||
expect(h!.hy).toBe(1446);
|
||||
expect(h!.hm).toBe(9);
|
||||
expect(h!.hd).toBe(1);
|
||||
});
|
||||
|
||||
it("throws on an out-of-range Hijri year (1501)", () => {
|
||||
expect(() => fromHijriDate(1501, 1, 1)).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("isValidHijriDate", () => {
|
||||
it("returns true for 1 Ramadan 1446", () => {
|
||||
expect(isValidHijriDate(1446, 9, 1)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false for month 13", () => {
|
||||
expect(isValidHijriDate(1446, 13, 1)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("field getters", () => {
|
||||
it("getHijriYear returns 1446 for noon 2025-03-01", () => {
|
||||
expect(getHijriYear(RAMADAN_1446_NOON)).toBe(1446);
|
||||
});
|
||||
|
||||
it("getHijriMonth returns 9 for Ramadan", () => {
|
||||
expect(getHijriMonth(RAMADAN_1446_NOON)).toBe(9);
|
||||
});
|
||||
|
||||
it("getHijriDay returns 1", () => {
|
||||
expect(getHijriDay(RAMADAN_1446_NOON)).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getDaysInHijriMonth", () => {
|
||||
it("returns 29 or 30 for Ramadan 1446", () => {
|
||||
const days = getDaysInHijriMonth(1446, 9);
|
||||
expect([29, 30]).toContain(days);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getHijriMonthName", () => {
|
||||
it("returns Ramadan for month 9 (long)", () => {
|
||||
expect(getHijriMonthName(9, "long")).toBe("Ramadan");
|
||||
});
|
||||
|
||||
it("throws RangeError for month 0", () => {
|
||||
expect(() => getHijriMonthName(0)).toThrow(RangeError);
|
||||
});
|
||||
|
||||
it("returns a non-empty medium name for month 1", () => {
|
||||
const name = getHijriMonthName(1, "medium");
|
||||
expect(name.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getHijriWeekdayName", () => {
|
||||
it("returns a non-empty long weekday name for 2025-03-01 (Saturday)", () => {
|
||||
const name = getHijriWeekdayName(RAMADAN_1446_NOON, "long");
|
||||
expect(typeof name).toBe("string");
|
||||
expect(name.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("short name is no longer than long name for the same date", () => {
|
||||
const long = getHijriWeekdayName(RAMADAN_1446_NOON, "long");
|
||||
const short = getHijriWeekdayName(RAMADAN_1446_NOON, "short");
|
||||
expect(short.length).toBeLessThanOrEqual(long.length);
|
||||
});
|
||||
});
|
||||
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 [
|
||||
{
|
||||
ignores: ['dist/', 'node_modules/', 'test.mjs', 'test-cjs.cjs'],
|
||||
},
|
||||
{
|
||||
files: ['src/**/*.ts'],
|
||||
plugins: { '@typescript-eslint': tsPlugin },
|
||||
languageOptions: {
|
||||
parser: tsParser,
|
||||
parserOptions: { project: true, tsconfigRootDir: import.meta.dirname },
|
||||
},
|
||||
},
|
||||
...typescript.map((cfg) => ({ ...cfg, files: ['src/**/*.ts'] })),
|
||||
{ ...eslintConfigPrettier, files: ['src/**/*.ts'] },
|
||||
];
|
||||
61
package.json
61
package.json
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "date-fns-hijri",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.4",
|
||||
"description": "date-fns-style utility functions for Hijri calendar operations. Wraps hijri-core with a functional API for converting, formatting, and validating Hijri dates.",
|
||||
"author": "Aric Camarata",
|
||||
"license": "MIT",
|
||||
|
|
@ -9,9 +9,11 @@
|
|||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": { "types": "./dist/index.d.mts", "default": "./dist/index.mjs" },
|
||||
"require": { "types": "./dist/index.d.ts", "default": "./dist/index.cjs" }
|
||||
}
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.mjs",
|
||||
"require": "./dist/index.cjs"
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"sideEffects": false,
|
||||
"files": [
|
||||
|
|
@ -23,14 +25,23 @@
|
|||
"CHANGELOG.md",
|
||||
"LICENSE"
|
||||
],
|
||||
"engines": { "node": ">=20" },
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"packageManager": "pnpm@10.30.1",
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"pretest": "tsup",
|
||||
"test": "node test.mjs && node test-cjs.cjs",
|
||||
"prepublishOnly": "tsup"
|
||||
"test": "node --test test.mjs && node --test test-cjs.cjs",
|
||||
"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",
|
||||
"test:vitest": "vitest run"
|
||||
},
|
||||
"keywords": [
|
||||
"date-fns",
|
||||
|
|
@ -48,13 +59,37 @@
|
|||
"hijri-core": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.0",
|
||||
"hijri-core": "^1.0.0",
|
||||
"@acamarata/eslint-config": "^0.1.0",
|
||||
"@acamarata/prettier-config": "^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",
|
||||
"eslint": "^10.0.3",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"hijri-core": "^1.0.3",
|
||||
"prettier": "^3.8.1",
|
||||
"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",
|
||||
"vitest": "^2.1.9"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/acamarata/date-fns-hijri.git"
|
||||
},
|
||||
"publishConfig": { "access": "public", "registry": "https://registry.npmjs.org/" },
|
||||
"repository": { "type": "git", "url": "git+https://github.com/acamarata/date-fns-hijri.git" },
|
||||
"homepage": "https://github.com/acamarata/date-fns-hijri#readme",
|
||||
"bugs": { "url": "https://github.com/acamarata/date-fns-hijri/issues" }
|
||||
"bugs": {
|
||||
"url": "https://github.com/acamarata/date-fns-hijri/issues"
|
||||
},
|
||||
"type": "module",
|
||||
"prettier": "@acamarata/prettier-config"
|
||||
}
|
||||
|
|
|
|||
1954
pnpm-lock.yaml
1954
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
268
src/index.ts
268
src/index.ts
|
|
@ -9,11 +9,32 @@ import {
|
|||
hwLong,
|
||||
hwShort,
|
||||
hwNumeric,
|
||||
} from 'hijri-core';
|
||||
} from "hijri-core";
|
||||
|
||||
export type { HijriDate, CalendarEngine, ConversionOptions } from './types';
|
||||
export type { HijriDate, CalendarEngine, ConversionOptions } from "./types";
|
||||
|
||||
import type { HijriDate, ConversionOptions } from './types';
|
||||
import type { HijriDate, ConversionOptions } from "./types";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Internal helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Purpose: Lift a Date's LOCAL calendar components (year, month, day) into the
|
||||
* UTC slot so that hijri-core's UTC-day contract reads the caller's
|
||||
* intended calendar day regardless of host timezone.
|
||||
* Inputs: Any Gregorian Date.
|
||||
* Outputs: A new Date whose UTC year/month/date equal the input's LOCAL year/month/date.
|
||||
* Constraints: Used only as input to coreToHijri; the returned value is an ephemeral
|
||||
* intermediate — never hand it to Date#getFullYear or date-fns functions.
|
||||
* WHY: date-fns is a LOCAL-time library: its functions read local components.
|
||||
* hijri-core (after fix/utc-day-boundary) reads the UTC calendar day.
|
||||
* Without this shim, hosts west of UTC see the previous UTC day for
|
||||
* a local-midnight Date, causing off-by-one conversions.
|
||||
*/
|
||||
function localDayToUtcSlot(date: Date): Date {
|
||||
return new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Conversion
|
||||
|
|
@ -22,19 +43,39 @@ import type { HijriDate, ConversionOptions } from './types';
|
|||
/**
|
||||
* Convert a Gregorian `Date` to a Hijri date object.
|
||||
*
|
||||
* Follows date-fns conventions: the input `Date` is interpreted by its
|
||||
* **local calendar day** (year/month/date in the host timezone). This matches
|
||||
* how date-fns' own `format()` and field accessors work, so there are no
|
||||
* timezone surprises when chaining with other date-fns functions.
|
||||
*
|
||||
* Returns `null` when the date falls outside the calendar's supported range
|
||||
* (UAQ: 1318–1500 AH / 1900–2076 CE; FCNA extends slightly further).
|
||||
*
|
||||
* @example
|
||||
* // Use local-date constructor, not the string form "2025-03-01" (parses as UTC)
|
||||
* toHijriDate(new Date(2025, 2, 1)); // { hy: 1446, hm: 9, hd: 1 }
|
||||
*/
|
||||
export function toHijriDate(date: Date, options?: ConversionOptions): HijriDate | null {
|
||||
return coreToHijri(date, options);
|
||||
return coreToHijri(localDayToUtcSlot(date), options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a Hijri date to a Gregorian `Date`.
|
||||
*
|
||||
* The returned `Date` is set to midnight UTC of the equivalent Gregorian day.
|
||||
* Returns a **local-midnight** Date so that local field accessors
|
||||
* (`getFullYear`, `getMonth`, `getDate`) and date-fns' `format()` render the
|
||||
* intended calendar day on every host timezone.
|
||||
*
|
||||
* Round-trips exactly: `toHijriDate(fromHijriDate(y, m, d))` returns
|
||||
* `{ hy: y, hm: m, hd: d }` on every timezone.
|
||||
*
|
||||
* @throws {Error} If the Hijri date is invalid or outside the calendar's range.
|
||||
*
|
||||
* @example
|
||||
* const d = fromHijriDate(1446, 9, 1);
|
||||
* d.getFullYear(); // 2025
|
||||
* d.getMonth(); // 2 (March)
|
||||
* d.getDate(); // 1
|
||||
*/
|
||||
export function fromHijriDate(
|
||||
hy: number,
|
||||
|
|
@ -42,13 +83,13 @@ export function fromHijriDate(
|
|||
hd: number,
|
||||
options?: ConversionOptions,
|
||||
): Date {
|
||||
const result = coreToGregorian(hy, hm, hd, options);
|
||||
if (result === null) {
|
||||
throw new Error(
|
||||
`Hijri date ${hy}/${hm}/${hd} is invalid or outside the supported range.`,
|
||||
);
|
||||
const greg = coreToGregorian(hy, hm, hd, options);
|
||||
if (greg === null) {
|
||||
throw new Error(`Hijri date ${hy}/${hm}/${hd} is invalid or outside the supported range.`);
|
||||
}
|
||||
return result;
|
||||
// coreToGregorian returns UTC midnight; lift to local midnight so that
|
||||
// local field accessors and date-fns format() show the right calendar day.
|
||||
return new Date(greg.getUTCFullYear(), greg.getUTCMonth(), greg.getUTCDate());
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -77,28 +118,34 @@ export function isValidHijriDate(
|
|||
/**
|
||||
* Get the Hijri year for a Gregorian date.
|
||||
*
|
||||
* The input Date is interpreted by its **local calendar day** (date-fns convention).
|
||||
*
|
||||
* Returns `null` when the date is outside the supported range.
|
||||
*/
|
||||
export function getHijriYear(date: Date, options?: ConversionOptions): number | null {
|
||||
return coreToHijri(date, options)?.hy ?? null;
|
||||
return coreToHijri(localDayToUtcSlot(date), options)?.hy ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Hijri month (1–12) for a Gregorian date.
|
||||
*
|
||||
* The input Date is interpreted by its **local calendar day** (date-fns convention).
|
||||
*
|
||||
* Returns `null` when the date is outside the supported range.
|
||||
*/
|
||||
export function getHijriMonth(date: Date, options?: ConversionOptions): number | null {
|
||||
return coreToHijri(date, options)?.hm ?? null;
|
||||
return coreToHijri(localDayToUtcSlot(date), options)?.hm ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Hijri day of month (1–30) for a Gregorian date.
|
||||
*
|
||||
* The input Date is interpreted by its **local calendar day** (date-fns convention).
|
||||
*
|
||||
* Returns `null` when the date is outside the supported range.
|
||||
*/
|
||||
export function getHijriDay(date: Date, options?: ConversionOptions): number | null {
|
||||
return coreToHijri(date, options)?.hd ?? null;
|
||||
return coreToHijri(localDayToUtcSlot(date), options)?.hd ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -106,11 +153,7 @@ export function getHijriDay(date: Date, options?: ConversionOptions): number | n
|
|||
*
|
||||
* @throws {RangeError} If the year is outside the calendar's supported range.
|
||||
*/
|
||||
export function getDaysInHijriMonth(
|
||||
hy: number,
|
||||
hm: number,
|
||||
options?: ConversionOptions,
|
||||
): number {
|
||||
export function getDaysInHijriMonth(hy: number, hm: number, options?: ConversionOptions): number {
|
||||
return coreDaysInHijriMonth(hy, hm, options);
|
||||
}
|
||||
|
||||
|
|
@ -128,31 +171,36 @@ export function getDaysInHijriMonth(
|
|||
*/
|
||||
export function getHijriMonthName(
|
||||
hm: number,
|
||||
length: 'long' | 'medium' | 'short' = 'long',
|
||||
length: "long" | "medium" | "short" = "long",
|
||||
): string {
|
||||
if (hm < 1 || hm > 12) {
|
||||
throw new RangeError(`Hijri month must be 1–12, got ${hm}.`);
|
||||
}
|
||||
const idx = hm - 1;
|
||||
if (length === 'medium') return hmMedium[idx];
|
||||
if (length === 'short') return hmShort[idx];
|
||||
return hmLong[idx];
|
||||
// Non-null: hm validated 1-12 above; idx is always 0-11, within all hm* array bounds.
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
if (length === "medium") return hmMedium[idx]!;
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
if (length === "short") return hmShort[idx]!;
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
return hmLong[idx]!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Arabic weekday name for a Gregorian date.
|
||||
*
|
||||
* Uses `Date.getDay()` (0 = Sunday, 6 = Saturday) as the index.
|
||||
* `getDay()` reads the local weekday, which is correct — weekday display
|
||||
* follows the host's local calendar day just like date-fns.
|
||||
*
|
||||
* @param date - Any Gregorian `Date`.
|
||||
* @param length - `'long'` (default) or `'short'`.
|
||||
*/
|
||||
export function getHijriWeekdayName(
|
||||
date: Date,
|
||||
length: 'long' | 'short' = 'long',
|
||||
): string {
|
||||
export function getHijriWeekdayName(date: Date, length: "long" | "short" = "long"): string {
|
||||
const day = date.getDay(); // 0–6
|
||||
return length === 'short' ? hwShort[day] : hwLong[day];
|
||||
// Non-null: day is always 0-6 from getDay(), within hw* array bounds.
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
return length === "short" ? hwShort[day]! : hwLong[day]!;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -165,6 +213,9 @@ const TOKEN_RE = /iYYYY|iYY|iMMMM|iMMM|iMM|iM|iDD|iD|iEEEE|iEEE|iE|ioooo|iooo/g;
|
|||
/**
|
||||
* Format a Gregorian date using Hijri calendar tokens.
|
||||
*
|
||||
* The input Date is interpreted by its **local calendar day** (date-fns convention),
|
||||
* matching the behavior of date-fns' own `format()`.
|
||||
*
|
||||
* Supported tokens:
|
||||
*
|
||||
* | Token | Output | Example |
|
||||
|
|
@ -190,49 +241,52 @@ export function formatHijriDate(
|
|||
formatStr: string,
|
||||
options?: ConversionOptions,
|
||||
): string {
|
||||
const h = coreToHijri(date, options);
|
||||
if (!h) return '';
|
||||
const h = coreToHijri(localDayToUtcSlot(date), options);
|
||||
if (!h) return "";
|
||||
|
||||
const day = date.getDay(); // 0–6
|
||||
const day = date.getDay(); // 0–6 local weekday — correct for display
|
||||
|
||||
return formatStr.replace(TOKEN_RE, (token) => {
|
||||
return formatStr.replace(TOKEN_RE, (token): string => {
|
||||
switch (token) {
|
||||
case 'iYYYY': return String(h.hy);
|
||||
case 'iYY': return String(h.hy).slice(-2).padStart(2, '0');
|
||||
case 'iMMMM': return hmLong[h.hm - 1];
|
||||
case 'iMMM': return hmMedium[h.hm - 1];
|
||||
case 'iMM': return String(h.hm).padStart(2, '0');
|
||||
case 'iM': return String(h.hm);
|
||||
case 'iDD': return String(h.hd).padStart(2, '0');
|
||||
case 'iD': return String(h.hd);
|
||||
case 'iEEEE': return hwLong[day];
|
||||
case 'iEEE': return hwShort[day];
|
||||
case 'iE': return String(hwNumeric[day]);
|
||||
case 'ioooo': return 'AH';
|
||||
case 'iooo': return 'AH';
|
||||
default: return token;
|
||||
case "iYYYY":
|
||||
return String(h.hy);
|
||||
case "iYY":
|
||||
return String(h.hy).slice(-2).padStart(2, "0");
|
||||
case "iMMMM":
|
||||
// Non-null: hm is a valid Hijri month 1-12; index hm-1 is within hmLong bounds.
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
return hmLong[h.hm - 1]!;
|
||||
case "iMMM":
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
return hmMedium[h.hm - 1]!;
|
||||
case "iMM":
|
||||
return String(h.hm).padStart(2, "0");
|
||||
case "iM":
|
||||
return String(h.hm);
|
||||
case "iDD":
|
||||
return String(h.hd).padStart(2, "0");
|
||||
case "iD":
|
||||
return String(h.hd);
|
||||
case "iEEEE":
|
||||
// Non-null: day is always 0-6 from getDay(), within hwLong bounds.
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
return hwLong[day]!;
|
||||
case "iEEE":
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
return hwShort[day]!;
|
||||
case "iE":
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
return String(hwNumeric[day]!);
|
||||
case "ioooo":
|
||||
return "AH";
|
||||
case "iooo":
|
||||
return "AH";
|
||||
default:
|
||||
return token;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Internal helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* `coreToGregorian` returns a UTC-midnight Date. When `coreToHijri` is then
|
||||
* called on that Date, it normalises using local year/month/day components
|
||||
* (`getFullYear`, `getMonth`, `getDate`). In timezones west of UTC the local
|
||||
* date of a UTC-midnight instant is the *previous* calendar day, which causes
|
||||
* the round-trip to drift by one day.
|
||||
*
|
||||
* This helper converts a UTC-midnight Date to a local-noon Date so that local
|
||||
* calendar components always match the intended Gregorian date.
|
||||
*/
|
||||
function utcMidnightToLocalNoon(d: Date): Date {
|
||||
return new Date(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), 12);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Arithmetic
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -240,57 +294,55 @@ function utcMidnightToLocalNoon(d: Date): Date {
|
|||
/**
|
||||
* Add a number of Hijri months to a Gregorian date.
|
||||
*
|
||||
* The input Date is interpreted by its **local calendar day** (date-fns convention).
|
||||
* Returns a **local-midnight** Date.
|
||||
*
|
||||
* Handles year rollover automatically. Month addition wraps at month 12 and
|
||||
* increments the year. If the result's month has fewer days than the original
|
||||
* day, the day is clamped to the last day of the new month.
|
||||
*
|
||||
* @throws {Error} If the resulting Hijri date is outside the supported range.
|
||||
*/
|
||||
export function addHijriMonths(
|
||||
date: Date,
|
||||
months: number,
|
||||
options?: ConversionOptions,
|
||||
): Date {
|
||||
const h = coreToHijri(date, options);
|
||||
export function addHijriMonths(date: Date, months: number, options?: ConversionOptions): Date {
|
||||
const h = coreToHijri(localDayToUtcSlot(date), options);
|
||||
if (!h) {
|
||||
throw new Error('Date is outside the supported Hijri calendar range.');
|
||||
throw new Error("Date is outside the supported Hijri calendar range.");
|
||||
}
|
||||
|
||||
// Total months from epoch: 0-based
|
||||
const totalMonths = (h.hy - 1) * 12 + (h.hm - 1) + months;
|
||||
const newYear = Math.floor(totalMonths / 12) + 1;
|
||||
const newYear = Math.floor(totalMonths / 12) + 1;
|
||||
const newMonth = (((totalMonths % 12) + 12) % 12) + 1;
|
||||
|
||||
// Clamp day to the target month's length
|
||||
const maxDay = coreDaysInHijriMonth(newYear, newMonth, options);
|
||||
const newDay = Math.min(h.hd, maxDay);
|
||||
|
||||
return utcMidnightToLocalNoon(fromHijriDate(newYear, newMonth, newDay, options));
|
||||
return fromHijriDate(newYear, newMonth, newDay, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a number of Hijri years to a Gregorian date.
|
||||
*
|
||||
* If the resulting year has a shorter Ramadan (or any month) than the original
|
||||
* day, the day is clamped to the last day of that month.
|
||||
* The input Date is interpreted by its **local calendar day** (date-fns convention).
|
||||
* Returns a **local-midnight** Date.
|
||||
*
|
||||
* If the resulting year has a shorter month than the original day, the day is
|
||||
* clamped to the last day of that month.
|
||||
*
|
||||
* @throws {Error} If the resulting Hijri date is outside the supported range.
|
||||
*/
|
||||
export function addHijriYears(
|
||||
date: Date,
|
||||
years: number,
|
||||
options?: ConversionOptions,
|
||||
): Date {
|
||||
const h = coreToHijri(date, options);
|
||||
export function addHijriYears(date: Date, years: number, options?: ConversionOptions): Date {
|
||||
const h = coreToHijri(localDayToUtcSlot(date), options);
|
||||
if (!h) {
|
||||
throw new Error('Date is outside the supported Hijri calendar range.');
|
||||
throw new Error("Date is outside the supported Hijri calendar range.");
|
||||
}
|
||||
|
||||
const newYear = h.hy + years;
|
||||
const maxDay = coreDaysInHijriMonth(newYear, h.hm, options);
|
||||
const newDay = Math.min(h.hd, maxDay);
|
||||
const maxDay = coreDaysInHijriMonth(newYear, h.hm, options);
|
||||
const newDay = Math.min(h.hd, maxDay);
|
||||
|
||||
return utcMidnightToLocalNoon(fromHijriDate(newYear, h.hm, newDay, options));
|
||||
return fromHijriDate(newYear, h.hm, newDay, options);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -300,28 +352,34 @@ export function addHijriYears(
|
|||
/**
|
||||
* Get the first day of the Hijri month that contains the given date.
|
||||
*
|
||||
* The input Date is interpreted by its **local calendar day** (date-fns convention).
|
||||
* Returns a **local-midnight** Date.
|
||||
*
|
||||
* @throws {Error} If the date is outside the supported range.
|
||||
*/
|
||||
export function startOfHijriMonth(date: Date, options?: ConversionOptions): Date {
|
||||
const h = coreToHijri(date, options);
|
||||
const h = coreToHijri(localDayToUtcSlot(date), options);
|
||||
if (!h) {
|
||||
throw new Error('Date is outside the supported Hijri calendar range.');
|
||||
throw new Error("Date is outside the supported Hijri calendar range.");
|
||||
}
|
||||
return utcMidnightToLocalNoon(fromHijriDate(h.hy, h.hm, 1, options));
|
||||
return fromHijriDate(h.hy, h.hm, 1, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last day of the Hijri month that contains the given date.
|
||||
*
|
||||
* The input Date is interpreted by its **local calendar day** (date-fns convention).
|
||||
* Returns a **local-midnight** Date.
|
||||
*
|
||||
* @throws {Error} If the date is outside the supported range.
|
||||
*/
|
||||
export function endOfHijriMonth(date: Date, options?: ConversionOptions): Date {
|
||||
const h = coreToHijri(date, options);
|
||||
const h = coreToHijri(localDayToUtcSlot(date), options);
|
||||
if (!h) {
|
||||
throw new Error('Date is outside the supported Hijri calendar range.');
|
||||
throw new Error("Date is outside the supported Hijri calendar range.");
|
||||
}
|
||||
const lastDay = coreDaysInHijriMonth(h.hy, h.hm, options);
|
||||
return utcMidnightToLocalNoon(fromHijriDate(h.hy, h.hm, lastDay, options));
|
||||
return fromHijriDate(h.hy, h.hm, lastDay, options);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -331,15 +389,13 @@ export function endOfHijriMonth(date: Date, options?: ConversionOptions): Date {
|
|||
/**
|
||||
* Check whether two Gregorian dates fall in the same Hijri month.
|
||||
*
|
||||
* Both input Dates are interpreted by their **local calendar days** (date-fns convention).
|
||||
*
|
||||
* Returns `false` if either date is outside the supported range.
|
||||
*/
|
||||
export function isSameHijriMonth(
|
||||
dateA: Date,
|
||||
dateB: Date,
|
||||
options?: ConversionOptions,
|
||||
): boolean {
|
||||
const a = coreToHijri(dateA, options);
|
||||
const b = coreToHijri(dateB, options);
|
||||
export function isSameHijriMonth(dateA: Date, dateB: Date, options?: ConversionOptions): boolean {
|
||||
const a = coreToHijri(localDayToUtcSlot(dateA), options);
|
||||
const b = coreToHijri(localDayToUtcSlot(dateB), options);
|
||||
if (!a || !b) return false;
|
||||
return a.hy === b.hy && a.hm === b.hm;
|
||||
}
|
||||
|
|
@ -347,15 +403,13 @@ export function isSameHijriMonth(
|
|||
/**
|
||||
* Check whether two Gregorian dates fall in the same Hijri year.
|
||||
*
|
||||
* Both input Dates are interpreted by their **local calendar days** (date-fns convention).
|
||||
*
|
||||
* Returns `false` if either date is outside the supported range.
|
||||
*/
|
||||
export function isSameHijriYear(
|
||||
dateA: Date,
|
||||
dateB: Date,
|
||||
options?: ConversionOptions,
|
||||
): boolean {
|
||||
const a = coreToHijri(dateA, options);
|
||||
const b = coreToHijri(dateB, options);
|
||||
export function isSameHijriYear(dateA: Date, dateB: Date, options?: ConversionOptions): boolean {
|
||||
const a = coreToHijri(localDayToUtcSlot(dateA), options);
|
||||
const b = coreToHijri(localDayToUtcSlot(dateB), options);
|
||||
if (!a || !b) return false;
|
||||
return a.hy === b.hy;
|
||||
}
|
||||
|
|
@ -367,12 +421,14 @@ export function isSameHijriYear(
|
|||
/**
|
||||
* Get the Hijri quarter (1–4) for a Gregorian date.
|
||||
*
|
||||
* The input Date is interpreted by its **local calendar day** (date-fns convention).
|
||||
*
|
||||
* Months 1–3 = Q1, 4–6 = Q2, 7–9 = Q3, 10–12 = Q4.
|
||||
*
|
||||
* Returns `null` when the date is outside the supported range.
|
||||
*/
|
||||
export function getHijriQuarter(date: Date, options?: ConversionOptions): number | null {
|
||||
const h = coreToHijri(date, options);
|
||||
const h = coreToHijri(localDayToUtcSlot(date), options);
|
||||
if (!h) return null;
|
||||
return Math.ceil(h.hm / 3);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export type { HijriDate, CalendarEngine, ConversionOptions } from 'hijri-core';
|
||||
export type { HijriDate, CalendarEngine, ConversionOptions } from "hijri-core";
|
||||
|
|
|
|||
100
test-cjs.cjs
100
test-cjs.cjs
|
|
@ -1,5 +1,6 @@
|
|||
'use strict';
|
||||
|
||||
const { describe, it } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const {
|
||||
toHijriDate,
|
||||
|
|
@ -12,71 +13,64 @@ const {
|
|||
getHijriDay,
|
||||
} = require('./dist/index.cjs');
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
function test(name, fn) {
|
||||
try {
|
||||
fn();
|
||||
console.log(`[${name}]... PASS`);
|
||||
passed++;
|
||||
} catch (err) {
|
||||
console.error(`[${name}]... FAIL: ${err.message}`);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
const REF = new Date(2023, 2, 23, 12); // 1 Ramadan 1444
|
||||
|
||||
test('CJS: toHijriDate returns correct HijriDate', () => {
|
||||
const h = toHijriDate(REF);
|
||||
assert.ok(h !== null);
|
||||
assert.equal(h.hy, 1444);
|
||||
assert.equal(h.hm, 9);
|
||||
assert.equal(h.hd, 1);
|
||||
describe('CJS: toHijriDate', () => {
|
||||
it('returns correct HijriDate', () => {
|
||||
const h = toHijriDate(REF);
|
||||
assert.ok(h !== null);
|
||||
assert.equal(h.hy, 1444);
|
||||
assert.equal(h.hm, 9);
|
||||
assert.equal(h.hd, 1);
|
||||
});
|
||||
});
|
||||
|
||||
test('CJS: fromHijriDate converts to correct Gregorian date', () => {
|
||||
const d = fromHijriDate(1444, 9, 1);
|
||||
assert.equal(d.getUTCFullYear(), 2023);
|
||||
assert.equal(d.getUTCMonth(), 2);
|
||||
assert.equal(d.getUTCDate(), 23);
|
||||
describe('CJS: fromHijriDate', () => {
|
||||
it('converts to correct Gregorian date (local midnight)', () => {
|
||||
const d = fromHijriDate(1444, 9, 1);
|
||||
// Returns local midnight — use local accessors, not UTC
|
||||
assert.equal(d.getFullYear(), 2023);
|
||||
assert.equal(d.getMonth(), 2);
|
||||
assert.equal(d.getDate(), 23);
|
||||
});
|
||||
});
|
||||
|
||||
test('CJS: isValidHijriDate true for valid date', () => {
|
||||
assert.equal(isValidHijriDate(1444, 9, 1), true);
|
||||
describe('CJS: isValidHijriDate', () => {
|
||||
it('true for valid date', () => {
|
||||
assert.equal(isValidHijriDate(1444, 9, 1), true);
|
||||
});
|
||||
|
||||
it('false for invalid month', () => {
|
||||
assert.equal(isValidHijriDate(1444, 13, 1), false);
|
||||
});
|
||||
});
|
||||
|
||||
test('CJS: isValidHijriDate false for invalid month', () => {
|
||||
assert.equal(isValidHijriDate(1444, 13, 1), false);
|
||||
describe('CJS: getHijriMonthName', () => {
|
||||
it('long', () => {
|
||||
assert.equal(getHijriMonthName(9), 'Ramadan');
|
||||
});
|
||||
|
||||
it('short', () => {
|
||||
assert.equal(getHijriMonthName(9, 'short'), 'Ram');
|
||||
});
|
||||
});
|
||||
|
||||
test('CJS: getHijriMonthName long', () => {
|
||||
assert.equal(getHijriMonthName(9), 'Ramadan');
|
||||
describe('CJS: formatHijriDate', () => {
|
||||
it('iYYYY-iMM-iDD', () => {
|
||||
assert.equal(formatHijriDate(REF, 'iYYYY-iMM-iDD'), '1444-09-01');
|
||||
});
|
||||
});
|
||||
|
||||
test('CJS: getHijriMonthName short', () => {
|
||||
assert.equal(getHijriMonthName(9, 'short'), 'Ram');
|
||||
});
|
||||
describe('CJS: field getters', () => {
|
||||
it('getHijriYear', () => {
|
||||
assert.equal(getHijriYear(REF), 1444);
|
||||
});
|
||||
|
||||
test('CJS: formatHijriDate iYYYY-iMM-iDD', () => {
|
||||
assert.equal(formatHijriDate(REF, 'iYYYY-iMM-iDD'), '1444-09-01');
|
||||
});
|
||||
it('getHijriMonth', () => {
|
||||
assert.equal(getHijriMonth(REF), 9);
|
||||
});
|
||||
|
||||
test('CJS: getHijriYear', () => {
|
||||
assert.equal(getHijriYear(REF), 1444);
|
||||
it('getHijriDay', () => {
|
||||
assert.equal(getHijriDay(REF), 1);
|
||||
});
|
||||
});
|
||||
|
||||
test('CJS: getHijriMonth', () => {
|
||||
assert.equal(getHijriMonth(REF), 9);
|
||||
});
|
||||
|
||||
test('CJS: getHijriDay', () => {
|
||||
assert.equal(getHijriDay(REF), 1);
|
||||
});
|
||||
|
||||
const total = passed + failed;
|
||||
console.log(`\n${passed}/${total} tests passed`);
|
||||
if (failed > 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
|
|
|||
623
test.mjs
623
test.mjs
|
|
@ -1,3 +1,4 @@
|
|||
import { describe, it } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
toHijriDate,
|
||||
|
|
@ -19,365 +20,327 @@ import {
|
|||
getHijriQuarter,
|
||||
} from './dist/index.mjs';
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
function test(name, fn) {
|
||||
try {
|
||||
fn();
|
||||
console.log(`[${name}]... PASS`);
|
||||
passed++;
|
||||
} catch (err) {
|
||||
console.error(`[${name}]... FAIL: ${err.message}`);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// toHijriDate
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test('toHijriDate: 1 Ramadan 1444', () => {
|
||||
const h = toHijriDate(new Date(2023, 2, 23, 12));
|
||||
assert.ok(h !== null, 'expected non-null');
|
||||
assert.equal(h.hy, 1444);
|
||||
assert.equal(h.hm, 9);
|
||||
assert.equal(h.hd, 1);
|
||||
});
|
||||
|
||||
test('toHijriDate: 1 Muharram 1446', () => {
|
||||
const h = toHijriDate(new Date(2024, 6, 7, 12));
|
||||
assert.ok(h !== null, 'expected non-null');
|
||||
assert.equal(h.hy, 1446);
|
||||
assert.equal(h.hm, 1);
|
||||
assert.equal(h.hd, 1);
|
||||
});
|
||||
|
||||
test('toHijriDate: out of range returns null', () => {
|
||||
const h = toHijriDate(new Date(1800, 0, 1));
|
||||
assert.equal(h, null);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// fromHijriDate
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test('fromHijriDate: 1 Ramadan 1444 -> 2023-03-23', () => {
|
||||
const d = fromHijriDate(1444, 9, 1);
|
||||
assert.equal(d.getUTCFullYear(), 2023);
|
||||
assert.equal(d.getUTCMonth(), 2); // March
|
||||
assert.equal(d.getUTCDate(), 23);
|
||||
});
|
||||
|
||||
test('fromHijriDate: 1 Muharram 1446 -> 2024-07-07', () => {
|
||||
const d = fromHijriDate(1446, 1, 1);
|
||||
assert.equal(d.getUTCFullYear(), 2024);
|
||||
assert.equal(d.getUTCMonth(), 6); // July
|
||||
assert.equal(d.getUTCDate(), 7);
|
||||
});
|
||||
|
||||
test('fromHijriDate: throws on invalid month', () => {
|
||||
assert.throws(() => fromHijriDate(1444, 13, 1), /invalid|range/i);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// isValidHijriDate
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test('isValidHijriDate: valid date', () => {
|
||||
assert.equal(isValidHijriDate(1444, 9, 1), true);
|
||||
});
|
||||
|
||||
test('isValidHijriDate: invalid month 13', () => {
|
||||
assert.equal(isValidHijriDate(1444, 13, 1), false);
|
||||
});
|
||||
|
||||
test('isValidHijriDate: day 0 is invalid', () => {
|
||||
assert.equal(isValidHijriDate(1444, 9, 0), false);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Field getters
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const REF = new Date(2023, 2, 23, 12); // 1 Ramadan 1444
|
||||
|
||||
test('getHijriYear', () => {
|
||||
assert.equal(getHijriYear(REF), 1444);
|
||||
describe('toHijriDate', () => {
|
||||
it('1 Ramadan 1444', () => {
|
||||
const h = toHijriDate(new Date(2023, 2, 23, 12));
|
||||
assert.ok(h !== null, 'expected non-null');
|
||||
assert.equal(h.hy, 1444);
|
||||
assert.equal(h.hm, 9);
|
||||
assert.equal(h.hd, 1);
|
||||
});
|
||||
|
||||
it('1 Muharram 1446', () => {
|
||||
const h = toHijriDate(new Date(2024, 6, 7, 12));
|
||||
assert.ok(h !== null, 'expected non-null');
|
||||
assert.equal(h.hy, 1446);
|
||||
assert.equal(h.hm, 1);
|
||||
assert.equal(h.hd, 1);
|
||||
});
|
||||
|
||||
it('out of range returns null', () => {
|
||||
const h = toHijriDate(new Date(1800, 0, 1));
|
||||
assert.equal(h, null);
|
||||
});
|
||||
|
||||
it('toHijriDate(new Date(2025, 2, 1, 12)) -> {1446, 9, 1}', () => {
|
||||
// Local-noon: verifies local-day interpretation ignores the time component
|
||||
const h = toHijriDate(new Date(2025, 2, 1, 12));
|
||||
assert.ok(h !== null, 'expected non-null');
|
||||
assert.equal(h.hy, 1446);
|
||||
assert.equal(h.hm, 9);
|
||||
assert.equal(h.hd, 1);
|
||||
});
|
||||
});
|
||||
|
||||
test('getHijriMonth', () => {
|
||||
assert.equal(getHijriMonth(REF), 9);
|
||||
describe('fromHijriDate', () => {
|
||||
it('1 Ramadan 1444 -> local 2023-03-23', () => {
|
||||
const d = fromHijriDate(1444, 9, 1);
|
||||
// Returns local midnight: local accessors show the intended calendar day
|
||||
assert.equal(d.getFullYear(), 2023);
|
||||
assert.equal(d.getMonth(), 2);
|
||||
assert.equal(d.getDate(), 23);
|
||||
});
|
||||
|
||||
it('1 Muharram 1446 -> local 2024-07-07', () => {
|
||||
const d = fromHijriDate(1446, 1, 1);
|
||||
assert.equal(d.getFullYear(), 2024);
|
||||
assert.equal(d.getMonth(), 6);
|
||||
assert.equal(d.getDate(), 7);
|
||||
});
|
||||
|
||||
it('round-trip: toHijriDate(fromHijriDate(1446, 9, 1)) === {1446, 9, 1}', () => {
|
||||
const d = fromHijriDate(1446, 9, 1);
|
||||
const h = toHijriDate(d);
|
||||
assert.ok(h !== null, 'expected non-null round-trip result');
|
||||
assert.equal(h.hy, 1446);
|
||||
assert.equal(h.hm, 9);
|
||||
assert.equal(h.hd, 1);
|
||||
});
|
||||
|
||||
it('fromHijriDate(1446,9,1) local accessors show 2025-03-01', () => {
|
||||
const d = fromHijriDate(1446, 9, 1);
|
||||
// Local accessors — not toISOString() — are the correct API for this adapter
|
||||
assert.equal(d.getFullYear(), 2025);
|
||||
assert.equal(d.getMonth(), 2); // March
|
||||
assert.equal(d.getDate(), 1);
|
||||
});
|
||||
|
||||
it('throws on invalid month', () => {
|
||||
assert.throws(() => fromHijriDate(1444, 13, 1), /invalid|range/i);
|
||||
});
|
||||
});
|
||||
|
||||
test('getHijriDay', () => {
|
||||
assert.equal(getHijriDay(REF), 1);
|
||||
describe('isValidHijriDate', () => {
|
||||
it('valid date', () => {
|
||||
assert.equal(isValidHijriDate(1444, 9, 1), true);
|
||||
});
|
||||
|
||||
it('invalid month 13', () => {
|
||||
assert.equal(isValidHijriDate(1444, 13, 1), false);
|
||||
});
|
||||
|
||||
it('day 0 is invalid', () => {
|
||||
assert.equal(isValidHijriDate(1444, 9, 0), false);
|
||||
});
|
||||
});
|
||||
|
||||
test('getHijriYear: out of range returns null', () => {
|
||||
assert.equal(getHijriYear(new Date(1800, 0, 1)), null);
|
||||
describe('field getters', () => {
|
||||
it('getHijriYear', () => {
|
||||
assert.equal(getHijriYear(REF), 1444);
|
||||
});
|
||||
|
||||
it('getHijriMonth', () => {
|
||||
assert.equal(getHijriMonth(REF), 9);
|
||||
});
|
||||
|
||||
it('getHijriDay', () => {
|
||||
assert.equal(getHijriDay(REF), 1);
|
||||
});
|
||||
|
||||
it('getHijriYear: out of range returns null', () => {
|
||||
assert.equal(getHijriYear(new Date(1800, 0, 1)), null);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// getDaysInHijriMonth
|
||||
// ---------------------------------------------------------------------------
|
||||
describe('getDaysInHijriMonth', () => {
|
||||
it('Ramadan 1444', () => {
|
||||
const days = getDaysInHijriMonth(1444, 9);
|
||||
assert.ok(days === 29 || days === 30, `expected 29 or 30, got ${days}`);
|
||||
});
|
||||
|
||||
test('getDaysInHijriMonth: Ramadan 1444', () => {
|
||||
const days = getDaysInHijriMonth(1444, 9);
|
||||
// Must be either 29 or 30
|
||||
assert.ok(days === 29 || days === 30, `expected 29 or 30, got ${days}`);
|
||||
it('month 1 of 1444', () => {
|
||||
const days = getDaysInHijriMonth(1444, 1);
|
||||
assert.ok(days === 29 || days === 30, `expected 29 or 30, got ${days}`);
|
||||
});
|
||||
});
|
||||
|
||||
test('getDaysInHijriMonth: month 1 of 1444', () => {
|
||||
const days = getDaysInHijriMonth(1444, 1);
|
||||
assert.ok(days === 29 || days === 30, `expected 29 or 30, got ${days}`);
|
||||
describe('getHijriMonthName', () => {
|
||||
it('long (default)', () => {
|
||||
assert.equal(getHijriMonthName(9), 'Ramadan');
|
||||
});
|
||||
|
||||
it('medium', () => {
|
||||
assert.equal(getHijriMonthName(9, 'medium'), 'Ramadan');
|
||||
});
|
||||
|
||||
it('short', () => {
|
||||
assert.equal(getHijriMonthName(9, 'short'), 'Ram');
|
||||
});
|
||||
|
||||
it('Muharram long', () => {
|
||||
assert.equal(getHijriMonthName(1), 'Muharram');
|
||||
});
|
||||
|
||||
it('Dhul Hijjah long', () => {
|
||||
assert.equal(getHijriMonthName(12), 'Dhul Hijjah');
|
||||
});
|
||||
|
||||
it('throws on month 0', () => {
|
||||
assert.throws(() => getHijriMonthName(0), RangeError);
|
||||
});
|
||||
|
||||
it('throws on month 13', () => {
|
||||
assert.throws(() => getHijriMonthName(13), RangeError);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// getHijriMonthName
|
||||
// ---------------------------------------------------------------------------
|
||||
describe('getHijriWeekdayName', () => {
|
||||
it('Thursday long', () => {
|
||||
assert.equal(getHijriWeekdayName(new Date(2023, 2, 23)), 'Yawm al-Khamis');
|
||||
});
|
||||
|
||||
test('getHijriMonthName: long (default)', () => {
|
||||
assert.equal(getHijriMonthName(9), 'Ramadan');
|
||||
it('Thursday short', () => {
|
||||
assert.equal(getHijriWeekdayName(new Date(2023, 2, 23), 'short'), 'Kham');
|
||||
});
|
||||
});
|
||||
|
||||
test('getHijriMonthName: medium', () => {
|
||||
assert.equal(getHijriMonthName(9, 'medium'), 'Ramadan');
|
||||
describe('formatHijriDate', () => {
|
||||
it('iYYYY-iMM-iDD', () => {
|
||||
assert.equal(formatHijriDate(REF, 'iYYYY-iMM-iDD'), '1444-09-01');
|
||||
});
|
||||
|
||||
it('iMMMM', () => {
|
||||
assert.equal(formatHijriDate(REF, 'iMMMM'), 'Ramadan');
|
||||
});
|
||||
|
||||
it('iEEEE', () => {
|
||||
assert.equal(formatHijriDate(new Date(2023, 2, 23), 'iEEEE'), 'Yawm al-Khamis');
|
||||
});
|
||||
|
||||
it('iEEE', () => {
|
||||
assert.equal(formatHijriDate(new Date(2023, 2, 23), 'iEEE'), 'Kham');
|
||||
});
|
||||
|
||||
it('ioooo era', () => {
|
||||
assert.equal(formatHijriDate(REF, 'ioooo'), 'AH');
|
||||
});
|
||||
|
||||
it('iooo era', () => {
|
||||
assert.equal(formatHijriDate(REF, 'iooo'), 'AH');
|
||||
});
|
||||
|
||||
it('iYY two-digit year', () => {
|
||||
assert.equal(formatHijriDate(REF, 'iYY'), '44');
|
||||
});
|
||||
|
||||
it('iMMM medium month', () => {
|
||||
assert.equal(formatHijriDate(REF, 'iMMM'), 'Ramadan');
|
||||
});
|
||||
|
||||
it('iM bare month', () => {
|
||||
assert.equal(formatHijriDate(REF, 'iM'), '9');
|
||||
});
|
||||
|
||||
it('iD bare day', () => {
|
||||
assert.equal(formatHijriDate(REF, 'iD'), '1');
|
||||
});
|
||||
|
||||
it('iE numeric weekday (Thursday = 5)', () => {
|
||||
assert.equal(formatHijriDate(new Date(2023, 2, 23), 'iE'), '5');
|
||||
});
|
||||
|
||||
it('out of range returns empty string', () => {
|
||||
assert.equal(formatHijriDate(new Date(1800, 0, 1), 'iYYYY-iMM-iDD'), '');
|
||||
});
|
||||
|
||||
it('mixed literal and tokens', () => {
|
||||
const result = formatHijriDate(REF, 'iD iMMMM iYYYY ioooo');
|
||||
assert.equal(result, '1 Ramadan 1444 AH');
|
||||
});
|
||||
});
|
||||
|
||||
test('getHijriMonthName: short', () => {
|
||||
assert.equal(getHijriMonthName(9, 'short'), 'Ram');
|
||||
describe('addHijriMonths', () => {
|
||||
it('+1 from Ramadan -> Shawwal', () => {
|
||||
const result = toHijriDate(addHijriMonths(REF, 1));
|
||||
assert.ok(result !== null);
|
||||
assert.equal(result.hy, 1444);
|
||||
assert.equal(result.hm, 10);
|
||||
});
|
||||
|
||||
it('+3 from month 10 -> wraps to next year', () => {
|
||||
const dec = new Date(2023, 3, 21, 12);
|
||||
const result = toHijriDate(addHijriMonths(dec, 3));
|
||||
assert.ok(result !== null);
|
||||
assert.equal(result.hy, 1445);
|
||||
});
|
||||
|
||||
it('+0 is identity', () => {
|
||||
const result = toHijriDate(addHijriMonths(REF, 0));
|
||||
assert.ok(result !== null);
|
||||
assert.equal(result.hy, 1444);
|
||||
assert.equal(result.hm, 9);
|
||||
assert.equal(result.hd, 1);
|
||||
});
|
||||
|
||||
it('-1 from Ramadan -> Shaban', () => {
|
||||
const result = toHijriDate(addHijriMonths(REF, -1));
|
||||
assert.ok(result !== null);
|
||||
assert.equal(result.hm, 8);
|
||||
});
|
||||
});
|
||||
|
||||
test('getHijriMonthName: Muharram long', () => {
|
||||
assert.equal(getHijriMonthName(1), 'Muharram');
|
||||
describe('addHijriYears', () => {
|
||||
it('+1 from Ramadan 1444 -> Ramadan 1445', () => {
|
||||
const result = toHijriDate(addHijriYears(REF, 1));
|
||||
assert.ok(result !== null);
|
||||
assert.equal(result.hy, 1445);
|
||||
assert.equal(result.hm, 9);
|
||||
});
|
||||
|
||||
it('-1 from Ramadan 1444 -> Ramadan 1443', () => {
|
||||
const result = toHijriDate(addHijriYears(REF, -1));
|
||||
assert.ok(result !== null);
|
||||
assert.equal(result.hy, 1443);
|
||||
assert.equal(result.hm, 9);
|
||||
});
|
||||
});
|
||||
|
||||
test('getHijriMonthName: Dhul Hijjah long', () => {
|
||||
assert.equal(getHijriMonthName(12), 'Dhul Hijjah');
|
||||
describe('startOfHijriMonth / endOfHijriMonth', () => {
|
||||
it('startOfHijriMonth: 1 Ramadan 1444 = 2023-03-23', () => {
|
||||
const start = startOfHijriMonth(REF);
|
||||
assert.equal(start.getFullYear(), 2023);
|
||||
assert.equal(start.getMonth(), 2);
|
||||
assert.equal(start.getDate(), 23);
|
||||
});
|
||||
|
||||
it('endOfHijriMonth: last day of Ramadan 1444', () => {
|
||||
const end = toHijriDate(endOfHijriMonth(REF));
|
||||
assert.ok(end !== null);
|
||||
assert.equal(end.hy, 1444);
|
||||
assert.equal(end.hm, 9);
|
||||
assert.ok(end.hd === 29 || end.hd === 30, `expected 29 or 30, got ${end.hd}`);
|
||||
});
|
||||
});
|
||||
|
||||
test('getHijriMonthName: throws on month 0', () => {
|
||||
assert.throws(() => getHijriMonthName(0), RangeError);
|
||||
describe('isSameHijriMonth / isSameHijriYear', () => {
|
||||
it('both in Ramadan 1444', () => {
|
||||
assert.equal(isSameHijriMonth(new Date(2023, 2, 23, 12), new Date(2023, 3, 10, 12)), true);
|
||||
});
|
||||
|
||||
it('different months', () => {
|
||||
assert.equal(isSameHijriMonth(new Date(2023, 2, 23, 12), new Date(2023, 4, 1, 12)), false);
|
||||
});
|
||||
|
||||
it('out of range returns false', () => {
|
||||
assert.equal(isSameHijriMonth(new Date(1800, 0, 1), new Date(2023, 2, 23, 12)), false);
|
||||
});
|
||||
|
||||
it('both in 1444', () => {
|
||||
assert.equal(isSameHijriYear(new Date(2023, 2, 23, 12), new Date(2023, 1, 10, 12)), true);
|
||||
});
|
||||
|
||||
it('different years', () => {
|
||||
assert.equal(isSameHijriYear(new Date(2023, 2, 23, 12), new Date(2024, 6, 7, 12)), false);
|
||||
});
|
||||
});
|
||||
|
||||
test('getHijriMonthName: throws on month 13', () => {
|
||||
assert.throws(() => getHijriMonthName(13), RangeError);
|
||||
describe('getHijriQuarter', () => {
|
||||
it('month 9 = Q3', () => {
|
||||
assert.equal(getHijriQuarter(REF), 3);
|
||||
});
|
||||
|
||||
it('month 1 = Q1', () => {
|
||||
assert.equal(getHijriQuarter(new Date(2024, 6, 7, 12)), 1);
|
||||
});
|
||||
|
||||
it('out of range returns null', () => {
|
||||
assert.equal(getHijriQuarter(new Date(1800, 0, 1)), null);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// getHijriWeekdayName
|
||||
// ---------------------------------------------------------------------------
|
||||
describe('FCNA calendar', () => {
|
||||
it('toHijriDate returns valid HijriDate', () => {
|
||||
const h = toHijriDate(new Date(2023, 2, 23, 12), { calendar: 'fcna' });
|
||||
assert.ok(h !== null, 'expected non-null for FCNA');
|
||||
assert.ok(typeof h.hy === 'number');
|
||||
assert.ok(h.hm >= 1 && h.hm <= 12);
|
||||
assert.ok(h.hd >= 1 && h.hd <= 30);
|
||||
});
|
||||
|
||||
// March 23, 2023 was a Thursday (getDay() === 4)
|
||||
test('getHijriWeekdayName: Thursday long', () => {
|
||||
assert.equal(getHijriWeekdayName(new Date(2023, 2, 23)), 'Yawm al-Khamis');
|
||||
it('formatHijriDate works', () => {
|
||||
const result = formatHijriDate(new Date(2023, 2, 23, 12), 'iYYYY-iMM-iDD', { calendar: 'fcna' });
|
||||
assert.ok(result.length > 0, 'expected non-empty string');
|
||||
});
|
||||
});
|
||||
|
||||
test('getHijriWeekdayName: Thursday short', () => {
|
||||
assert.equal(getHijriWeekdayName(new Date(2023, 2, 23), 'short'), 'Kham');
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// formatHijriDate
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test('formatHijriDate: iYYYY-iMM-iDD', () => {
|
||||
assert.equal(formatHijriDate(REF, 'iYYYY-iMM-iDD'), '1444-09-01');
|
||||
});
|
||||
|
||||
test('formatHijriDate: iMMMM', () => {
|
||||
assert.equal(formatHijriDate(REF, 'iMMMM'), 'Ramadan');
|
||||
});
|
||||
|
||||
test('formatHijriDate: iEEEE', () => {
|
||||
assert.equal(formatHijriDate(new Date(2023, 2, 23), 'iEEEE'), 'Yawm al-Khamis');
|
||||
});
|
||||
|
||||
test('formatHijriDate: iEEE', () => {
|
||||
assert.equal(formatHijriDate(new Date(2023, 2, 23), 'iEEE'), 'Kham');
|
||||
});
|
||||
|
||||
test('formatHijriDate: ioooo era', () => {
|
||||
assert.equal(formatHijriDate(REF, 'ioooo'), 'AH');
|
||||
});
|
||||
|
||||
test('formatHijriDate: iooo era', () => {
|
||||
assert.equal(formatHijriDate(REF, 'iooo'), 'AH');
|
||||
});
|
||||
|
||||
test('formatHijriDate: iYY two-digit year', () => {
|
||||
assert.equal(formatHijriDate(REF, 'iYY'), '44');
|
||||
});
|
||||
|
||||
test('formatHijriDate: iMMM medium month', () => {
|
||||
assert.equal(formatHijriDate(REF, 'iMMM'), 'Ramadan');
|
||||
});
|
||||
|
||||
test('formatHijriDate: iM bare month', () => {
|
||||
assert.equal(formatHijriDate(REF, 'iM'), '9');
|
||||
});
|
||||
|
||||
test('formatHijriDate: iD bare day', () => {
|
||||
assert.equal(formatHijriDate(REF, 'iD'), '1');
|
||||
});
|
||||
|
||||
test('formatHijriDate: iE numeric weekday (Thursday = 5)', () => {
|
||||
// hwNumeric[4] = 5 (Thursday, 0-indexed from Sunday)
|
||||
assert.equal(formatHijriDate(new Date(2023, 2, 23), 'iE'), '5');
|
||||
});
|
||||
|
||||
test('formatHijriDate: out of range returns empty string', () => {
|
||||
assert.equal(formatHijriDate(new Date(1800, 0, 1), 'iYYYY-iMM-iDD'), '');
|
||||
});
|
||||
|
||||
test('formatHijriDate: mixed literal and tokens', () => {
|
||||
const result = formatHijriDate(REF, 'iD iMMMM iYYYY ioooo');
|
||||
assert.equal(result, '1 Ramadan 1444 AH');
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// addHijriMonths
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test('addHijriMonths: +1 from Ramadan -> Shawwal', () => {
|
||||
const result = toHijriDate(addHijriMonths(REF, 1));
|
||||
assert.ok(result !== null);
|
||||
assert.equal(result.hy, 1444);
|
||||
assert.equal(result.hm, 10); // Shawwal
|
||||
});
|
||||
|
||||
test('addHijriMonths: +3 from month 10 -> wraps to month 1 of next year', () => {
|
||||
const dec = new Date(2023, 3, 21, 12); // Shawwal 1444 approx
|
||||
const result = toHijriDate(addHijriMonths(dec, 3));
|
||||
assert.ok(result !== null);
|
||||
// Should be in 1445
|
||||
assert.equal(result.hy, 1445);
|
||||
});
|
||||
|
||||
test('addHijriMonths: +0 is identity', () => {
|
||||
const result = toHijriDate(addHijriMonths(REF, 0));
|
||||
assert.ok(result !== null);
|
||||
assert.equal(result.hy, 1444);
|
||||
assert.equal(result.hm, 9);
|
||||
assert.equal(result.hd, 1);
|
||||
});
|
||||
|
||||
test('addHijriMonths: -1 from Ramadan -> Sha\'ban', () => {
|
||||
const result = toHijriDate(addHijriMonths(REF, -1));
|
||||
assert.ok(result !== null);
|
||||
assert.equal(result.hm, 8); // Sha'ban
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// addHijriYears
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test('addHijriYears: +1 from Ramadan 1444 -> Ramadan 1445', () => {
|
||||
const result = toHijriDate(addHijriYears(REF, 1));
|
||||
assert.ok(result !== null);
|
||||
assert.equal(result.hy, 1445);
|
||||
assert.equal(result.hm, 9);
|
||||
});
|
||||
|
||||
test('addHijriYears: -1 from Ramadan 1444 -> Ramadan 1443', () => {
|
||||
const result = toHijriDate(addHijriYears(REF, -1));
|
||||
assert.ok(result !== null);
|
||||
assert.equal(result.hy, 1443);
|
||||
assert.equal(result.hm, 9);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// startOfHijriMonth / endOfHijriMonth
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test('startOfHijriMonth: 1 Ramadan 1444 = 2023-03-23', () => {
|
||||
const start = startOfHijriMonth(REF);
|
||||
// Use local date components — startOfHijriMonth returns a local-noon Date
|
||||
// to round-trip correctly with toHijriDate across all timezones.
|
||||
assert.equal(start.getFullYear(), 2023);
|
||||
assert.equal(start.getMonth(), 2);
|
||||
assert.equal(start.getDate(), 23);
|
||||
});
|
||||
|
||||
test('endOfHijriMonth: last day of Ramadan 1444', () => {
|
||||
const end = toHijriDate(endOfHijriMonth(REF));
|
||||
assert.ok(end !== null);
|
||||
assert.equal(end.hy, 1444);
|
||||
assert.equal(end.hm, 9);
|
||||
// Last day is either 29 or 30
|
||||
assert.ok(end.hd === 29 || end.hd === 30, `expected 29 or 30, got ${end.hd}`);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// isSameHijriMonth / isSameHijriYear
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// April 10, 2023 is 19 Ramadan 1444 — same Hijri month as March 23, 2023
|
||||
test('isSameHijriMonth: both in Ramadan 1444', () => {
|
||||
assert.equal(isSameHijriMonth(new Date(2023, 2, 23, 12), new Date(2023, 3, 10, 12)), true);
|
||||
});
|
||||
|
||||
test('isSameHijriMonth: different months', () => {
|
||||
assert.equal(isSameHijriMonth(new Date(2023, 2, 23, 12), new Date(2023, 4, 1, 12)), false);
|
||||
});
|
||||
|
||||
test('isSameHijriMonth: out of range returns false', () => {
|
||||
assert.equal(isSameHijriMonth(new Date(1800, 0, 1), new Date(2023, 2, 23, 12)), false);
|
||||
});
|
||||
|
||||
// March 10, 2024 is in Ramadan 1445 — different year
|
||||
// But we need same year: 1444 spans roughly April 2022 - April 2023
|
||||
// 1444 starts ~July 30, 2022. Let's pick two dates in 1444:
|
||||
// March 23, 2023 = 1 Ramadan 1444
|
||||
// Feb 10, 2023 = in Jumadal Thani 1444 (still year 1444)
|
||||
test('isSameHijriYear: both in 1444', () => {
|
||||
assert.equal(isSameHijriYear(new Date(2023, 2, 23, 12), new Date(2023, 1, 10, 12)), true);
|
||||
});
|
||||
|
||||
test('isSameHijriYear: different years', () => {
|
||||
assert.equal(isSameHijriYear(new Date(2023, 2, 23, 12), new Date(2024, 6, 7, 12)), false);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// getHijriQuarter
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test('getHijriQuarter: month 9 = Q3', () => {
|
||||
assert.equal(getHijriQuarter(REF), 3);
|
||||
});
|
||||
|
||||
test('getHijriQuarter: month 1 = Q1', () => {
|
||||
assert.equal(getHijriQuarter(new Date(2024, 6, 7, 12)), 1); // 1 Muharram 1446
|
||||
});
|
||||
|
||||
test('getHijriQuarter: out of range returns null', () => {
|
||||
assert.equal(getHijriQuarter(new Date(1800, 0, 1)), null);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// FCNA calendar
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test('toHijriDate: FCNA calendar returns valid HijriDate', () => {
|
||||
const h = toHijriDate(new Date(2023, 2, 23, 12), { calendar: 'fcna' });
|
||||
assert.ok(h !== null, 'expected non-null for FCNA');
|
||||
assert.ok(typeof h.hy === 'number');
|
||||
assert.ok(h.hm >= 1 && h.hm <= 12);
|
||||
assert.ok(h.hd >= 1 && h.hd <= 30);
|
||||
});
|
||||
|
||||
test('formatHijriDate: FCNA calendar', () => {
|
||||
const result = formatHijriDate(new Date(2023, 2, 23, 12), 'iYYYY-iMM-iDD', { calendar: 'fcna' });
|
||||
assert.ok(result.length > 0, 'expected non-empty string');
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Summary
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const total = passed + failed;
|
||||
console.log(`\n${passed}/${total} tests passed`);
|
||||
if (failed > 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,9 @@
|
|||
{
|
||||
"extends": "@acamarata/tsconfig/tsconfig.library.json",
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"esModuleInterop": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"types": ["node"]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ export default defineConfig({
|
|||
splitting: false,
|
||||
sourcemap: true,
|
||||
target: 'es2020',
|
||||
platform: 'node',
|
||||
platform: 'neutral',
|
||||
external: ['hijri-core'],
|
||||
outExtension({ format }) {
|
||||
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
|
||||
}
|
||||
9
vitest.config.ts
Normal file
9
vitest.config.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
environment: "node",
|
||||
include: ["date-fns-hijri.test.ts"],
|
||||
},
|
||||
});
|
||||
Loading…
Reference in a new issue