mirror of
https://github.com/acamarata/date-fns-hijri.git
synced 2026-07-01 11:14:26 +00:00
Compare commits
7 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf1e8fa43e | ||
|
|
ee6c78d68f | ||
|
|
4d414b2056 | ||
|
|
750f7e19ad | ||
|
|
a8e72ac2b2 | ||
|
|
d12117f000 | ||
|
|
f260912927 |
13 changed files with 974 additions and 108 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,5 +1,6 @@
|
||||||
node_modules/
|
node_modules/
|
||||||
dist/
|
dist/
|
||||||
|
coverage/
|
||||||
*.tgz
|
*.tgz
|
||||||
*.log
|
*.log
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
|
||||||
14
CHANGELOG.md
14
CHANGELOG.md
|
|
@ -5,7 +5,19 @@ 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.1.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## [Unreleased]
|
## [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
|
## [1.0.2] - 2026-05-30
|
||||||
|
|
||||||
|
|
|
||||||
25
README.md
25
README.md
|
|
@ -51,6 +51,31 @@ Full API reference, guides, and examples: **[Wiki](https://github.com/acamarata/
|
||||||
- [Architecture](https://github.com/acamarata/date-fns-hijri/wiki/Architecture): design decisions and hijri-core integration
|
- [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)
|
- [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
|
## Related
|
||||||
|
|
||||||
- [hijri-core](https://github.com/acamarata/hijri-core): the calendar engine powering this library
|
- [hijri-core](https://github.com/acamarata/hijri-core): the calendar engine powering this library
|
||||||
|
|
|
||||||
8
TELEMETRY.md
Normal file
8
TELEMETRY.md
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
# Telemetry Disclosure
|
||||||
|
|
||||||
|
This package supports opt-in anonymous usage telemetry via [`@acamarata/telemetry`](https://github.com/acamarata/telemetry).
|
||||||
|
|
||||||
|
Telemetry is **off by default**. No data is sent unless you set `ACAMARATA_TELEMETRY=1`.
|
||||||
|
|
||||||
|
Full disclosure (what is sent, where it goes, how to disable):
|
||||||
|
[github.com/acamarata/telemetry/blob/main/TELEMETRY.md](https://github.com/acamarata/telemetry/blob/main/TELEMETRY.md)
|
||||||
126
date-fns-hijri.test.ts
Normal file
126
date-fns-hijri.test.ts
Normal file
|
|
@ -0,0 +1,126 @@
|
||||||
|
/**
|
||||||
|
* Purpose: Vitest suite for date-fns-hijri — functional Hijri date utilities.
|
||||||
|
* Inputs: Pure functions from src/index.ts wrapping hijri-core. No network, no I/O.
|
||||||
|
* Outputs: Vitest pass/fail assertions.
|
||||||
|
* Constraints: UAQ range 1318–1500 AH; fromHijriDate throws on invalid input (null path).
|
||||||
|
* Use local-date constructor new Date(y, m, d) — not string "YYYY-MM-DD" which
|
||||||
|
* parses as UTC midnight and can be the previous LOCAL day west of UTC.
|
||||||
|
* Usage: pnpm vitest run
|
||||||
|
* SOT: packages.md — date-fns-hijri row
|
||||||
|
*/
|
||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import {
|
||||||
|
toHijriDate,
|
||||||
|
fromHijriDate,
|
||||||
|
isValidHijriDate,
|
||||||
|
getHijriYear,
|
||||||
|
getHijriMonth,
|
||||||
|
getHijriDay,
|
||||||
|
getDaysInHijriMonth,
|
||||||
|
getHijriMonthName,
|
||||||
|
getHijriWeekdayName,
|
||||||
|
} from "./src/index";
|
||||||
|
|
||||||
|
// Anchor: 1 Ramadan 1446 = 2025-03-01 in the Gregorian calendar.
|
||||||
|
// Use local-date constructor to avoid the UTC-parsing pitfall with string form.
|
||||||
|
// At local noon the local calendar day is unambiguous on every timezone.
|
||||||
|
const RAMADAN_1446_NOON = new Date(2025, 2, 1, 12); // local noon 2025-03-01
|
||||||
|
|
||||||
|
describe("toHijriDate", () => {
|
||||||
|
it("converts noon 2025-03-01 UTC to 1 Ramadan 1446", () => {
|
||||||
|
const result = toHijriDate(RAMADAN_1446_NOON);
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result!.hy).toBe(1446);
|
||||||
|
expect(result!.hm).toBe(9);
|
||||||
|
expect(result!.hd).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns null for dates outside UAQ range (2100)", () => {
|
||||||
|
expect(toHijriDate(new Date("2100-01-01"))).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("fromHijriDate", () => {
|
||||||
|
it("converts 1 Ramadan 1446 to local 2025-03-01 (via local accessors)", () => {
|
||||||
|
const result = fromHijriDate(1446, 9, 1);
|
||||||
|
// Returns local midnight: local accessors show the intended calendar day
|
||||||
|
// on every host timezone. Do NOT use toISOString() — it shows UTC which
|
||||||
|
// will be the previous day in timezones west of UTC.
|
||||||
|
expect(result.getFullYear()).toBe(2025);
|
||||||
|
expect(result.getMonth()).toBe(2); // March
|
||||||
|
expect(result.getDate()).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("round-trip: toHijriDate(fromHijriDate(1446, 9, 1)) === {1446, 9, 1}", () => {
|
||||||
|
const d = fromHijriDate(1446, 9, 1);
|
||||||
|
const h = toHijriDate(d);
|
||||||
|
expect(h).not.toBeNull();
|
||||||
|
expect(h!.hy).toBe(1446);
|
||||||
|
expect(h!.hm).toBe(9);
|
||||||
|
expect(h!.hd).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws on an out-of-range Hijri year (1501)", () => {
|
||||||
|
expect(() => fromHijriDate(1501, 1, 1)).toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("isValidHijriDate", () => {
|
||||||
|
it("returns true for 1 Ramadan 1446", () => {
|
||||||
|
expect(isValidHijriDate(1446, 9, 1)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false for month 13", () => {
|
||||||
|
expect(isValidHijriDate(1446, 13, 1)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("field getters", () => {
|
||||||
|
it("getHijriYear returns 1446 for noon 2025-03-01", () => {
|
||||||
|
expect(getHijriYear(RAMADAN_1446_NOON)).toBe(1446);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("getHijriMonth returns 9 for Ramadan", () => {
|
||||||
|
expect(getHijriMonth(RAMADAN_1446_NOON)).toBe(9);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("getHijriDay returns 1", () => {
|
||||||
|
expect(getHijriDay(RAMADAN_1446_NOON)).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getDaysInHijriMonth", () => {
|
||||||
|
it("returns 29 or 30 for Ramadan 1446", () => {
|
||||||
|
const days = getDaysInHijriMonth(1446, 9);
|
||||||
|
expect([29, 30]).toContain(days);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getHijriMonthName", () => {
|
||||||
|
it("returns Ramadan for month 9 (long)", () => {
|
||||||
|
expect(getHijriMonthName(9, "long")).toBe("Ramadan");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws RangeError for month 0", () => {
|
||||||
|
expect(() => getHijriMonthName(0)).toThrow(RangeError);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns a non-empty medium name for month 1", () => {
|
||||||
|
const name = getHijriMonthName(1, "medium");
|
||||||
|
expect(name.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getHijriWeekdayName", () => {
|
||||||
|
it("returns a non-empty long weekday name for 2025-03-01 (Saturday)", () => {
|
||||||
|
const name = getHijriWeekdayName(RAMADAN_1446_NOON, "long");
|
||||||
|
expect(typeof name).toBe("string");
|
||||||
|
expect(name.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("short name is no longer than long name for the same date", () => {
|
||||||
|
const long = getHijriWeekdayName(RAMADAN_1446_NOON, "long");
|
||||||
|
const short = getHijriWeekdayName(RAMADAN_1446_NOON, "short");
|
||||||
|
expect(short.length).toBeLessThanOrEqual(long.length);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -4,13 +4,17 @@ import eslintConfigPrettier from 'eslint-config-prettier';
|
||||||
import { typescript } from '@acamarata/eslint-config';
|
import { typescript } from '@acamarata/eslint-config';
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
{
|
|
||||||
plugins: { '@typescript-eslint': tsPlugin },
|
|
||||||
languageOptions: { parser: tsParser },
|
|
||||||
},
|
|
||||||
...typescript,
|
|
||||||
eslintConfigPrettier,
|
|
||||||
{
|
{
|
||||||
ignores: ['dist/', 'node_modules/', 'test.mjs', 'test-cjs.cjs'],
|
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'] },
|
||||||
];
|
];
|
||||||
|
|
|
||||||
15
package.json
15
package.json
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "date-fns-hijri",
|
"name": "date-fns-hijri",
|
||||||
"version": "1.0.2",
|
"version": "1.0.4",
|
||||||
"description": "date-fns-style utility functions for Hijri calendar operations. Wraps hijri-core with a functional API for converting, formatting, and validating Hijri dates.",
|
"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",
|
"author": "Aric Camarata",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
@ -37,10 +37,11 @@
|
||||||
"lint": "eslint src/",
|
"lint": "eslint src/",
|
||||||
"format": "prettier --write src/",
|
"format": "prettier --write src/",
|
||||||
"format:check": "prettier --check src/",
|
"format:check": "prettier --check src/",
|
||||||
"prepublishOnly": "pnpm run build",
|
"prepack": "pnpm run build",
|
||||||
"coverage": "c8 --reporter=lcov --reporter=text node test.mjs",
|
"coverage": "c8 --reporter=lcov --reporter=text node test.mjs",
|
||||||
"docs": "typedoc --out .github/wiki/api src/index.ts",
|
"docs": "typedoc --out .github/wiki/api src/index.ts",
|
||||||
"postbuild": "cp dist/index.d.ts dist/index.d.mts"
|
"postbuild": "cp dist/index.d.ts dist/index.d.mts",
|
||||||
|
"test:vitest": "vitest run"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"date-fns",
|
"date-fns",
|
||||||
|
|
@ -60,19 +61,23 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@acamarata/eslint-config": "^0.1.0",
|
"@acamarata/eslint-config": "^0.1.0",
|
||||||
"@acamarata/prettier-config": "^0.1.0",
|
"@acamarata/prettier-config": "^0.1.0",
|
||||||
|
"@acamarata/telemetry": "^0.1.0",
|
||||||
"@acamarata/tsconfig": "^0.1.0",
|
"@acamarata/tsconfig": "^0.1.0",
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
"@types/node": "^25.3.5",
|
"@types/node": "^25.3.5",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^8.56.1",
|
||||||
|
"@typescript-eslint/parser": "^8.56.1",
|
||||||
"c8": "^10.1.3",
|
"c8": "^10.1.3",
|
||||||
"eslint": "^10.0.3",
|
"eslint": "^10.0.3",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"hijri-core": "^1.0.0",
|
"hijri-core": "^1.0.3",
|
||||||
"prettier": "^3.8.1",
|
"prettier": "^3.8.1",
|
||||||
"tsup": "^8.0.0",
|
"tsup": "^8.0.0",
|
||||||
"typedoc": "^0.28.19",
|
"typedoc": "^0.28.19",
|
||||||
"typedoc-plugin-markdown": "^4.11.0",
|
"typedoc-plugin-markdown": "^4.11.0",
|
||||||
"typescript": "^5.5.0",
|
"typescript": "^5.5.0",
|
||||||
"typescript-eslint": "^8.56.1"
|
"typescript-eslint": "^8.56.1",
|
||||||
|
"vitest": "^2.1.9"
|
||||||
},
|
},
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public",
|
"access": "public",
|
||||||
|
|
|
||||||
603
pnpm-lock.yaml
603
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
211
src/index.ts
211
src/index.ts
|
|
@ -9,11 +9,32 @@ import {
|
||||||
hwLong,
|
hwLong,
|
||||||
hwShort,
|
hwShort,
|
||||||
hwNumeric,
|
hwNumeric,
|
||||||
} from 'hijri-core';
|
} from "hijri-core";
|
||||||
|
|
||||||
export type { HijriDate, CalendarEngine, ConversionOptions } from './types';
|
export type { HijriDate, CalendarEngine, ConversionOptions } from "./types";
|
||||||
|
|
||||||
import type { HijriDate, ConversionOptions } from './types';
|
import type { HijriDate, ConversionOptions } from "./types";
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Internal helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Purpose: Lift a Date's LOCAL calendar components (year, month, day) into the
|
||||||
|
* UTC slot so that hijri-core's UTC-day contract reads the caller's
|
||||||
|
* intended calendar day regardless of host timezone.
|
||||||
|
* Inputs: Any Gregorian Date.
|
||||||
|
* Outputs: A new Date whose UTC year/month/date equal the input's LOCAL year/month/date.
|
||||||
|
* Constraints: Used only as input to coreToHijri; the returned value is an ephemeral
|
||||||
|
* intermediate — never hand it to Date#getFullYear or date-fns functions.
|
||||||
|
* WHY: date-fns is a LOCAL-time library: its functions read local components.
|
||||||
|
* hijri-core (after fix/utc-day-boundary) reads the UTC calendar day.
|
||||||
|
* Without this shim, hosts west of UTC see the previous UTC day for
|
||||||
|
* a local-midnight Date, causing off-by-one conversions.
|
||||||
|
*/
|
||||||
|
function localDayToUtcSlot(date: Date): Date {
|
||||||
|
return new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Conversion
|
// Conversion
|
||||||
|
|
@ -22,19 +43,39 @@ import type { HijriDate, ConversionOptions } from './types';
|
||||||
/**
|
/**
|
||||||
* Convert a Gregorian `Date` to a Hijri date object.
|
* 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
|
* Returns `null` when the date falls outside the calendar's supported range
|
||||||
* (UAQ: 1318–1500 AH / 1900–2076 CE; FCNA extends slightly further).
|
* (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 {
|
export function toHijriDate(date: Date, options?: ConversionOptions): HijriDate | null {
|
||||||
return coreToHijri(date, options);
|
return coreToHijri(localDayToUtcSlot(date), options);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a Hijri date to a Gregorian `Date`.
|
* Convert a Hijri date to a Gregorian `Date`.
|
||||||
*
|
*
|
||||||
* The returned `Date` is set to midnight UTC of the equivalent Gregorian day.
|
* Returns a **local-midnight** Date so that local field accessors
|
||||||
|
* (`getFullYear`, `getMonth`, `getDate`) and date-fns' `format()` render the
|
||||||
|
* intended calendar day on every host timezone.
|
||||||
|
*
|
||||||
|
* Round-trips exactly: `toHijriDate(fromHijriDate(y, m, d))` returns
|
||||||
|
* `{ hy: y, hm: m, hd: d }` on every timezone.
|
||||||
*
|
*
|
||||||
* @throws {Error} If the Hijri date is invalid or outside the calendar's range.
|
* @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(
|
export function fromHijriDate(
|
||||||
hy: number,
|
hy: number,
|
||||||
|
|
@ -42,11 +83,13 @@ export function fromHijriDate(
|
||||||
hd: number,
|
hd: number,
|
||||||
options?: ConversionOptions,
|
options?: ConversionOptions,
|
||||||
): Date {
|
): Date {
|
||||||
const result = coreToGregorian(hy, hm, hd, options);
|
const greg = coreToGregorian(hy, hm, hd, options);
|
||||||
if (result === null) {
|
if (greg === null) {
|
||||||
throw new Error(`Hijri date ${hy}/${hm}/${hd} is invalid or outside the supported range.`);
|
throw new Error(`Hijri date ${hy}/${hm}/${hd} is invalid or outside the supported range.`);
|
||||||
}
|
}
|
||||||
return result;
|
// coreToGregorian returns UTC midnight; lift to local midnight so that
|
||||||
|
// local field accessors and date-fns format() show the right calendar day.
|
||||||
|
return new Date(greg.getUTCFullYear(), greg.getUTCMonth(), greg.getUTCDate());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
@ -75,28 +118,34 @@ export function isValidHijriDate(
|
||||||
/**
|
/**
|
||||||
* Get the Hijri year for a Gregorian date.
|
* 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.
|
* Returns `null` when the date is outside the supported range.
|
||||||
*/
|
*/
|
||||||
export function getHijriYear(date: Date, options?: ConversionOptions): number | null {
|
export function getHijriYear(date: Date, options?: ConversionOptions): number | null {
|
||||||
return coreToHijri(date, options)?.hy ?? null;
|
return coreToHijri(localDayToUtcSlot(date), options)?.hy ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the Hijri month (1–12) for a Gregorian date.
|
* 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.
|
* Returns `null` when the date is outside the supported range.
|
||||||
*/
|
*/
|
||||||
export function getHijriMonth(date: Date, options?: ConversionOptions): number | null {
|
export function getHijriMonth(date: Date, options?: ConversionOptions): number | null {
|
||||||
return coreToHijri(date, options)?.hm ?? null;
|
return coreToHijri(localDayToUtcSlot(date), options)?.hm ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the Hijri day of month (1–30) for a Gregorian date.
|
* 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.
|
* Returns `null` when the date is outside the supported range.
|
||||||
*/
|
*/
|
||||||
export function getHijriDay(date: Date, options?: ConversionOptions): number | null {
|
export function getHijriDay(date: Date, options?: ConversionOptions): number | null {
|
||||||
return coreToHijri(date, options)?.hd ?? null;
|
return coreToHijri(localDayToUtcSlot(date), options)?.hd ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -122,7 +171,7 @@ export function getDaysInHijriMonth(hy: number, hm: number, options?: Conversion
|
||||||
*/
|
*/
|
||||||
export function getHijriMonthName(
|
export function getHijriMonthName(
|
||||||
hm: number,
|
hm: number,
|
||||||
length: 'long' | 'medium' | 'short' = 'long',
|
length: "long" | "medium" | "short" = "long",
|
||||||
): string {
|
): string {
|
||||||
if (hm < 1 || hm > 12) {
|
if (hm < 1 || hm > 12) {
|
||||||
throw new RangeError(`Hijri month must be 1–12, got ${hm}.`);
|
throw new RangeError(`Hijri month must be 1–12, got ${hm}.`);
|
||||||
|
|
@ -130,9 +179,9 @@ export function getHijriMonthName(
|
||||||
const idx = hm - 1;
|
const idx = hm - 1;
|
||||||
// Non-null: hm validated 1-12 above; idx is always 0-11, within all hm* array bounds.
|
// 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
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
if (length === 'medium') return hmMedium[idx]!;
|
if (length === "medium") return hmMedium[idx]!;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
if (length === 'short') return hmShort[idx]!;
|
if (length === "short") return hmShort[idx]!;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
return hmLong[idx]!;
|
return hmLong[idx]!;
|
||||||
}
|
}
|
||||||
|
|
@ -141,15 +190,17 @@ export function getHijriMonthName(
|
||||||
* Get the Arabic weekday name for a Gregorian date.
|
* Get the Arabic weekday name for a Gregorian date.
|
||||||
*
|
*
|
||||||
* Uses `Date.getDay()` (0 = Sunday, 6 = Saturday) as the index.
|
* 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 date - Any Gregorian `Date`.
|
||||||
* @param length - `'long'` (default) or `'short'`.
|
* @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
|
const day = date.getDay(); // 0–6
|
||||||
// Non-null: day is always 0-6 from getDay(), within hw* array bounds.
|
// Non-null: day is always 0-6 from getDay(), within hw* array bounds.
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
return length === 'short' ? hwShort[day]! : hwLong[day]!;
|
return length === "short" ? hwShort[day]! : hwLong[day]!;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
@ -162,6 +213,9 @@ const TOKEN_RE = /iYYYY|iYY|iMMMM|iMMM|iMM|iM|iDD|iD|iEEEE|iEEE|iE|ioooo|iooo/g;
|
||||||
/**
|
/**
|
||||||
* Format a Gregorian date using Hijri calendar tokens.
|
* 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:
|
* Supported tokens:
|
||||||
*
|
*
|
||||||
* | Token | Output | Example |
|
* | Token | Output | Example |
|
||||||
|
|
@ -187,70 +241,52 @@ export function formatHijriDate(
|
||||||
formatStr: string,
|
formatStr: string,
|
||||||
options?: ConversionOptions,
|
options?: ConversionOptions,
|
||||||
): string {
|
): string {
|
||||||
const h = coreToHijri(date, options);
|
const h = coreToHijri(localDayToUtcSlot(date), options);
|
||||||
if (!h) return '';
|
if (!h) return "";
|
||||||
|
|
||||||
const day = date.getDay(); // 0–6
|
const day = date.getDay(); // 0–6 local weekday — correct for display
|
||||||
|
|
||||||
return formatStr.replace(TOKEN_RE, (token): string => {
|
return formatStr.replace(TOKEN_RE, (token): string => {
|
||||||
switch (token) {
|
switch (token) {
|
||||||
case 'iYYYY':
|
case "iYYYY":
|
||||||
return String(h.hy);
|
return String(h.hy);
|
||||||
case 'iYY':
|
case "iYY":
|
||||||
return String(h.hy).slice(-2).padStart(2, '0');
|
return String(h.hy).slice(-2).padStart(2, "0");
|
||||||
case 'iMMMM':
|
case "iMMMM":
|
||||||
// Non-null: hm is a valid Hijri month 1-12; index hm-1 is within hmLong bounds.
|
// 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
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
return hmLong[h.hm - 1]!;
|
return hmLong[h.hm - 1]!;
|
||||||
case 'iMMM':
|
case "iMMM":
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
return hmMedium[h.hm - 1]!;
|
return hmMedium[h.hm - 1]!;
|
||||||
case 'iMM':
|
case "iMM":
|
||||||
return String(h.hm).padStart(2, '0');
|
return String(h.hm).padStart(2, "0");
|
||||||
case 'iM':
|
case "iM":
|
||||||
return String(h.hm);
|
return String(h.hm);
|
||||||
case 'iDD':
|
case "iDD":
|
||||||
return String(h.hd).padStart(2, '0');
|
return String(h.hd).padStart(2, "0");
|
||||||
case 'iD':
|
case "iD":
|
||||||
return String(h.hd);
|
return String(h.hd);
|
||||||
case 'iEEEE':
|
case "iEEEE":
|
||||||
// Non-null: day is always 0-6 from getDay(), within hwLong bounds.
|
// Non-null: day is always 0-6 from getDay(), within hwLong bounds.
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
return hwLong[day]!;
|
return hwLong[day]!;
|
||||||
case 'iEEE':
|
case "iEEE":
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
return hwShort[day]!;
|
return hwShort[day]!;
|
||||||
case 'iE':
|
case "iE":
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
return String(hwNumeric[day]!);
|
return String(hwNumeric[day]!);
|
||||||
case 'ioooo':
|
case "ioooo":
|
||||||
return 'AH';
|
return "AH";
|
||||||
case 'iooo':
|
case "iooo":
|
||||||
return 'AH';
|
return "AH";
|
||||||
default:
|
default:
|
||||||
return token;
|
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
|
// Arithmetic
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
@ -258,6 +294,9 @@ function utcMidnightToLocalNoon(d: Date): Date {
|
||||||
/**
|
/**
|
||||||
* Add a number of Hijri months to a Gregorian date.
|
* Add a number of Hijri months to a Gregorian date.
|
||||||
*
|
*
|
||||||
|
* The input Date is interpreted by its **local calendar day** (date-fns convention).
|
||||||
|
* Returns a **local-midnight** Date.
|
||||||
|
*
|
||||||
* Handles year rollover automatically. Month addition wraps at month 12 and
|
* 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
|
* 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.
|
* day, the day is clamped to the last day of the new month.
|
||||||
|
|
@ -265,9 +304,9 @@ function utcMidnightToLocalNoon(d: Date): Date {
|
||||||
* @throws {Error} If the resulting Hijri date is outside the supported range.
|
* @throws {Error} If the resulting Hijri date is outside the supported range.
|
||||||
*/
|
*/
|
||||||
export function addHijriMonths(date: Date, months: number, options?: ConversionOptions): Date {
|
export function addHijriMonths(date: Date, months: number, options?: ConversionOptions): Date {
|
||||||
const h = coreToHijri(date, options);
|
const h = coreToHijri(localDayToUtcSlot(date), options);
|
||||||
if (!h) {
|
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
|
// Total months from epoch: 0-based
|
||||||
|
|
@ -279,28 +318,31 @@ export function addHijriMonths(date: Date, months: number, options?: ConversionO
|
||||||
const maxDay = coreDaysInHijriMonth(newYear, newMonth, options);
|
const maxDay = coreDaysInHijriMonth(newYear, newMonth, options);
|
||||||
const newDay = Math.min(h.hd, maxDay);
|
const newDay = Math.min(h.hd, maxDay);
|
||||||
|
|
||||||
return utcMidnightToLocalNoon(fromHijriDate(newYear, newMonth, newDay, options));
|
return fromHijriDate(newYear, newMonth, newDay, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a number of Hijri years to a Gregorian date.
|
* Add a number of Hijri years to a Gregorian date.
|
||||||
*
|
*
|
||||||
* If the resulting year has a shorter Ramadan (or any month) than the original
|
* The input Date is interpreted by its **local calendar day** (date-fns convention).
|
||||||
* day, the day is clamped to the last day of that month.
|
* Returns a **local-midnight** Date.
|
||||||
|
*
|
||||||
|
* If the resulting year has a shorter month than the original day, the day is
|
||||||
|
* clamped to the last day of that month.
|
||||||
*
|
*
|
||||||
* @throws {Error} If the resulting Hijri date is outside the supported range.
|
* @throws {Error} If the resulting Hijri date is outside the supported range.
|
||||||
*/
|
*/
|
||||||
export function addHijriYears(date: Date, years: number, options?: ConversionOptions): Date {
|
export function addHijriYears(date: Date, years: number, options?: ConversionOptions): Date {
|
||||||
const h = coreToHijri(date, options);
|
const h = coreToHijri(localDayToUtcSlot(date), options);
|
||||||
if (!h) {
|
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 newYear = h.hy + years;
|
||||||
const maxDay = coreDaysInHijriMonth(newYear, h.hm, options);
|
const maxDay = coreDaysInHijriMonth(newYear, h.hm, options);
|
||||||
const newDay = Math.min(h.hd, maxDay);
|
const newDay = Math.min(h.hd, maxDay);
|
||||||
|
|
||||||
return utcMidnightToLocalNoon(fromHijriDate(newYear, h.hm, newDay, options));
|
return fromHijriDate(newYear, h.hm, newDay, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
@ -310,28 +352,34 @@ export function addHijriYears(date: Date, years: number, options?: ConversionOpt
|
||||||
/**
|
/**
|
||||||
* Get the first day of the Hijri month that contains the given date.
|
* 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.
|
* @throws {Error} If the date is outside the supported range.
|
||||||
*/
|
*/
|
||||||
export function startOfHijriMonth(date: Date, options?: ConversionOptions): Date {
|
export function startOfHijriMonth(date: Date, options?: ConversionOptions): Date {
|
||||||
const h = coreToHijri(date, options);
|
const h = coreToHijri(localDayToUtcSlot(date), options);
|
||||||
if (!h) {
|
if (!h) {
|
||||||
throw new Error('Date is outside the supported Hijri calendar range.');
|
throw new Error("Date is outside the supported Hijri calendar range.");
|
||||||
}
|
}
|
||||||
return utcMidnightToLocalNoon(fromHijriDate(h.hy, h.hm, 1, options));
|
return fromHijriDate(h.hy, h.hm, 1, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the last day of the Hijri month that contains the given date.
|
* 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.
|
* @throws {Error} If the date is outside the supported range.
|
||||||
*/
|
*/
|
||||||
export function endOfHijriMonth(date: Date, options?: ConversionOptions): Date {
|
export function endOfHijriMonth(date: Date, options?: ConversionOptions): Date {
|
||||||
const h = coreToHijri(date, options);
|
const h = coreToHijri(localDayToUtcSlot(date), options);
|
||||||
if (!h) {
|
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);
|
const lastDay = coreDaysInHijriMonth(h.hy, h.hm, options);
|
||||||
return utcMidnightToLocalNoon(fromHijriDate(h.hy, h.hm, lastDay, options));
|
return fromHijriDate(h.hy, h.hm, lastDay, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
@ -341,11 +389,13 @@ export function endOfHijriMonth(date: Date, options?: ConversionOptions): Date {
|
||||||
/**
|
/**
|
||||||
* Check whether two Gregorian dates fall in the same Hijri month.
|
* 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.
|
* Returns `false` if either date is outside the supported range.
|
||||||
*/
|
*/
|
||||||
export function isSameHijriMonth(dateA: Date, dateB: Date, options?: ConversionOptions): boolean {
|
export function isSameHijriMonth(dateA: Date, dateB: Date, options?: ConversionOptions): boolean {
|
||||||
const a = coreToHijri(dateA, options);
|
const a = coreToHijri(localDayToUtcSlot(dateA), options);
|
||||||
const b = coreToHijri(dateB, options);
|
const b = coreToHijri(localDayToUtcSlot(dateB), options);
|
||||||
if (!a || !b) return false;
|
if (!a || !b) return false;
|
||||||
return a.hy === b.hy && a.hm === b.hm;
|
return a.hy === b.hy && a.hm === b.hm;
|
||||||
}
|
}
|
||||||
|
|
@ -353,11 +403,13 @@ export function isSameHijriMonth(dateA: Date, dateB: Date, options?: ConversionO
|
||||||
/**
|
/**
|
||||||
* Check whether two Gregorian dates fall in the same Hijri year.
|
* 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.
|
* Returns `false` if either date is outside the supported range.
|
||||||
*/
|
*/
|
||||||
export function isSameHijriYear(dateA: Date, dateB: Date, options?: ConversionOptions): boolean {
|
export function isSameHijriYear(dateA: Date, dateB: Date, options?: ConversionOptions): boolean {
|
||||||
const a = coreToHijri(dateA, options);
|
const a = coreToHijri(localDayToUtcSlot(dateA), options);
|
||||||
const b = coreToHijri(dateB, options);
|
const b = coreToHijri(localDayToUtcSlot(dateB), options);
|
||||||
if (!a || !b) return false;
|
if (!a || !b) return false;
|
||||||
return a.hy === b.hy;
|
return a.hy === b.hy;
|
||||||
}
|
}
|
||||||
|
|
@ -369,12 +421,23 @@ export function isSameHijriYear(dateA: Date, dateB: Date, options?: ConversionOp
|
||||||
/**
|
/**
|
||||||
* Get the Hijri quarter (1–4) for a Gregorian date.
|
* 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.
|
* Months 1–3 = Q1, 4–6 = Q2, 7–9 = Q3, 10–12 = Q4.
|
||||||
*
|
*
|
||||||
* Returns `null` when the date is outside the supported range.
|
* Returns `null` when the date is outside the supported range.
|
||||||
*/
|
*/
|
||||||
export function getHijriQuarter(date: Date, options?: ConversionOptions): number | null {
|
export function getHijriQuarter(date: Date, options?: ConversionOptions): number | null {
|
||||||
const h = coreToHijri(date, options);
|
const h = coreToHijri(localDayToUtcSlot(date), options);
|
||||||
if (!h) return null;
|
if (!h) return null;
|
||||||
return Math.ceil(h.hm / 3);
|
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";
|
||||||
|
|
|
||||||
|
|
@ -26,11 +26,12 @@ describe('CJS: toHijriDate', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('CJS: fromHijriDate', () => {
|
describe('CJS: fromHijriDate', () => {
|
||||||
it('converts to correct Gregorian date', () => {
|
it('converts to correct Gregorian date (local midnight)', () => {
|
||||||
const d = fromHijriDate(1444, 9, 1);
|
const d = fromHijriDate(1444, 9, 1);
|
||||||
assert.equal(d.getUTCFullYear(), 2023);
|
// Returns local midnight — use local accessors, not UTC
|
||||||
assert.equal(d.getUTCMonth(), 2);
|
assert.equal(d.getFullYear(), 2023);
|
||||||
assert.equal(d.getUTCDate(), 23);
|
assert.equal(d.getMonth(), 2);
|
||||||
|
assert.equal(d.getDate(), 23);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
43
test.mjs
43
test.mjs
|
|
@ -43,21 +43,48 @@ describe('toHijriDate', () => {
|
||||||
const h = toHijriDate(new Date(1800, 0, 1));
|
const h = toHijriDate(new Date(1800, 0, 1));
|
||||||
assert.equal(h, null);
|
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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('fromHijriDate', () => {
|
describe('fromHijriDate', () => {
|
||||||
it('1 Ramadan 1444 -> 2023-03-23', () => {
|
it('1 Ramadan 1444 -> local 2023-03-23', () => {
|
||||||
const d = fromHijriDate(1444, 9, 1);
|
const d = fromHijriDate(1444, 9, 1);
|
||||||
assert.equal(d.getUTCFullYear(), 2023);
|
// Returns local midnight: local accessors show the intended calendar day
|
||||||
assert.equal(d.getUTCMonth(), 2);
|
assert.equal(d.getFullYear(), 2023);
|
||||||
assert.equal(d.getUTCDate(), 23);
|
assert.equal(d.getMonth(), 2);
|
||||||
|
assert.equal(d.getDate(), 23);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('1 Muharram 1446 -> 2024-07-07', () => {
|
it('1 Muharram 1446 -> local 2024-07-07', () => {
|
||||||
const d = fromHijriDate(1446, 1, 1);
|
const d = fromHijriDate(1446, 1, 1);
|
||||||
assert.equal(d.getUTCFullYear(), 2024);
|
assert.equal(d.getFullYear(), 2024);
|
||||||
assert.equal(d.getUTCMonth(), 6);
|
assert.equal(d.getMonth(), 6);
|
||||||
assert.equal(d.getUTCDate(), 7);
|
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', () => {
|
it('throws on invalid month', () => {
|
||||||
|
|
|
||||||
9
vitest.config.ts
Normal file
9
vitest.config.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { defineConfig } from "vitest/config";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
globals: true,
|
||||||
|
environment: "node",
|
||||||
|
include: ["date-fns-hijri.test.ts"],
|
||||||
|
},
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue