mirror of
https://github.com/acamarata/date-fns-hijri.git
synced 2026-07-01 03:04:25 +00:00
Compare commits
No commits in common. "main" and "v1.0.0" have entirely different histories.
55 changed files with 792 additions and 4364 deletions
|
|
@ -1,12 +1,12 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.{ts,mts,cts,js,mjs,cjs,json,yaml,yml,md}]
|
||||
[*.{js,mjs,cjs,ts,mts,cts,json,yaml,yml,md}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
|
|
|
|||
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
|
|
@ -1 +0,0 @@
|
|||
github: [acamarata]
|
||||
30
.github/docs/CHANGELOG.md
vendored
30
.github/docs/CHANGELOG.md
vendored
|
|
@ -1,30 +0,0 @@
|
|||
# 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`
|
||||
29
.github/wiki/CODE_OF_CONDUCT.md
vendored
29
.github/wiki/CODE_OF_CONDUCT.md
vendored
|
|
@ -1,29 +0,0 @@
|
|||
# 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
49
.github/wiki/CONTRIBUTING.md
vendored
|
|
@ -1,49 +0,0 @@
|
|||
# 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.
|
||||
30
.github/wiki/SECURITY.md
vendored
30
.github/wiki/SECURITY.md
vendored
|
|
@ -1,30 +0,0 @@
|
|||
# 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
1
.github/wiki/_Footer.md
vendored
|
|
@ -1 +0,0 @@
|
|||
[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
38
.github/wiki/_Sidebar.md
vendored
|
|
@ -1,38 +0,0 @@
|
|||
**[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
31
.github/wiki/api/README.md
vendored
|
|
@ -1,31 +0,0 @@
|
|||
**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
39
.github/wiki/api/functions/addHijriMonths.md
vendored
|
|
@ -1,39 +0,0 @@
|
|||
[**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
38
.github/wiki/api/functions/addHijriYears.md
vendored
|
|
@ -1,38 +0,0 @@
|
|||
[**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
31
.github/wiki/api/functions/endOfHijriMonth.md
vendored
|
|
@ -1,31 +0,0 @@
|
|||
[**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
51
.github/wiki/api/functions/formatHijriDate.md
vendored
|
|
@ -1,51 +0,0 @@
|
|||
[**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
41
.github/wiki/api/functions/fromHijriDate.md
vendored
|
|
@ -1,41 +0,0 @@
|
|||
[**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.
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
[**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
29
.github/wiki/api/functions/getHijriDay.md
vendored
|
|
@ -1,29 +0,0 @@
|
|||
[**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
29
.github/wiki/api/functions/getHijriMonth.md
vendored
|
|
@ -1,29 +0,0 @@
|
|||
[**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
35
.github/wiki/api/functions/getHijriMonthName.md
vendored
|
|
@ -1,35 +0,0 @@
|
|||
[**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
31
.github/wiki/api/functions/getHijriQuarter.md
vendored
|
|
@ -1,31 +0,0 @@
|
|||
[**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`
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
[**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
29
.github/wiki/api/functions/getHijriYear.md
vendored
|
|
@ -1,29 +0,0 @@
|
|||
[**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
33
.github/wiki/api/functions/isSameHijriMonth.md
vendored
|
|
@ -1,33 +0,0 @@
|
|||
[**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
33
.github/wiki/api/functions/isSameHijriYear.md
vendored
|
|
@ -1,33 +0,0 @@
|
|||
[**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
38
.github/wiki/api/functions/isValidHijriDate.md
vendored
|
|
@ -1,38 +0,0 @@
|
|||
[**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
31
.github/wiki/api/functions/startOfHijriMonth.md
vendored
|
|
@ -1,31 +0,0 @@
|
|||
[**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
30
.github/wiki/api/functions/toHijriDate.md
vendored
|
|
@ -1,30 +0,0 @@
|
|||
[**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
111
.github/wiki/api/interfaces/CalendarEngine.md
vendored
|
|
@ -1,111 +0,0 @@
|
|||
[**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
17
.github/wiki/api/interfaces/ConversionOptions.md
vendored
|
|
@ -1,17 +0,0 @@
|
|||
[**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
33
.github/wiki/api/interfaces/HijriDate.md
vendored
|
|
@ -1,33 +0,0 @@
|
|||
[**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
66
.github/wiki/benchmarks/index.md
vendored
|
|
@ -1,66 +0,0 @@
|
|||
# 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
108
.github/wiki/examples/basic-usage.md
vendored
|
|
@ -1,108 +0,0 @@
|
|||
# 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
98
.github/wiki/examples/formatting.md
vendored
|
|
@ -1,98 +0,0 @@
|
|||
# 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
112
.github/wiki/guides/advanced.md
vendored
|
|
@ -1,112 +0,0 @@
|
|||
# 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
104
.github/wiki/guides/quickstart.md
vendored
|
|
@ -1,104 +0,0 @@
|
|||
# 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,39 +15,22 @@ jobs:
|
|||
node: [20, 22, 24]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Enable corepack
|
||||
run: corepack enable
|
||||
- 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 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
|
||||
- run: node test.mjs
|
||||
- run: node test-cjs.cjs
|
||||
|
||||
typecheck:
|
||||
name: Typecheck
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Enable corepack
|
||||
run: corepack enable
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
|
|
@ -60,8 +43,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Enable corepack
|
||||
run: corepack enable
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
|
|
@ -78,24 +60,3 @@ 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,25 +1,22 @@
|
|||
name: Wiki Sync
|
||||
name: Wiki sync
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- '.github/wiki/**'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
- '.wiki/**'
|
||||
|
||||
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 .github/wiki/ to GitHub Wiki
|
||||
uses: Andrew-Chen-Wang/github-wiki-action@v4
|
||||
with:
|
||||
path: .github/wiki/
|
||||
- name: Sync wiki pages
|
||||
uses: nicenshtein/wiki-page-creator-action@v1
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_ACTOR: ${{ github.actor }}
|
||||
GH_PAT: ${{ secrets.GH_PAT }}
|
||||
with:
|
||||
owner: acamarata
|
||||
repo-name: date-fns-hijri
|
||||
md-folder: .wiki
|
||||
|
|
|
|||
15
.gitignore
vendored
15
.gitignore
vendored
|
|
@ -1,23 +1,8 @@
|
|||
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/
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
53
CHANGELOG.md
53
CHANGELOG.md
|
|
@ -2,38 +2,29 @@
|
|||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
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.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
|
||||
## [1.0.0] - 2026-02-25
|
||||
|
||||
### Added
|
||||
- Initial release
|
||||
|
||||
- `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`
|
||||
|
|
|
|||
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. Each function is a pure, stateless utility. Pass a `Date`, get a result. No classes, no global configuration.
|
||||
date-fns-style functions for Hijri calendar operations. Works with any date library.
|
||||
|
||||
Built on [hijri-core](https://github.com/acamarata/hijri-core). Supports Umm al-Qura (UAQ) and FCNA/ISNA calendar systems.
|
||||
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
|
||||
|
||||
|
|
@ -14,7 +14,7 @@ Built on [hijri-core](https://github.com/acamarata/hijri-core). Supports Umm al-
|
|||
pnpm add date-fns-hijri hijri-core
|
||||
```
|
||||
|
||||
`hijri-core` is a peer dependency. It provides the underlying calendar engine.
|
||||
`hijri-core` is a peer dependency. It provides the underlying calendar engine and must be installed alongside this package.
|
||||
|
||||
## Quick Start
|
||||
|
||||
|
|
@ -27,67 +27,158 @@ import {
|
|||
getHijriMonthName,
|
||||
} from 'date-fns-hijri';
|
||||
|
||||
// Convert Gregorian to Hijri
|
||||
const hijri = toHijriDate(new Date(2023, 2, 23, 12));
|
||||
// { hy: 1444, hm: 9, hd: 1 } (1 Ramadan 1444)
|
||||
// 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, 12), 'iD iMMMM iYYYY ioooo');
|
||||
// '1 Ramadan 1444 AH'
|
||||
const label = formatHijriDate(new Date(2023, 2, 23), 'iDD iMMMM iYYYY');
|
||||
// '01 Ramadan 1444'
|
||||
|
||||
// Add Hijri months
|
||||
const eid = addHijriMonths(new Date(2023, 2, 23, 12), 1);
|
||||
// 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
|
||||
```
|
||||
|
||||
// Get the month name
|
||||
getHijriMonthName(9); // 'Ramadan'
|
||||
## 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')
|
||||
// '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, guides, and examples: **[Wiki](https://github.com/acamarata/date-fns-hijri/wiki)**
|
||||
Full API reference, architecture notes, and examples: [Wiki](https://github.com/acamarata/date-fns-hijri/wiki)
|
||||
|
||||
- [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)
|
||||
## Related Packages
|
||||
|
||||
## 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
|
||||
- [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
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
# Telemetry Disclosure
|
||||
|
||||
This package supports opt-in anonymous usage telemetry via [`@acamarata/telemetry`](https://github.com/acamarata/telemetry).
|
||||
|
||||
Telemetry is **off by default**. No data is sent unless you set `ACAMARATA_TELEMETRY=1`.
|
||||
|
||||
Full disclosure (what is sent, where it goes, how to disable):
|
||||
[github.com/acamarata/telemetry/blob/main/TELEMETRY.md](https://github.com/acamarata/telemetry/blob/main/TELEMETRY.md)
|
||||
|
|
@ -1,126 +0,0 @@
|
|||
/**
|
||||
* 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);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
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'] },
|
||||
];
|
||||
62
package.json
62
package.json
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "date-fns-hijri",
|
||||
"version": "1.0.4",
|
||||
"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",
|
||||
|
|
@ -9,11 +9,9 @@
|
|||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.mjs",
|
||||
"require": "./dist/index.cjs"
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
"import": { "types": "./dist/index.d.mts", "default": "./dist/index.mjs" },
|
||||
"require": { "types": "./dist/index.d.ts", "default": "./dist/index.cjs" }
|
||||
}
|
||||
},
|
||||
"sideEffects": false,
|
||||
"files": [
|
||||
|
|
@ -25,23 +23,14 @@
|
|||
"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 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"
|
||||
"test": "node test.mjs && node test-cjs.cjs",
|
||||
"prepublishOnly": "tsup"
|
||||
},
|
||||
"keywords": [
|
||||
"date-fns",
|
||||
|
|
@ -59,38 +48,13 @@
|
|||
"hijri-core": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@acamarata/eslint-config": "^0.1.0",
|
||||
"@acamarata/prettier-config": "^0.1.0",
|
||||
"@acamarata/telemetry": "^0.1.0",
|
||||
"@acamarata/tsconfig": "^0.1.0",
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@types/node": "^25.3.5",
|
||||
"@typescript-eslint/eslint-plugin": "^8.56.1",
|
||||
"@typescript-eslint/parser": "^8.56.1",
|
||||
"c8": "^10.1.3",
|
||||
"eslint": "^10.0.3",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"hijri-core": "^1.0.3",
|
||||
"prettier": "^3.8.1",
|
||||
"@types/node": "^22.0.0",
|
||||
"hijri-core": "^1.0.0",
|
||||
"tsup": "^8.0.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"
|
||||
"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"
|
||||
},
|
||||
"type": "module",
|
||||
"prettier": "@acamarata/prettier-config"
|
||||
"bugs": { "url": "https://github.com/acamarata/date-fns-hijri/issues" }
|
||||
}
|
||||
|
|
|
|||
1963
pnpm-lock.yaml
1963
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
277
src/index.ts
277
src/index.ts
|
|
@ -9,32 +9,11 @@ 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";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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()));
|
||||
}
|
||||
import type { HijriDate, ConversionOptions } from './types';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Conversion
|
||||
|
|
@ -43,39 +22,19 @@ function localDayToUtcSlot(date: Date): Date {
|
|||
/**
|
||||
* 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(localDayToUtcSlot(date), options);
|
||||
return coreToHijri(date, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a Hijri date to a Gregorian `Date`.
|
||||
*
|
||||
* 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.
|
||||
* 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.
|
||||
*
|
||||
* @example
|
||||
* const d = fromHijriDate(1446, 9, 1);
|
||||
* d.getFullYear(); // 2025
|
||||
* d.getMonth(); // 2 (March)
|
||||
* d.getDate(); // 1
|
||||
*/
|
||||
export function fromHijriDate(
|
||||
hy: number,
|
||||
|
|
@ -83,13 +42,13 @@ export function fromHijriDate(
|
|||
hd: number,
|
||||
options?: ConversionOptions,
|
||||
): Date {
|
||||
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.`);
|
||||
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.`,
|
||||
);
|
||||
}
|
||||
// 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());
|
||||
return result;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -118,34 +77,28 @@ 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(localDayToUtcSlot(date), options)?.hy ?? null;
|
||||
return coreToHijri(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(localDayToUtcSlot(date), options)?.hm ?? null;
|
||||
return coreToHijri(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(localDayToUtcSlot(date), options)?.hd ?? null;
|
||||
return coreToHijri(date, options)?.hd ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -153,7 +106,11 @@ 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);
|
||||
}
|
||||
|
||||
|
|
@ -171,36 +128,31 @@ export function getDaysInHijriMonth(hy: number, hm: number, options?: Conversion
|
|||
*/
|
||||
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;
|
||||
// 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]!;
|
||||
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.
|
||||
* `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
|
||||
// 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]!;
|
||||
return length === 'short' ? hwShort[day] : hwLong[day];
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -213,9 +165,6 @@ 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 |
|
||||
|
|
@ -241,52 +190,49 @@ export function formatHijriDate(
|
|||
formatStr: string,
|
||||
options?: ConversionOptions,
|
||||
): string {
|
||||
const h = coreToHijri(localDayToUtcSlot(date), options);
|
||||
if (!h) return "";
|
||||
const h = coreToHijri(date, options);
|
||||
if (!h) return '';
|
||||
|
||||
const day = date.getDay(); // 0–6 local weekday — correct for display
|
||||
const day = date.getDay(); // 0–6
|
||||
|
||||
return formatStr.replace(TOKEN_RE, (token): string => {
|
||||
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":
|
||||
// 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;
|
||||
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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -294,55 +240,57 @@ export function formatHijriDate(
|
|||
/**
|
||||
* 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(localDayToUtcSlot(date), options);
|
||||
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.");
|
||||
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 fromHijriDate(newYear, newMonth, newDay, options);
|
||||
return utcMidnightToLocalNoon(fromHijriDate(newYear, newMonth, newDay, options));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a number of Hijri years to a Gregorian date.
|
||||
*
|
||||
* 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.
|
||||
* 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(localDayToUtcSlot(date), options);
|
||||
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.");
|
||||
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 fromHijriDate(newYear, h.hm, newDay, options);
|
||||
return utcMidnightToLocalNoon(fromHijriDate(newYear, h.hm, newDay, options));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -352,34 +300,28 @@ export function addHijriYears(date: Date, years: number, options?: ConversionOpt
|
|||
/**
|
||||
* 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(localDayToUtcSlot(date), options);
|
||||
const h = coreToHijri(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 fromHijriDate(h.hy, h.hm, 1, options);
|
||||
return utcMidnightToLocalNoon(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(localDayToUtcSlot(date), options);
|
||||
const h = coreToHijri(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 fromHijriDate(h.hy, h.hm, lastDay, options);
|
||||
return utcMidnightToLocalNoon(fromHijriDate(h.hy, h.hm, lastDay, options));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -389,13 +331,15 @@ 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(localDayToUtcSlot(dateA), options);
|
||||
const b = coreToHijri(localDayToUtcSlot(dateB), options);
|
||||
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;
|
||||
}
|
||||
|
|
@ -403,13 +347,15 @@ export function isSameHijriMonth(dateA: Date, dateB: Date, options?: ConversionO
|
|||
/**
|
||||
* 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(localDayToUtcSlot(dateA), options);
|
||||
const b = coreToHijri(localDayToUtcSlot(dateB), options);
|
||||
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;
|
||||
}
|
||||
|
|
@ -421,23 +367,12 @@ export function isSameHijriYear(dateA: Date, dateB: Date, options?: ConversionOp
|
|||
/**
|
||||
* 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(localDayToUtcSlot(date), options);
|
||||
const h = coreToHijri(date, options);
|
||||
if (!h) return null;
|
||||
return Math.ceil(h.hm / 3);
|
||||
}
|
||||
|
||||
// ── Opt-in anonymous telemetry ────────────────────────────────────────────────
|
||||
// Off by default. Enable: ACAMARATA_TELEMETRY=1
|
||||
// What is sent + how to disable: https://github.com/acamarata/telemetry/blob/main/TELEMETRY.md
|
||||
import("@acamarata/telemetry")
|
||||
.then(({ track }) => track("load", { package: "date-fns-hijri", version: "1.0.4" }))
|
||||
.catch(() => {
|
||||
// telemetry not installed or disabled — that's fine
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export type { HijriDate, CalendarEngine, ConversionOptions } from "hijri-core";
|
||||
export type { HijriDate, CalendarEngine, ConversionOptions } from 'hijri-core';
|
||||
|
|
|
|||
104
test-cjs.cjs
104
test-cjs.cjs
|
|
@ -1,6 +1,5 @@
|
|||
'use strict';
|
||||
|
||||
const { describe, it } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const {
|
||||
toHijriDate,
|
||||
|
|
@ -13,64 +12,71 @@ 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
|
||||
|
||||
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: 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: 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: 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: 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 true for valid date', () => {
|
||||
assert.equal(isValidHijriDate(1444, 9, 1), true);
|
||||
});
|
||||
|
||||
describe('CJS: getHijriMonthName', () => {
|
||||
it('long', () => {
|
||||
assert.equal(getHijriMonthName(9), 'Ramadan');
|
||||
});
|
||||
|
||||
it('short', () => {
|
||||
assert.equal(getHijriMonthName(9, 'short'), 'Ram');
|
||||
});
|
||||
test('CJS: isValidHijriDate false for invalid month', () => {
|
||||
assert.equal(isValidHijriDate(1444, 13, 1), false);
|
||||
});
|
||||
|
||||
describe('CJS: formatHijriDate', () => {
|
||||
it('iYYYY-iMM-iDD', () => {
|
||||
assert.equal(formatHijriDate(REF, 'iYYYY-iMM-iDD'), '1444-09-01');
|
||||
});
|
||||
test('CJS: getHijriMonthName long', () => {
|
||||
assert.equal(getHijriMonthName(9), 'Ramadan');
|
||||
});
|
||||
|
||||
describe('CJS: field getters', () => {
|
||||
it('getHijriYear', () => {
|
||||
assert.equal(getHijriYear(REF), 1444);
|
||||
});
|
||||
|
||||
it('getHijriMonth', () => {
|
||||
assert.equal(getHijriMonth(REF), 9);
|
||||
});
|
||||
|
||||
it('getHijriDay', () => {
|
||||
assert.equal(getHijriDay(REF), 1);
|
||||
});
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
623
test.mjs
623
test.mjs
|
|
@ -1,4 +1,3 @@
|
|||
import { describe, it } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
toHijriDate,
|
||||
|
|
@ -20,327 +19,365 @@ 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
|
||||
|
||||
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('getHijriYear', () => {
|
||||
assert.equal(getHijriYear(REF), 1444);
|
||||
});
|
||||
|
||||
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('getHijriMonth', () => {
|
||||
assert.equal(getHijriMonth(REF), 9);
|
||||
});
|
||||
|
||||
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('getHijriDay', () => {
|
||||
assert.equal(getHijriDay(REF), 1);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
test('getHijriYear: out of range returns null', () => {
|
||||
assert.equal(getHijriYear(new Date(1800, 0, 1)), null);
|
||||
});
|
||||
|
||||
describe('getDaysInHijriMonth', () => {
|
||||
it('Ramadan 1444', () => {
|
||||
const days = getDaysInHijriMonth(1444, 9);
|
||||
assert.ok(days === 29 || days === 30, `expected 29 or 30, got ${days}`);
|
||||
});
|
||||
// ---------------------------------------------------------------------------
|
||||
// getDaysInHijriMonth
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
it('month 1 of 1444', () => {
|
||||
const days = getDaysInHijriMonth(1444, 1);
|
||||
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}`);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
test('getDaysInHijriMonth: month 1 of 1444', () => {
|
||||
const days = getDaysInHijriMonth(1444, 1);
|
||||
assert.ok(days === 29 || days === 30, `expected 29 or 30, got ${days}`);
|
||||
});
|
||||
|
||||
describe('getHijriWeekdayName', () => {
|
||||
it('Thursday long', () => {
|
||||
assert.equal(getHijriWeekdayName(new Date(2023, 2, 23)), 'Yawm al-Khamis');
|
||||
});
|
||||
// ---------------------------------------------------------------------------
|
||||
// getHijriMonthName
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
it('Thursday short', () => {
|
||||
assert.equal(getHijriWeekdayName(new Date(2023, 2, 23), 'short'), 'Kham');
|
||||
});
|
||||
test('getHijriMonthName: long (default)', () => {
|
||||
assert.equal(getHijriMonthName(9), '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: medium', () => {
|
||||
assert.equal(getHijriMonthName(9, 'medium'), 'Ramadan');
|
||||
});
|
||||
|
||||
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: short', () => {
|
||||
assert.equal(getHijriMonthName(9, 'short'), 'Ram');
|
||||
});
|
||||
|
||||
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: Muharram long', () => {
|
||||
assert.equal(getHijriMonthName(1), 'Muharram');
|
||||
});
|
||||
|
||||
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: Dhul Hijjah long', () => {
|
||||
assert.equal(getHijriMonthName(12), 'Dhul Hijjah');
|
||||
});
|
||||
|
||||
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 0', () => {
|
||||
assert.throws(() => getHijriMonthName(0), 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);
|
||||
});
|
||||
test('getHijriMonthName: throws on month 13', () => {
|
||||
assert.throws(() => getHijriMonthName(13), RangeError);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
// ---------------------------------------------------------------------------
|
||||
// getHijriWeekdayName
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
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');
|
||||
});
|
||||
// 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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,14 @@
|
|||
{
|
||||
"extends": "@acamarata/tsconfig/tsconfig.library.json",
|
||||
"compilerOptions": {
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": 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: 'neutral',
|
||||
platform: 'node',
|
||||
external: ['hijri-core'],
|
||||
outExtension({ format }) {
|
||||
return { js: format === 'esm' ? '.mjs' : '.cjs' };
|
||||
|
|
|
|||
10
typedoc.json
10
typedoc.json
|
|
@ -1,10 +0,0 @@
|
|||
{
|
||||
"entryPoints": ["src/index.ts"],
|
||||
"out": ".github/wiki/api",
|
||||
"plugin": ["typedoc-plugin-markdown"],
|
||||
"readme": "none",
|
||||
"skipErrorChecking": false,
|
||||
"excludePrivate": true,
|
||||
"excludeProtected": true,
|
||||
"includeVersion": true
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
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