feat: initial release of date-fns-hijri v1.0.0

Functional Hijri date utility library in date-fns style. 17 exported
functions: toHijriDate, fromHijriDate, isValidHijriDate, getHijriYear,
getHijriMonth, getHijriDay, getDaysInHijriMonth, getHijriMonthName,
getHijriWeekdayName, formatHijriDate, addHijriMonths, addHijriYears,
startOfHijriMonth, endOfHijriMonth, isSameHijriMonth, isSameHijriYear,
getHijriQuarter. All delegate to hijri-core. UTC-midnight-to-local-noon
correction applied in arithmetic functions to avoid timezone boundary
drift. 55 ESM + 10 CJS tests passing. Dual CJS/ESM build.
This commit is contained in:
Aric Camarata 2026-02-25 14:15:24 -05:00
commit d8c5398fdf
21 changed files with 2811 additions and 0 deletions

18
.editorconfig Normal file
View file

@ -0,0 +1,18 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.{js,mjs,cjs,ts,mts,cts,json,yaml,yml,md}]
indent_style = space
indent_size = 2
[*.{c,h}]
indent_style = space
indent_size = 4
[Makefile]
indent_style = tab

62
.github/workflows/ci.yml vendored Normal file
View file

@ -0,0 +1,62 @@
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
name: Test (Node ${{ matrix.node }})
runs-on: ubuntu-latest
strategy:
matrix:
node: [20, 22, 24]
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- 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
typecheck:
name: Typecheck
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm run typecheck
pack-check:
name: Pack check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm run build
- name: Verify pack contents
run: |
npm pack --dry-run 2>&1 | tee pack-output.txt
grep "dist/index.cjs" pack-output.txt
grep "dist/index.mjs" pack-output.txt
grep "dist/index.d.ts" pack-output.txt
grep "dist/index.d.mts" pack-output.txt
grep "README.md" pack-output.txt
grep "CHANGELOG.md" pack-output.txt
grep "LICENSE" pack-output.txt

22
.github/workflows/wiki-sync.yml vendored Normal file
View file

@ -0,0 +1,22 @@
name: Wiki sync
on:
push:
branches: [main]
paths:
- '.wiki/**'
jobs:
sync:
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 }}
with:
owner: acamarata
repo-name: date-fns-hijri
md-folder: .wiki

8
.gitignore vendored Normal file
View file

@ -0,0 +1,8 @@
node_modules/
dist/
*.tgz
*.log
.DS_Store
.claude/
.env
.env.*

1
.npmrc Normal file
View file

@ -0,0 +1 @@
package-import-method=hardlink

1
.nvmrc Normal file
View file

@ -0,0 +1 @@
24

392
.wiki/API-Reference.md Normal file
View file

@ -0,0 +1,392 @@
# API Reference
All functions accept an optional `options?: ConversionOptions` argument as their last parameter. When omitted, the Umm al-Qura (UAQ) calendar is used.
```typescript
interface ConversionOptions {
calendar?: string; // 'uaq' (default) | 'fcna'
}
```
---
## Conversion
### `toHijriDate`
```typescript
function toHijriDate(date: Date, options?: ConversionOptions): HijriDate | null
```
Convert a Gregorian `Date` to a Hijri date object.
Returns `null` when the date falls outside the calendar's supported range. UAQ covers 1356-1500 AH (approximately 1937-2077 CE).
**Parameters:**
| Name | Type | Description |
| --- | --- | --- |
| `date` | `Date` | A valid Gregorian date |
| `options` | `ConversionOptions` | Optional. Calendar system selection |
**Returns:** `HijriDate | null`
**Example:**
```typescript
import { toHijriDate } from 'date-fns-hijri';
toHijriDate(new Date(2023, 2, 23, 12));
// { hy: 1444, hm: 9, hd: 1 } — 1 Ramadan 1444
toHijriDate(new Date(1800, 0, 1));
// null — out of range
```
---
### `fromHijriDate`
```typescript
function fromHijriDate(hy: number, hm: number, hd: number, options?: ConversionOptions): Date
```
Convert a Hijri date to a Gregorian `Date`.
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) |
| `options` | `ConversionOptions` | Optional. Calendar system selection |
**Returns:** `Date`
**Throws:** `Error` if the Hijri date is invalid or outside the supported range.
**Example:**
```typescript
import { fromHijriDate } from 'date-fns-hijri';
fromHijriDate(1444, 9, 1);
// Date: 2023-03-23T00:00:00.000Z
fromHijriDate(1444, 13, 1);
// throws Error
```
---
## Validation
### `isValidHijriDate`
```typescript
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.
**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
```
---
## Field Getters
### `getHijriYear`
```typescript
function getHijriYear(date: Date, options?: ConversionOptions): number | null
```
Get the Hijri year for a Gregorian date. Returns `null` when out of range.
---
### `getHijriMonth`
```typescript
function getHijriMonth(date: Date, options?: ConversionOptions): number | null
```
Get the Hijri month (1-12) for a Gregorian date. Returns `null` when out of range.
---
### `getHijriDay`
```typescript
function getHijriDay(date: Date, options?: ConversionOptions): number | null
```
Get the Hijri day of month for a Gregorian date. Returns `null` when out of range.
---
### `getDaysInHijriMonth`
```typescript
function getDaysInHijriMonth(hy: number, hm: number, options?: ConversionOptions): number
```
Get the number of days in a Hijri month. Returns 29 or 30.
**Throws:** `Error` if the year/month combination is outside the supported range.
**Example:**
```typescript
getDaysInHijriMonth(1444, 9); // 29 or 30 (Ramadan 1444)
getDaysInHijriMonth(1444, 1); // 30 (Muharram 1444)
```
---
### `getHijriQuarter`
```typescript
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.
Returns `null` when out of range.
**Example:**
```typescript
getHijriQuarter(new Date(2023, 2, 23, 12)); // 3 (Ramadan = month 9 = Q3)
```
---
## Names
### `getHijriMonthName`
```typescript
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 |
**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-Hijah | DhH |
---
### `getHijriWeekdayName`
```typescript
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 |
---
## Formatting
### `formatHijriDate`
```typescript
function formatHijriDate(date: Date, formatStr: string, options?: ConversionOptions): string
```
Format a Gregorian date using Hijri calendar tokens.
Returns an empty string when the date falls outside the supported range. Non-token text passes through unchanged.
**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` |
**Examples:**
```typescript
formatHijriDate(new Date(2023, 2, 23, 12), 'iYYYY-iMM-iDD');
// '1444-09-01'
formatHijriDate(new Date(2023, 2, 23, 12), 'iD iMMMM iYYYY ioooo');
// '1 Ramadan 1444 AH'
formatHijriDate(new Date(2023, 2, 23), 'iEEEE');
// 'Yawm al-Khamis'
```
---
## Arithmetic
### `addHijriMonths`
```typescript
function addHijriMonths(date: Date, months: number, options?: ConversionOptions): Date
```
Add a number of Hijri months to a Gregorian date.
Handles year rollover automatically. If the resulting month has fewer days than the original day, the day is clamped to the last day of the new month. Accepts negative values to subtract months.
**Throws:** `Error` if the input or result is outside the supported range.
**Example:**
```typescript
// 1 Ramadan 1444 + 1 month = 1 Shawwal 1444
addHijriMonths(new Date(2023, 2, 23, 12), 1);
// Month rollover: Dhul Hijjah + 1 = Muharram of next year
addHijriMonths(new Date(2023, 6, 18, 12), 1);
```
---
### `addHijriYears`
```typescript
function addHijriYears(date: Date, years: number, options?: ConversionOptions): Date
```
Add a number of Hijri years to a Gregorian date.
If the resulting year has fewer days in the same month (e.g., day 30 in a 29-day month), the day is clamped to the last valid day. Accepts negative values to subtract years.
**Throws:** `Error` if the input or result is outside the supported range.
---
## Month Boundaries
### `startOfHijriMonth`
```typescript
function startOfHijriMonth(date: Date, options?: ConversionOptions): Date
```
Get the first day of the Hijri month that contains the given date.
**Throws:** `Error` if the date is outside the supported range.
---
### `endOfHijriMonth`
```typescript
function endOfHijriMonth(date: Date, options?: ConversionOptions): Date
```
Get the last day of the Hijri month that contains the given date.
**Throws:** `Error` if the date is outside the supported range.
**Example:**
```typescript
// 1 Ramadan 1444 is March 23, 2023
const start = startOfHijriMonth(new Date(2023, 3, 1, 12));
// Date: 2023-03-23T00:00:00.000Z
const end = endOfHijriMonth(new Date(2023, 3, 1, 12));
// Date: last day of Ramadan 1444
```
---
## Comparisons
### `isSameHijriMonth`
```typescript
function isSameHijriMonth(dateA: Date, dateB: Date, options?: ConversionOptions): boolean
```
Check whether two Gregorian dates fall in the same Hijri month.
Returns `false` if either date is outside the supported range.
---
### `isSameHijriYear`
```typescript
function isSameHijriYear(dateA: Date, dateB: Date, options?: ConversionOptions): boolean
```
Check whether two Gregorian dates fall in the same Hijri year.
Returns `false` if either date is outside the supported range.
**Example:**
```typescript
// Both in Ramadan 1444
isSameHijriMonth(new Date(2023, 2, 23, 12), new Date(2023, 3, 10, 12)); // true
// 1444 vs 1446
isSameHijriYear(new Date(2023, 2, 23, 12), new Date(2024, 6, 7, 12)); // false
```
---
[Home](Home) · [API Reference](API-Reference) · [Architecture](Architecture)

125
.wiki/Architecture.md Normal file
View file

@ -0,0 +1,125 @@
# Architecture
## Design Goals
The core goal was a function library that feels native to the JavaScript ecosystem. Anyone who has used date-fns will find the API familiar: one function per operation, plain `Date` inputs and outputs, no side effects, tree-shakeable.
Two constraints shaped the design:
1. The Hijri calendar engine should be a separate, zero-dependency package (`hijri-core`). This keeps `date-fns-hijri` thin and lets other libraries use the engine directly.
2. `date-fns-hijri` should not couple users to any specific calendar algorithm. The `options.calendar` parameter threads through to `hijri-core`'s registry, so custom calendar engines registered there are usable without changes to this package.
## Functional API Pattern
Every exported function is a standalone pure function. There are no classes, no global state, no configuration objects that persist between calls.
This mirrors how date-fns works:
```typescript
// date-fns style
import { addMonths, format } from 'date-fns';
// date-fns-hijri style
import { addHijriMonths, formatHijriDate } from 'date-fns-hijri';
```
Tree-shaking works as expected. `sideEffects: false` in `package.json` tells bundlers they can eliminate any function that isn't imported.
## Peer Dependency Pattern
`hijri-core` is a peer dependency. The consumer installs it explicitly:
```bash
pnpm add date-fns-hijri hijri-core
```
This pattern has two benefits:
- The consumer controls which version of `hijri-core` they use. If a critical fix ships in `hijri-core`, they can upgrade without waiting for a new `date-fns-hijri` release.
- Applications that use multiple Hijri-aware packages (e.g., both `date-fns-hijri` and a separate formatter) share a single `hijri-core` instance. The engine's calendar registry is global within that instance, so a calendar registered once is available everywhere.
The `devDependencies` includes `hijri-core: file:../hijri-core` for local development and CI. Published packages pick up the peer from the consumer's `node_modules`.
## Format Token Resolution
`formatHijriDate` uses a single regular expression to locate all tokens in one pass:
```
/iYYYY|iYY|iMMMM|iMMM|iMM|iM|iDD|iD|iEEEE|iEEE|iE|ioooo|iooo/g
```
Token ordering within the pattern matters. The engine uses JavaScript's `String.replace` with a global regex, which matches the first alternative at each position. Longer tokens appear before their prefixes:
- `iYYYY` before `iYY` (prevents `iYYYY` matching as `iYY` + `YY`)
- `iMMMM` before `iMMM` before `iMM` before `iM`
- `iEEEE` before `iEEE` before `iE`
- `ioooo` before `iooo`
The `iE` token uses `hwNumeric[date.getDay()]` where `hwNumeric = [1, 2, 3, 4, 5, 6, 7]`. Sunday maps to 1, Saturday to 7. This matches the ISO weekday convention in the Hijri context (Sunday = first day of the Islamic week).
Non-token text passes through unchanged because `String.replace` with a regex only replaces matched substrings. This means literal separators (`-`, `/`, ` `, `.`) and arbitrary text work without escaping.
## Out-of-Range Handling
The two types of functions handle out-of-range differently:
**Query functions** (getters, comparisons, `toHijriDate`, `formatHijriDate`): return `null` or an empty string. These functions are often called in display contexts where a silent fallback is appropriate. Throwing would require try/catch around every date display call.
**Mutation functions** (arithmetic, boundary, `fromHijriDate`): throw. When you call `addHijriMonths` or `startOfHijriMonth`, you expect a `Date` back. Returning `null` would require null checks on every arithmetic result, which is error-prone. An exception is the better signal.
## Calendar System Architecture
`hijri-core` implements a registry pattern:
```typescript
// hijri-core/src/registry.ts
const _engines = new Map<string, CalendarEngine>();
export function registerCalendar(name: string, engine: CalendarEngine): void {
_engines.set(name, engine);
}
```
Two engines ship by default: `'uaq'` (Umm al-Qura) and `'fcna'` (FCNA/ISNA). Third parties can register custom engines and use them with `date-fns-hijri` by passing `{ calendar: 'mycalendar' }` to any function.
This package never touches the registry directly. It passes the `options` argument through to `hijri-core`'s convenience wrappers, which resolve the calendar name internally.
## Arithmetic Implementation
Month arithmetic works by converting to a total month offset from the Hijri epoch, adding the delta, then decomposing back to year/month:
```typescript
const totalMonths = (h.hy - 1) * 12 + (h.hm - 1) + months;
const newYear = Math.floor(totalMonths / 12) + 1;
const newMonth = (((totalMonths % 12) + 12) % 12) + 1;
```
The double-modulo pattern `(((n % 12) + 12) % 12)` handles negative values correctly. If `months` is `-1` and the current month is 1 (Muharram), `totalMonths % 12` gives `-1`, the pattern corrects it to `11`, and the year decrements by 1 via `Math.floor`.
Day clamping runs after the new month is determined:
```typescript
const maxDay = coreDaysInHijriMonth(newYear, newMonth, options);
const newDay = Math.min(h.hd, maxDay);
```
This handles the case where month lengths differ (29 vs 30 days) without requiring the caller to know the calendar structure.
## Build Configuration
`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 |
`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.
Source maps ship in the package for debugging. The `target: 'es2020'` setting is conservative enough to run on all supported Node versions (20+) without polyfills.
---
[Home](Home) · [API Reference](API-Reference) · [Architecture](Architecture)

73
.wiki/Home.md Normal file
View file

@ -0,0 +1,73 @@
# date-fns-hijri
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.
The library wraps [hijri-core](https://github.com/acamarata/hijri-core), which provides the underlying calendar engine with support for both Umm al-Qura (UAQ) and FCNA/ISNA calendar systems.
## Quick Example
```typescript
import { toHijriDate, formatHijriDate, addHijriMonths } from 'date-fns-hijri';
// 1 Ramadan 1444 AH
const hijri = toHijriDate(new Date(2023, 2, 23, 12));
// { hy: 1444, hm: 9, hd: 1 }
// Format
const label = formatHijriDate(new Date(2023, 2, 23, 12), 'iD iMMMM iYYYY ioooo');
// '1 Ramadan 1444 AH'
// Arithmetic
const eid = addHijriMonths(new Date(2023, 2, 23, 12), 1);
// Date in Shawwal 1444
```
## Installation
```bash
pnpm add date-fns-hijri hijri-core
```
`hijri-core` is a peer dependency — install it alongside this package.
## Table of Contents
- [API Reference](API-Reference) - All 17 functions with full signatures and examples
- [Architecture](Architecture) - Design decisions, format token resolution, hijri-core integration
## Calendar Systems
Two calendar systems are built in:
- **UAQ (default):** Umm al-Qura. Official Saudi Arabia calendar. Tabular data covering 1356-1500 AH. Deterministic.
- **FCNA:** Fiqh Council of North America. Astronomical calculation. Extends slightly beyond UAQ's range.
Switch calendar system with the `options` argument:
```typescript
import { toHijriDate } from 'date-fns-hijri';
const fcna = toHijriDate(new Date(2023, 2, 23, 12), { calendar: 'fcna' });
```
## TypeScript
Full type definitions are included and exported:
```typescript
import type { HijriDate, ConversionOptions } from 'date-fns-hijri';
```
The `HijriDate` interface:
```typescript
interface HijriDate {
hy: number; // Hijri year
hm: number; // Hijri month (1-12)
hd: number; // Hijri day (1-30)
}
```
---
[Home](Home) · [API Reference](API-Reference) · [Architecture](Architecture)

30
CHANGELOG.md Normal file
View 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`

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Aric Camarata
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

185
README.md Normal file
View file

@ -0,0 +1,185 @@
# date-fns-hijri
[![npm version](https://img.shields.io/npm/v/date-fns-hijri.svg)](https://www.npmjs.com/package/date-fns-hijri)
[![CI](https://github.com/acamarata/date-fns-hijri/actions/workflows/ci.yml/badge.svg)](https://github.com/acamarata/date-fns-hijri/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
date-fns-style functions for Hijri calendar operations. Works with any date library.
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.
## Installation
```bash
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.
## Quick Start
```typescript
import {
toHijriDate,
fromHijriDate,
formatHijriDate,
addHijriMonths,
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
// 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 13561500 AH. 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')
// '1 Ramadan 1444 AH'
```
## TypeScript
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' };
```
## Documentation
Full API reference, architecture notes, and examples: [Wiki](https://github.com/acamarata/date-fns-hijri/wiki)
## Related Packages
- [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
## License
MIT. Copyright (c) 2026 Aric Camarata.

60
package.json Normal file
View file

@ -0,0 +1,60 @@
{
"name": "date-fns-hijri",
"version": "1.0.0",
"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",
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
"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" }
}
},
"sideEffects": false,
"files": [
"dist/index.cjs",
"dist/index.mjs",
"dist/index.d.ts",
"dist/index.d.mts",
"README.md",
"CHANGELOG.md",
"LICENSE"
],
"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"
},
"keywords": [
"date-fns",
"hijri",
"islamic",
"calendar",
"umm-al-qura",
"fcna",
"gregorian",
"converter",
"typescript",
"functional"
],
"peerDependencies": {
"hijri-core": "^1.0.0"
},
"devDependencies": {
"@types/node": "^22.0.0",
"hijri-core": "file:../hijri-core",
"tsup": "^8.0.0",
"typescript": "^5.5.0"
},
"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" }
}

934
pnpm-lock.yaml Normal file
View file

@ -0,0 +1,934 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
devDependencies:
'@types/node':
specifier: ^22.0.0
version: 22.19.11
hijri-core:
specifier: file:../hijri-core
version: file:../hijri-core
tsup:
specifier: ^8.0.0
version: 8.5.1(typescript@5.9.3)
typescript:
specifier: ^5.5.0
version: 5.9.3
packages:
'@esbuild/aix-ppc64@0.27.3':
resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [aix]
'@esbuild/android-arm64@0.27.3':
resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [android]
'@esbuild/android-arm@0.27.3':
resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==}
engines: {node: '>=18'}
cpu: [arm]
os: [android]
'@esbuild/android-x64@0.27.3':
resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [android]
'@esbuild/darwin-arm64@0.27.3':
resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [darwin]
'@esbuild/darwin-x64@0.27.3':
resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==}
engines: {node: '>=18'}
cpu: [x64]
os: [darwin]
'@esbuild/freebsd-arm64@0.27.3':
resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==}
engines: {node: '>=18'}
cpu: [arm64]
os: [freebsd]
'@esbuild/freebsd-x64@0.27.3':
resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==}
engines: {node: '>=18'}
cpu: [x64]
os: [freebsd]
'@esbuild/linux-arm64@0.27.3':
resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [linux]
'@esbuild/linux-arm@0.27.3':
resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==}
engines: {node: '>=18'}
cpu: [arm]
os: [linux]
'@esbuild/linux-ia32@0.27.3':
resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==}
engines: {node: '>=18'}
cpu: [ia32]
os: [linux]
'@esbuild/linux-loong64@0.27.3':
resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==}
engines: {node: '>=18'}
cpu: [loong64]
os: [linux]
'@esbuild/linux-mips64el@0.27.3':
resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==}
engines: {node: '>=18'}
cpu: [mips64el]
os: [linux]
'@esbuild/linux-ppc64@0.27.3':
resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [linux]
'@esbuild/linux-riscv64@0.27.3':
resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==}
engines: {node: '>=18'}
cpu: [riscv64]
os: [linux]
'@esbuild/linux-s390x@0.27.3':
resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==}
engines: {node: '>=18'}
cpu: [s390x]
os: [linux]
'@esbuild/linux-x64@0.27.3':
resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==}
engines: {node: '>=18'}
cpu: [x64]
os: [linux]
'@esbuild/netbsd-arm64@0.27.3':
resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==}
engines: {node: '>=18'}
cpu: [arm64]
os: [netbsd]
'@esbuild/netbsd-x64@0.27.3':
resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==}
engines: {node: '>=18'}
cpu: [x64]
os: [netbsd]
'@esbuild/openbsd-arm64@0.27.3':
resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openbsd]
'@esbuild/openbsd-x64@0.27.3':
resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [openbsd]
'@esbuild/openharmony-arm64@0.27.3':
resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openharmony]
'@esbuild/sunos-x64@0.27.3':
resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==}
engines: {node: '>=18'}
cpu: [x64]
os: [sunos]
'@esbuild/win32-arm64@0.27.3':
resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==}
engines: {node: '>=18'}
cpu: [arm64]
os: [win32]
'@esbuild/win32-ia32@0.27.3':
resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==}
engines: {node: '>=18'}
cpu: [ia32]
os: [win32]
'@esbuild/win32-x64@0.27.3':
resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==}
engines: {node: '>=18'}
cpu: [x64]
os: [win32]
'@jridgewell/gen-mapping@0.3.13':
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
'@jridgewell/resolve-uri@3.1.2':
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
engines: {node: '>=6.0.0'}
'@jridgewell/sourcemap-codec@1.5.5':
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
'@jridgewell/trace-mapping@0.3.31':
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
'@rollup/rollup-android-arm-eabi@4.59.0':
resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==}
cpu: [arm]
os: [android]
'@rollup/rollup-android-arm64@4.59.0':
resolution: {integrity: sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==}
cpu: [arm64]
os: [android]
'@rollup/rollup-darwin-arm64@4.59.0':
resolution: {integrity: sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==}
cpu: [arm64]
os: [darwin]
'@rollup/rollup-darwin-x64@4.59.0':
resolution: {integrity: sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==}
cpu: [x64]
os: [darwin]
'@rollup/rollup-freebsd-arm64@4.59.0':
resolution: {integrity: sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==}
cpu: [arm64]
os: [freebsd]
'@rollup/rollup-freebsd-x64@4.59.0':
resolution: {integrity: sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==}
cpu: [x64]
os: [freebsd]
'@rollup/rollup-linux-arm-gnueabihf@4.59.0':
resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.59.0':
resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==}
cpu: [arm]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.59.0':
resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.59.0':
resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-loong64-gnu@4.59.0':
resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==}
cpu: [loong64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-loong64-musl@4.59.0':
resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==}
cpu: [loong64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-ppc64-gnu@4.59.0':
resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-ppc64-musl@4.59.0':
resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==}
cpu: [ppc64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-riscv64-gnu@4.59.0':
resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-musl@4.59.0':
resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-s390x-gnu@4.59.0':
resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.59.0':
resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.59.0':
resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==}
cpu: [x64]
os: [linux]
libc: [musl]
'@rollup/rollup-openbsd-x64@4.59.0':
resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==}
cpu: [x64]
os: [openbsd]
'@rollup/rollup-openharmony-arm64@4.59.0':
resolution: {integrity: sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==}
cpu: [arm64]
os: [openharmony]
'@rollup/rollup-win32-arm64-msvc@4.59.0':
resolution: {integrity: sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==}
cpu: [arm64]
os: [win32]
'@rollup/rollup-win32-ia32-msvc@4.59.0':
resolution: {integrity: sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==}
cpu: [ia32]
os: [win32]
'@rollup/rollup-win32-x64-gnu@4.59.0':
resolution: {integrity: sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==}
cpu: [x64]
os: [win32]
'@rollup/rollup-win32-x64-msvc@4.59.0':
resolution: {integrity: sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==}
cpu: [x64]
os: [win32]
'@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
'@types/node@22.19.11':
resolution: {integrity: sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==}
acorn@8.16.0:
resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==}
engines: {node: '>=0.4.0'}
hasBin: true
any-promise@1.3.0:
resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==}
bundle-require@5.1.0:
resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
peerDependencies:
esbuild: '>=0.18'
cac@6.7.14:
resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
engines: {node: '>=8'}
chokidar@4.0.3:
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
engines: {node: '>= 14.16.0'}
commander@4.1.1:
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
engines: {node: '>= 6'}
confbox@0.1.8:
resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==}
consola@3.4.2:
resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==}
engines: {node: ^14.18.0 || >=16.10.0}
debug@4.4.3:
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
engines: {node: '>=6.0'}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
esbuild@0.27.3:
resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==}
engines: {node: '>=18'}
hasBin: true
fdir@6.5.0:
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
engines: {node: '>=12.0.0'}
peerDependencies:
picomatch: ^3 || ^4
peerDependenciesMeta:
picomatch:
optional: true
fix-dts-default-cjs-exports@1.0.1:
resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==}
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
hijri-core@file:../hijri-core:
resolution: {directory: ../hijri-core, type: directory}
engines: {node: '>=20'}
joycon@3.1.1:
resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==}
engines: {node: '>=10'}
lilconfig@3.1.3:
resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
engines: {node: '>=14'}
lines-and-columns@1.2.4:
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
load-tsconfig@0.2.5:
resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
mlly@1.8.0:
resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==}
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
mz@2.7.0:
resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
object-assign@4.1.1:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'}
pathe@2.0.3:
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
picomatch@4.0.3:
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
engines: {node: '>=12'}
pirates@4.0.7:
resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==}
engines: {node: '>= 6'}
pkg-types@1.3.1:
resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==}
postcss-load-config@6.0.1:
resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==}
engines: {node: '>= 18'}
peerDependencies:
jiti: '>=1.21.0'
postcss: '>=8.0.9'
tsx: ^4.8.1
yaml: ^2.4.2
peerDependenciesMeta:
jiti:
optional: true
postcss:
optional: true
tsx:
optional: true
yaml:
optional: true
readdirp@4.1.2:
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
engines: {node: '>= 14.18.0'}
resolve-from@5.0.0:
resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==}
engines: {node: '>=8'}
rollup@4.59.0:
resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true
source-map@0.7.6:
resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==}
engines: {node: '>= 12'}
sucrase@3.35.1:
resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==}
engines: {node: '>=16 || 14 >=14.17'}
hasBin: true
thenify-all@1.6.0:
resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
engines: {node: '>=0.8'}
thenify@3.3.1:
resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
tinyexec@0.3.2:
resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==}
tinyglobby@0.2.15:
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
engines: {node: '>=12.0.0'}
tree-kill@1.2.2:
resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
hasBin: true
ts-interface-checker@0.1.13:
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
tsup@8.5.1:
resolution: {integrity: sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==}
engines: {node: '>=18'}
hasBin: true
peerDependencies:
'@microsoft/api-extractor': ^7.36.0
'@swc/core': ^1
postcss: ^8.4.12
typescript: '>=4.5.0'
peerDependenciesMeta:
'@microsoft/api-extractor':
optional: true
'@swc/core':
optional: true
postcss:
optional: true
typescript:
optional: true
typescript@5.9.3:
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
engines: {node: '>=14.17'}
hasBin: true
ufo@1.6.3:
resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==}
undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
snapshots:
'@esbuild/aix-ppc64@0.27.3':
optional: true
'@esbuild/android-arm64@0.27.3':
optional: true
'@esbuild/android-arm@0.27.3':
optional: true
'@esbuild/android-x64@0.27.3':
optional: true
'@esbuild/darwin-arm64@0.27.3':
optional: true
'@esbuild/darwin-x64@0.27.3':
optional: true
'@esbuild/freebsd-arm64@0.27.3':
optional: true
'@esbuild/freebsd-x64@0.27.3':
optional: true
'@esbuild/linux-arm64@0.27.3':
optional: true
'@esbuild/linux-arm@0.27.3':
optional: true
'@esbuild/linux-ia32@0.27.3':
optional: true
'@esbuild/linux-loong64@0.27.3':
optional: true
'@esbuild/linux-mips64el@0.27.3':
optional: true
'@esbuild/linux-ppc64@0.27.3':
optional: true
'@esbuild/linux-riscv64@0.27.3':
optional: true
'@esbuild/linux-s390x@0.27.3':
optional: true
'@esbuild/linux-x64@0.27.3':
optional: true
'@esbuild/netbsd-arm64@0.27.3':
optional: true
'@esbuild/netbsd-x64@0.27.3':
optional: true
'@esbuild/openbsd-arm64@0.27.3':
optional: true
'@esbuild/openbsd-x64@0.27.3':
optional: true
'@esbuild/openharmony-arm64@0.27.3':
optional: true
'@esbuild/sunos-x64@0.27.3':
optional: true
'@esbuild/win32-arm64@0.27.3':
optional: true
'@esbuild/win32-ia32@0.27.3':
optional: true
'@esbuild/win32-x64@0.27.3':
optional: true
'@jridgewell/gen-mapping@0.3.13':
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
'@jridgewell/trace-mapping': 0.3.31
'@jridgewell/resolve-uri@3.1.2': {}
'@jridgewell/sourcemap-codec@1.5.5': {}
'@jridgewell/trace-mapping@0.3.31':
dependencies:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5
'@rollup/rollup-android-arm-eabi@4.59.0':
optional: true
'@rollup/rollup-android-arm64@4.59.0':
optional: true
'@rollup/rollup-darwin-arm64@4.59.0':
optional: true
'@rollup/rollup-darwin-x64@4.59.0':
optional: true
'@rollup/rollup-freebsd-arm64@4.59.0':
optional: true
'@rollup/rollup-freebsd-x64@4.59.0':
optional: true
'@rollup/rollup-linux-arm-gnueabihf@4.59.0':
optional: true
'@rollup/rollup-linux-arm-musleabihf@4.59.0':
optional: true
'@rollup/rollup-linux-arm64-gnu@4.59.0':
optional: true
'@rollup/rollup-linux-arm64-musl@4.59.0':
optional: true
'@rollup/rollup-linux-loong64-gnu@4.59.0':
optional: true
'@rollup/rollup-linux-loong64-musl@4.59.0':
optional: true
'@rollup/rollup-linux-ppc64-gnu@4.59.0':
optional: true
'@rollup/rollup-linux-ppc64-musl@4.59.0':
optional: true
'@rollup/rollup-linux-riscv64-gnu@4.59.0':
optional: true
'@rollup/rollup-linux-riscv64-musl@4.59.0':
optional: true
'@rollup/rollup-linux-s390x-gnu@4.59.0':
optional: true
'@rollup/rollup-linux-x64-gnu@4.59.0':
optional: true
'@rollup/rollup-linux-x64-musl@4.59.0':
optional: true
'@rollup/rollup-openbsd-x64@4.59.0':
optional: true
'@rollup/rollup-openharmony-arm64@4.59.0':
optional: true
'@rollup/rollup-win32-arm64-msvc@4.59.0':
optional: true
'@rollup/rollup-win32-ia32-msvc@4.59.0':
optional: true
'@rollup/rollup-win32-x64-gnu@4.59.0':
optional: true
'@rollup/rollup-win32-x64-msvc@4.59.0':
optional: true
'@types/estree@1.0.8': {}
'@types/node@22.19.11':
dependencies:
undici-types: 6.21.0
acorn@8.16.0: {}
any-promise@1.3.0: {}
bundle-require@5.1.0(esbuild@0.27.3):
dependencies:
esbuild: 0.27.3
load-tsconfig: 0.2.5
cac@6.7.14: {}
chokidar@4.0.3:
dependencies:
readdirp: 4.1.2
commander@4.1.1: {}
confbox@0.1.8: {}
consola@3.4.2: {}
debug@4.4.3:
dependencies:
ms: 2.1.3
esbuild@0.27.3:
optionalDependencies:
'@esbuild/aix-ppc64': 0.27.3
'@esbuild/android-arm': 0.27.3
'@esbuild/android-arm64': 0.27.3
'@esbuild/android-x64': 0.27.3
'@esbuild/darwin-arm64': 0.27.3
'@esbuild/darwin-x64': 0.27.3
'@esbuild/freebsd-arm64': 0.27.3
'@esbuild/freebsd-x64': 0.27.3
'@esbuild/linux-arm': 0.27.3
'@esbuild/linux-arm64': 0.27.3
'@esbuild/linux-ia32': 0.27.3
'@esbuild/linux-loong64': 0.27.3
'@esbuild/linux-mips64el': 0.27.3
'@esbuild/linux-ppc64': 0.27.3
'@esbuild/linux-riscv64': 0.27.3
'@esbuild/linux-s390x': 0.27.3
'@esbuild/linux-x64': 0.27.3
'@esbuild/netbsd-arm64': 0.27.3
'@esbuild/netbsd-x64': 0.27.3
'@esbuild/openbsd-arm64': 0.27.3
'@esbuild/openbsd-x64': 0.27.3
'@esbuild/openharmony-arm64': 0.27.3
'@esbuild/sunos-x64': 0.27.3
'@esbuild/win32-arm64': 0.27.3
'@esbuild/win32-ia32': 0.27.3
'@esbuild/win32-x64': 0.27.3
fdir@6.5.0(picomatch@4.0.3):
optionalDependencies:
picomatch: 4.0.3
fix-dts-default-cjs-exports@1.0.1:
dependencies:
magic-string: 0.30.21
mlly: 1.8.0
rollup: 4.59.0
fsevents@2.3.3:
optional: true
hijri-core@file:../hijri-core: {}
joycon@3.1.1: {}
lilconfig@3.1.3: {}
lines-and-columns@1.2.4: {}
load-tsconfig@0.2.5: {}
magic-string@0.30.21:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
mlly@1.8.0:
dependencies:
acorn: 8.16.0
pathe: 2.0.3
pkg-types: 1.3.1
ufo: 1.6.3
ms@2.1.3: {}
mz@2.7.0:
dependencies:
any-promise: 1.3.0
object-assign: 4.1.1
thenify-all: 1.6.0
object-assign@4.1.1: {}
pathe@2.0.3: {}
picocolors@1.1.1: {}
picomatch@4.0.3: {}
pirates@4.0.7: {}
pkg-types@1.3.1:
dependencies:
confbox: 0.1.8
mlly: 1.8.0
pathe: 2.0.3
postcss-load-config@6.0.1:
dependencies:
lilconfig: 3.1.3
readdirp@4.1.2: {}
resolve-from@5.0.0: {}
rollup@4.59.0:
dependencies:
'@types/estree': 1.0.8
optionalDependencies:
'@rollup/rollup-android-arm-eabi': 4.59.0
'@rollup/rollup-android-arm64': 4.59.0
'@rollup/rollup-darwin-arm64': 4.59.0
'@rollup/rollup-darwin-x64': 4.59.0
'@rollup/rollup-freebsd-arm64': 4.59.0
'@rollup/rollup-freebsd-x64': 4.59.0
'@rollup/rollup-linux-arm-gnueabihf': 4.59.0
'@rollup/rollup-linux-arm-musleabihf': 4.59.0
'@rollup/rollup-linux-arm64-gnu': 4.59.0
'@rollup/rollup-linux-arm64-musl': 4.59.0
'@rollup/rollup-linux-loong64-gnu': 4.59.0
'@rollup/rollup-linux-loong64-musl': 4.59.0
'@rollup/rollup-linux-ppc64-gnu': 4.59.0
'@rollup/rollup-linux-ppc64-musl': 4.59.0
'@rollup/rollup-linux-riscv64-gnu': 4.59.0
'@rollup/rollup-linux-riscv64-musl': 4.59.0
'@rollup/rollup-linux-s390x-gnu': 4.59.0
'@rollup/rollup-linux-x64-gnu': 4.59.0
'@rollup/rollup-linux-x64-musl': 4.59.0
'@rollup/rollup-openbsd-x64': 4.59.0
'@rollup/rollup-openharmony-arm64': 4.59.0
'@rollup/rollup-win32-arm64-msvc': 4.59.0
'@rollup/rollup-win32-ia32-msvc': 4.59.0
'@rollup/rollup-win32-x64-gnu': 4.59.0
'@rollup/rollup-win32-x64-msvc': 4.59.0
fsevents: 2.3.3
source-map@0.7.6: {}
sucrase@3.35.1:
dependencies:
'@jridgewell/gen-mapping': 0.3.13
commander: 4.1.1
lines-and-columns: 1.2.4
mz: 2.7.0
pirates: 4.0.7
tinyglobby: 0.2.15
ts-interface-checker: 0.1.13
thenify-all@1.6.0:
dependencies:
thenify: 3.3.1
thenify@3.3.1:
dependencies:
any-promise: 1.3.0
tinyexec@0.3.2: {}
tinyglobby@0.2.15:
dependencies:
fdir: 6.5.0(picomatch@4.0.3)
picomatch: 4.0.3
tree-kill@1.2.2: {}
ts-interface-checker@0.1.13: {}
tsup@8.5.1(typescript@5.9.3):
dependencies:
bundle-require: 5.1.0(esbuild@0.27.3)
cac: 6.7.14
chokidar: 4.0.3
consola: 3.4.2
debug: 4.4.3
esbuild: 0.27.3
fix-dts-default-cjs-exports: 1.0.1
joycon: 3.1.1
picocolors: 1.1.1
postcss-load-config: 6.0.1
resolve-from: 5.0.0
rollup: 4.59.0
source-map: 0.7.6
sucrase: 3.35.1
tinyexec: 0.3.2
tinyglobby: 0.2.15
tree-kill: 1.2.2
optionalDependencies:
typescript: 5.9.3
transitivePeerDependencies:
- jiti
- supports-color
- tsx
- yaml
typescript@5.9.3: {}
ufo@1.6.3: {}
undici-types@6.21.0: {}

2
pnpm-workspace.yaml Normal file
View file

@ -0,0 +1,2 @@
onlyBuiltDependencies:
- esbuild

378
src/index.ts Normal file
View file

@ -0,0 +1,378 @@
import {
toHijri as coreToHijri,
toGregorian as coreToGregorian,
isValidHijriDate as coreIsValidHijriDate,
daysInHijriMonth as coreDaysInHijriMonth,
hmLong,
hmMedium,
hmShort,
hwLong,
hwShort,
hwNumeric,
} from 'hijri-core';
export type { HijriDate, CalendarEngine, ConversionOptions } from './types';
import type { HijriDate, ConversionOptions } from './types';
// ---------------------------------------------------------------------------
// Conversion
// ---------------------------------------------------------------------------
/**
* Convert a Gregorian `Date` to a Hijri date object.
*
* Returns `null` when the date falls outside the calendar's supported range
* (UAQ: 13561500 AH; FCNA extends slightly further).
*/
export function toHijriDate(date: Date, options?: ConversionOptions): HijriDate | null {
return coreToHijri(date, options);
}
/**
* Convert a Hijri date to a Gregorian `Date`.
*
* The returned `Date` is set to midnight UTC of the equivalent Gregorian day.
*
* @throws {Error} If the Hijri date is invalid or outside the calendar's range.
*/
export function fromHijriDate(
hy: number,
hm: number,
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.`,
);
}
return result;
}
// ---------------------------------------------------------------------------
// Validation
// ---------------------------------------------------------------------------
/**
* Check whether a Hijri date is valid for the given calendar system.
*
* Verifies that the year, month (112), and day (1daysInMonth) all exist
* in the calendar's data table.
*/
export function isValidHijriDate(
hy: number,
hm: number,
hd: number,
options?: ConversionOptions,
): boolean {
return coreIsValidHijriDate(hy, hm, hd, options);
}
// ---------------------------------------------------------------------------
// Field getters
// ---------------------------------------------------------------------------
/**
* Get the Hijri year for a Gregorian date.
*
* 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;
}
/**
* Get the Hijri month (112) for a Gregorian date.
*
* 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;
}
/**
* Get the Hijri day of month (130) for a Gregorian date.
*
* 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;
}
/**
* Get the number of days in a Hijri month (29 or 30).
*
* @throws {Error} If the year/month combination is outside the calendar's range.
*/
export function getDaysInHijriMonth(
hy: number,
hm: number,
options?: ConversionOptions,
): number {
return coreDaysInHijriMonth(hy, hm, options);
}
// ---------------------------------------------------------------------------
// Names
// ---------------------------------------------------------------------------
/**
* Get the English name of a Hijri month.
*
* @param hm - Month number (112).
* @param length - `'long'` (default), `'medium'`, or `'short'`.
*
* @throws {RangeError} If `hm` is not in [1, 12].
*/
export function getHijriMonthName(
hm: number,
length: 'long' | 'medium' | 'short' = 'long',
): string {
if (hm < 1 || hm > 12) {
throw new RangeError(`Hijri month must be 112, got ${hm}.`);
}
const idx = hm - 1;
if (length === 'medium') return hmMedium[idx];
if (length === 'short') return hmShort[idx];
return hmLong[idx];
}
/**
* Get the Arabic weekday name for a Gregorian date.
*
* Uses `Date.getDay()` (0 = Sunday, 6 = Saturday) as the index.
*
* @param date - Any Gregorian `Date`.
* @param length - `'long'` (default) or `'short'`.
*/
export function getHijriWeekdayName(
date: Date,
length: 'long' | 'short' = 'long',
): string {
const day = date.getDay(); // 06
return length === 'short' ? hwShort[day] : hwLong[day];
}
// ---------------------------------------------------------------------------
// Formatting
// ---------------------------------------------------------------------------
/** Ordered token pattern — longer tokens must appear before shorter prefixes. */
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.
*
* 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 (0112) | 09 |
* | iM | Month (112) | 9 |
* | iDD | Zero-padded day (0130) | 01 |
* | iD | Day (130) | 1 |
* | iEEEE | Long weekday name | Yawm al-Khamis |
* | iEEE | Short weekday name | Kham |
* | iE | Numeric weekday (1=Sun7=Sat)| 5 |
* | ioooo | Long era | AH |
* | iooo | Short era | AH |
*
* Returns an empty string when the date falls outside the supported range.
*/
export function formatHijriDate(
date: Date,
formatStr: string,
options?: ConversionOptions,
): string {
const h = coreToHijri(date, options);
if (!h) return '';
const day = date.getDay(); // 06
return formatStr.replace(TOKEN_RE, (token) => {
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;
}
});
}
// ---------------------------------------------------------------------------
// 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
// ---------------------------------------------------------------------------
/**
* 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.
*
* @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);
if (!h) {
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 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));
}
/**
* 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.
*
* @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);
if (!h) {
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);
return utcMidnightToLocalNoon(fromHijriDate(newYear, h.hm, newDay, options));
}
// ---------------------------------------------------------------------------
// Month boundaries
// ---------------------------------------------------------------------------
/**
* Get the first day of the Hijri month that contains the given date.
*
* @throws {Error} If the date is outside the supported range.
*/
export function startOfHijriMonth(date: Date, options?: ConversionOptions): Date {
const h = coreToHijri(date, options);
if (!h) {
throw new Error('Date is outside the supported Hijri calendar range.');
}
return utcMidnightToLocalNoon(fromHijriDate(h.hy, h.hm, 1, options));
}
/**
* Get the last day of the Hijri month that contains the given date.
*
* @throws {Error} If the date is outside the supported range.
*/
export function endOfHijriMonth(date: Date, options?: ConversionOptions): Date {
const h = coreToHijri(date, options);
if (!h) {
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));
}
// ---------------------------------------------------------------------------
// Comparisons
// ---------------------------------------------------------------------------
/**
* Check whether two Gregorian dates fall in the same Hijri month.
*
* 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);
if (!a || !b) return false;
return a.hy === b.hy && a.hm === b.hm;
}
/**
* Check whether two Gregorian dates fall in the same Hijri year.
*
* 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);
if (!a || !b) return false;
return a.hy === b.hy;
}
// ---------------------------------------------------------------------------
// Quarter
// ---------------------------------------------------------------------------
/**
* Get the Hijri quarter (14) for a Gregorian date.
*
* Months 13 = Q1, 46 = Q2, 79 = Q3, 1012 = 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);
if (!h) return null;
return Math.ceil(h.hm / 3);
}

1
src/types.ts Normal file
View file

@ -0,0 +1 @@
export type { HijriDate, CalendarEngine, ConversionOptions } from 'hijri-core';

82
test-cjs.cjs Normal file
View file

@ -0,0 +1,82 @@
'use strict';
const assert = require('node:assert/strict');
const {
toHijriDate,
fromHijriDate,
isValidHijriDate,
getHijriMonthName,
formatHijriDate,
getHijriYear,
getHijriMonth,
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);
});
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);
});
test('CJS: isValidHijriDate true for valid date', () => {
assert.equal(isValidHijriDate(1444, 9, 1), true);
});
test('CJS: isValidHijriDate false for invalid month', () => {
assert.equal(isValidHijriDate(1444, 13, 1), false);
});
test('CJS: getHijriMonthName long', () => {
assert.equal(getHijriMonthName(9), 'Ramadan');
});
test('CJS: getHijriMonthName short', () => {
assert.equal(getHijriMonthName(9, 'short'), 'Ram');
});
test('CJS: formatHijriDate iYYYY-iMM-iDD', () => {
assert.equal(formatHijriDate(REF, 'iYYYY-iMM-iDD'), '1444-09-01');
});
test('CJS: getHijriYear', () => {
assert.equal(getHijriYear(REF), 1444);
});
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);
}

383
test.mjs Normal file
View file

@ -0,0 +1,383 @@
import assert from 'node:assert/strict';
import {
toHijriDate,
fromHijriDate,
isValidHijriDate,
getHijriYear,
getHijriMonth,
getHijriDay,
getDaysInHijriMonth,
getHijriMonthName,
getHijriWeekdayName,
formatHijriDate,
addHijriMonths,
addHijriYears,
startOfHijriMonth,
endOfHijriMonth,
isSameHijriMonth,
isSameHijriYear,
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);
});
test('getHijriMonth', () => {
assert.equal(getHijriMonth(REF), 9);
});
test('getHijriDay', () => {
assert.equal(getHijriDay(REF), 1);
});
test('getHijriYear: out of range returns null', () => {
assert.equal(getHijriYear(new Date(1800, 0, 1)), null);
});
// ---------------------------------------------------------------------------
// getDaysInHijriMonth
// ---------------------------------------------------------------------------
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}`);
});
test('getDaysInHijriMonth: month 1 of 1444', () => {
const days = getDaysInHijriMonth(1444, 1);
assert.ok(days === 29 || days === 30, `expected 29 or 30, got ${days}`);
});
// ---------------------------------------------------------------------------
// getHijriMonthName
// ---------------------------------------------------------------------------
test('getHijriMonthName: long (default)', () => {
assert.equal(getHijriMonthName(9), 'Ramadan');
});
test('getHijriMonthName: medium', () => {
assert.equal(getHijriMonthName(9, 'medium'), 'Ramadan');
});
test('getHijriMonthName: short', () => {
assert.equal(getHijriMonthName(9, 'short'), 'Ram');
});
test('getHijriMonthName: Muharram long', () => {
assert.equal(getHijriMonthName(1), 'Muharram');
});
test('getHijriMonthName: Dhul Hijjah long', () => {
assert.equal(getHijriMonthName(12), 'Dhul Hijjah');
});
test('getHijriMonthName: throws on month 0', () => {
assert.throws(() => getHijriMonthName(0), RangeError);
});
test('getHijriMonthName: throws on month 13', () => {
assert.throws(() => getHijriMonthName(13), RangeError);
});
// ---------------------------------------------------------------------------
// getHijriWeekdayName
// ---------------------------------------------------------------------------
// March 23, 2023 was a Thursday (getDay() === 4)
test('getHijriWeekdayName: Thursday long', () => {
assert.equal(getHijriWeekdayName(new Date(2023, 2, 23)), 'Yawm al-Khamis');
});
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);
}

16
tsconfig.json Normal file
View file

@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "dist",
"rootDir": "src",
"types": ["node"]
},
"include": ["src"]
}

17
tsup.config.ts Normal file
View file

@ -0,0 +1,17 @@
import { defineConfig } from 'tsup';
export default defineConfig({
entry: ['src/index.ts'],
format: ['cjs', 'esm'],
dts: true,
clean: true,
outDir: 'dist',
splitting: false,
sourcemap: true,
target: 'es2020',
platform: 'node',
external: ['hijri-core'],
outExtension({ format }) {
return { js: format === 'esm' ? '.mjs' : '.cjs' };
},
});