mirror of
https://github.com/acamarata/hijri-core.git
synced 2026-06-30 18:54:27 +00:00
Bug: uaqToHijri read local date components (getFullYear/getMonth/getDate) and passed them to Date.UTC(), producing a UTC midnight that does not match the input's intended Gregorian day on hosts west of UTC. Concretely, a Date returned by toGregorian() (UTC midnight) would map to the *previous* Hijri day on UTC-5 or UTC-13 hosts, breaking the toHijri(toGregorian(hy,hm,hd)) round-trip. Fix: switch line 44 to read UTC components (getUTCFullYear/getUTCMonth/ getUTCDate), matching how the FCNA engine already worked. Both engines now share the same UTC-day contract: toHijri reads the UTC calendar day of the input, and toGregorian returns a UTC-midnight Date. Round-trips are exact; results are host-timezone-independent. Behavior change: on non-UTC hosts, toHijri results may shift to the UTC calendar day rather than the local calendar day. Users passing local wall-clock dates should use new Date(Date.UTC(y, m-1, d)). Also: - Fix misleading comment in uaqToHijri (previously claimed local components were "timezone-safe") - Add UTC-day contract to toHijri JSDoc in src/index.ts - Fix wrong constraint comment in hijri-core.test.ts header - Add "day boundaries (UTC contract)" describe block to vitest suite - Convert LOCAL Date constructors in test.mjs and test-cjs.cjs to Date.UTC() form; add UAQ round-trip assertion to test.mjs - Add "Day boundaries and time zones" section to README.md - Add [Unreleased] Fixed entry to CHANGELOG.md
140 lines
4.6 KiB
TypeScript
140 lines
4.6 KiB
TypeScript
/**
|
||
* Purpose: Vitest suite for hijri-core — conversion, validation, registry, and name exports.
|
||
* Inputs: Pure functions from src/index.ts (no network; module-load registers uaq+fcna engines).
|
||
* Outputs: Vitest pass/fail assertions.
|
||
* Constraints: UAQ calendar covers 1318–1500 AH (≈1900–2076 CE). toHijri interprets the input
|
||
* Date by its UTC calendar day (getUTC* components); pass UTC-explicit Dates for
|
||
* deterministic results. toGregorian returns UTC midnight, so round-trips are exact.
|
||
* Usage: pnpm vitest run
|
||
* SOT: packages.md — hijri-core row
|
||
*/
|
||
import { describe, it, expect } from "vitest";
|
||
import {
|
||
toHijri,
|
||
toGregorian,
|
||
isValidHijriDate,
|
||
daysInHijriMonth,
|
||
listCalendars,
|
||
hmLong,
|
||
hwLong,
|
||
} from "./src/index";
|
||
|
||
describe("toGregorian (UAQ default)", () => {
|
||
it("converts 1 Ramadan 1446 to 2025-03-01 UTC midnight", () => {
|
||
const result = toGregorian(1446, 9, 1);
|
||
expect(result).not.toBeNull();
|
||
expect(result!.toISOString()).toBe("2025-03-01T00:00:00.000Z");
|
||
});
|
||
|
||
it("converts 1 Muharram 1446 to 2024-07-07 UTC midnight", () => {
|
||
const result = toGregorian(1446, 1, 1);
|
||
expect(result).not.toBeNull();
|
||
expect(result!.toISOString()).toBe("2024-07-07T00:00:00.000Z");
|
||
});
|
||
|
||
it("returns null for an out-of-range Hijri year (1501)", () => {
|
||
expect(toGregorian(1501, 1, 1)).toBeNull();
|
||
});
|
||
});
|
||
|
||
describe("toHijri (UAQ default)", () => {
|
||
it("converts 2025-03-01 noon UTC to 1 Ramadan 1446", () => {
|
||
// toGregorian(1446,9,1) = 2025-03-01 midnight; add 12h to stay on that Gregorian day
|
||
const noonOnRamadanStart = new Date("2025-03-01T12:00:00Z");
|
||
const result = toHijri(noonOnRamadanStart);
|
||
expect(result).not.toBeNull();
|
||
expect(result!.hy).toBe(1446);
|
||
expect(result!.hm).toBe(9);
|
||
expect(result!.hd).toBe(1);
|
||
});
|
||
|
||
it("returns null for a date outside UAQ range (year 2100)", () => {
|
||
expect(toHijri(new Date("2100-01-01T12:00:00Z"))).toBeNull();
|
||
});
|
||
|
||
it("throws on an invalid Date object", () => {
|
||
expect(() => toHijri(new Date("invalid"))).toThrow();
|
||
});
|
||
});
|
||
|
||
describe("isValidHijriDate", () => {
|
||
it("accepts a known valid date", () => {
|
||
expect(isValidHijriDate(1446, 9, 1)).toBe(true);
|
||
});
|
||
|
||
it("rejects month 13", () => {
|
||
expect(isValidHijriDate(1446, 13, 1)).toBe(false);
|
||
});
|
||
|
||
it("rejects day 0", () => {
|
||
expect(isValidHijriDate(1446, 1, 0)).toBe(false);
|
||
});
|
||
});
|
||
|
||
describe("daysInHijriMonth", () => {
|
||
it("returns 29 or 30 for a valid month", () => {
|
||
const days = daysInHijriMonth(1446, 9);
|
||
expect([29, 30]).toContain(days);
|
||
});
|
||
});
|
||
|
||
describe("registry", () => {
|
||
it("lists at least uaq and fcna after module load", () => {
|
||
const calendars = listCalendars();
|
||
expect(calendars).toContain("uaq");
|
||
expect(calendars).toContain("fcna");
|
||
});
|
||
});
|
||
|
||
describe("name tables", () => {
|
||
it("hmLong has 12 entries and index 8 is Ramadan", () => {
|
||
expect(hmLong).toHaveLength(12);
|
||
expect(hmLong[8]).toBe("Ramadan");
|
||
});
|
||
|
||
it("hwLong has 7 weekday entries", () => {
|
||
expect(hwLong).toHaveLength(7);
|
||
});
|
||
});
|
||
|
||
describe("day boundaries (UTC contract)", () => {
|
||
it("UAQ round-trip: toHijri(toGregorian(1446, 9, 1)) returns {hy:1446, hm:9, hd:1}", () => {
|
||
const greg = toGregorian(1446, 9, 1);
|
||
expect(greg).not.toBeNull();
|
||
const hijri = toHijri(greg!);
|
||
expect(hijri).not.toBeNull();
|
||
expect(hijri!.hy).toBe(1446);
|
||
expect(hijri!.hm).toBe(9);
|
||
expect(hijri!.hd).toBe(1);
|
||
});
|
||
|
||
it("toHijri(new Date('2025-03-01T00:00:00Z')) = 1 Ramadan 1446", () => {
|
||
const result = toHijri(new Date("2025-03-01T00:00:00Z"));
|
||
expect(result).not.toBeNull();
|
||
expect(result!.hy).toBe(1446);
|
||
expect(result!.hm).toBe(9);
|
||
expect(result!.hd).toBe(1);
|
||
});
|
||
|
||
it("last ms of 2025-02-28 UTC maps to the same Hijri day as noon on 2025-02-28 UTC", () => {
|
||
const lastMs = toHijri(new Date("2025-02-28T23:59:59.999Z"));
|
||
const noon = toHijri(new Date("2025-02-28T12:00:00Z"));
|
||
expect(lastMs).not.toBeNull();
|
||
expect(noon).not.toBeNull();
|
||
expect(lastMs!.hy).toBe(noon!.hy);
|
||
expect(lastMs!.hm).toBe(noon!.hm);
|
||
expect(lastMs!.hd).toBe(noon!.hd);
|
||
// and it is the day before 1 Ramadan 1446
|
||
expect(lastMs!.hm).not.toBe(9);
|
||
});
|
||
|
||
it("FCNA round-trip: toHijri(toGregorian(1446, 9, 1, {calendar:'fcna'}), {calendar:'fcna'}) is exact", () => {
|
||
const greg = toGregorian(1446, 9, 1, { calendar: "fcna" });
|
||
expect(greg).not.toBeNull();
|
||
const hijri = toHijri(greg!, { calendar: "fcna" });
|
||
expect(hijri).not.toBeNull();
|
||
expect(hijri!.hy).toBe(1446);
|
||
expect(hijri!.hm).toBe(9);
|
||
expect(hijri!.hd).toBe(1);
|
||
});
|
||
});
|