date-fns-hijri/README.md
Aric Camarata d12117f000 fix: local-day adapter semantics with exact round-trips on hijri-core's UTC-day contract
Behavior changes (lock-step with hijri-core fix/utc-day-boundary):

- toHijriDate and all field/format/comparison/arithmetic functions now lift input
  Dates through localDayToUtcSlot() before calling coreToHijri(), reading the
  caller's LOCAL calendar day (date-fns convention). Previously passed the raw
  Date which caused off-by-one results in timezones west of UTC against the new
  UTC-day core contract.

- fromHijriDate now returns local-midnight Dates (new Date(y, m, d)) instead of
  UTC midnight. Local field accessors and date-fns format() render the intended
  calendar day on every host timezone. toISOString() is no longer the right API
  for this value.

- addHijriMonths, addHijriYears, startOfHijriMonth, endOfHijriMonth call
  fromHijriDate directly; the utcMidnightToLocalNoon shim is removed.

- Round-trip toHijriDate(fromHijriDate(y, m, d)) is now exact on every timezone.

Verified: 58/58 ESM tests, 10/10 CJS tests, 16/16 vitest assertions across
TZ=UTC, TZ=America/New_York, and TZ=Pacific/Auckland.
2026-06-10 16:38:32 -04:00

94 lines
3.5 KiB
Markdown

# date-fns-hijri
[![npm version](https://img.shields.io/npm/v/date-fns-hijri.svg)](https://www.npmjs.com/package/date-fns-hijri)
[![CI](https://github.com/acamarata/date-fns-hijri/actions/workflows/ci.yml/badge.svg)](https://github.com/acamarata/date-fns-hijri/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
date-fns-style functions for Hijri calendar operations. Each function is a pure, stateless utility. Pass a `Date`, get a result. No classes, no global configuration.
Built on [hijri-core](https://github.com/acamarata/hijri-core). Supports Umm al-Qura (UAQ) and FCNA/ISNA calendar systems.
## Installation
```bash
pnpm add date-fns-hijri hijri-core
```
`hijri-core` is a peer dependency. It provides the underlying calendar engine.
## Quick Start
```typescript
import {
toHijriDate,
fromHijriDate,
formatHijriDate,
addHijriMonths,
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)
// Format with Hijri tokens
const label = formatHijriDate(new Date(2023, 2, 23, 12), 'iD iMMMM iYYYY ioooo');
// '1 Ramadan 1444 AH'
// Add Hijri months
const eid = addHijriMonths(new Date(2023, 2, 23, 12), 1);
// Date in Shawwal 1444
// Get the month name
getHijriMonthName(9); // 'Ramadan'
```
## Documentation
Full API reference, guides, 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)
## Day boundaries and time zones
This package follows date-fns local-time conventions:
- **Inputs** (`toHijriDate`, `getHijri*`, `formatHijriDate`, arithmetic, comparisons) — the input `Date` is read by its **local calendar day** (using `getFullYear`/`getMonth`/`getDate`). This matches how date-fns' own `format()` and field accessors work.
- **Outputs** (`fromHijriDate` and all arithmetic/boundary functions) — returned `Date` values are **local midnight** of the equivalent Gregorian day. Local field accessors and date-fns' `format()` will render the intended date on every timezone.
Round-trips are exact on every host timezone:
```typescript
toHijriDate(fromHijriDate(1446, 9, 1)); // always { hy: 1446, hm: 9, hd: 1 }
```
**Pitfall:** `new Date("2025-03-01")` parses as UTC midnight. In timezones west of UTC this resolves to the previous local day (Feb 28), giving an off-by-one result. Use the local-date constructor instead:
```typescript
// Wrong in timezones west of UTC:
toHijriDate(new Date("2025-03-01")); // may return 29 Shaban in some zones
// Correct everywhere:
toHijriDate(new Date(2025, 2, 1)); // always 1 Ramadan 1446
```
Religious day-start (sunset boundary) is out of scope — this package only handles civil calendar day alignment.
## Related
- [hijri-core](https://github.com/acamarata/hijri-core): the calendar engine powering this library
- [luxon-hijri](https://github.com/acamarata/luxon-hijri): Hijri support for Luxon DateTime objects
- [pray-calc](https://github.com/acamarata/pray-calc): Islamic prayer times
## Compatibility
- Node.js 20, 22, 24
- ESM and CJS builds included
- TypeScript definitions bundled
- Works in browsers and all major bundlers
## License
MIT. Copyright (c) 2026 Aric Camarata.