mirror of
https://github.com/acamarata/date-fns-hijri.git
synced 2026-06-30 18:54:25 +00:00
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.
346 lines
9.5 KiB
JavaScript
346 lines
9.5 KiB
JavaScript
import { describe, it } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import {
|
|
toHijriDate,
|
|
fromHijriDate,
|
|
isValidHijriDate,
|
|
getHijriYear,
|
|
getHijriMonth,
|
|
getHijriDay,
|
|
getDaysInHijriMonth,
|
|
getHijriMonthName,
|
|
getHijriWeekdayName,
|
|
formatHijriDate,
|
|
addHijriMonths,
|
|
addHijriYears,
|
|
startOfHijriMonth,
|
|
endOfHijriMonth,
|
|
isSameHijriMonth,
|
|
isSameHijriYear,
|
|
getHijriQuarter,
|
|
} from './dist/index.mjs';
|
|
|
|
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);
|
|
});
|
|
});
|
|
|
|
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);
|
|
});
|
|
});
|
|
|
|
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);
|
|
});
|
|
});
|
|
|
|
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);
|
|
});
|
|
});
|
|
|
|
describe('getDaysInHijriMonth', () => {
|
|
it('Ramadan 1444', () => {
|
|
const days = getDaysInHijriMonth(1444, 9);
|
|
assert.ok(days === 29 || days === 30, `expected 29 or 30, got ${days}`);
|
|
});
|
|
|
|
it('month 1 of 1444', () => {
|
|
const days = getDaysInHijriMonth(1444, 1);
|
|
assert.ok(days === 29 || days === 30, `expected 29 or 30, got ${days}`);
|
|
});
|
|
});
|
|
|
|
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);
|
|
});
|
|
});
|
|
|
|
describe('getHijriWeekdayName', () => {
|
|
it('Thursday long', () => {
|
|
assert.equal(getHijriWeekdayName(new Date(2023, 2, 23)), 'Yawm al-Khamis');
|
|
});
|
|
|
|
it('Thursday short', () => {
|
|
assert.equal(getHijriWeekdayName(new Date(2023, 2, 23), 'short'), 'Kham');
|
|
});
|
|
});
|
|
|
|
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');
|
|
});
|
|
});
|
|
|
|
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);
|
|
});
|
|
});
|
|
|
|
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);
|
|
});
|
|
});
|
|
|
|
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}`);
|
|
});
|
|
});
|
|
|
|
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);
|
|
});
|
|
});
|
|
|
|
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);
|
|
});
|
|
});
|
|
|
|
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);
|
|
});
|
|
|
|
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');
|
|
});
|
|
});
|