diff --git a/CHANGELOG.md b/CHANGELOG.md index 119f3d3..815706a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed +- `.toHijri()` now converts the calendar date the dayjs instance displays (via `Date.UTC(year, month, date)`) instead of passing the raw instant to hijri-core. Fixes wrong-Hijri-day results around UTC-midnight instants on hosts east or west of UTC. Lock-step with the unreleased hijri-core `fix/utc-day-boundary` fix. + ## [1.0.2] - 2026-05-30 ### Changed diff --git a/README.md b/README.md index bf2f1a1..8680a6f 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,12 @@ dayjs.fromHijri(1444, 10, 1).format('YYYY-MM-DD'); // '2023-04-21' Full API reference, examples, and architecture notes are on the [GitHub Wiki](https://github.com/acamarata/dayjs-hijri-plus/wiki). +## Day boundaries and time zones + +`.toHijri()` converts the calendar date the dayjs instance displays — the same date you would read off the screen — regardless of the host's system timezone or whether the dayjs `utc` plugin is active. A call like `dayjs('2025-03-01').toHijri()` always maps the 1st of March 2025, not whatever local instant that string resolves to in UTC. + +Religious start-of-day at sunset is out of scope. Sunset-aware day boundaries require external prayer-time data and are not handled here. + ## Related - [hijri-core](https://github.com/acamarata/hijri-core): the zero-dependency Hijri calendar engine this plugin wraps diff --git a/src/index.ts b/src/index.ts index d7b04ff..473d381 100644 --- a/src/index.ts +++ b/src/index.ts @@ -167,7 +167,11 @@ const plugin: PluginFunc = (_option, dayjsClass, dayjsFactory) => { // ------------------------------------------------------------------ // dayjsClass.prototype.toHijri = function (opts?: ConversionOptions): HijriDate | null { - return toHijri(this.toDate(), opts); + // Build a UTC-noon Date from the calendar date this instance displays so + // that hijri-core's UTC-day contract reads the correct day regardless of + // the host timezone or whether the dayjs utc plugin is active. + // dayjs .month() is 0-based, matching Date.UTC's month parameter. + return toHijri(new Date(Date.UTC(this.year(), this.month(), this.date())), opts); }; dayjsClass.prototype.isValidHijri = function (opts?: ConversionOptions): boolean { @@ -262,9 +266,11 @@ const plugin: PluginFunc = (_option, dayjsClass, dayjsFactory) => { if (!greg) { throw new Error(`Invalid or out-of-range Hijri date: ${hy}/${hm}/${hd}`); } - // Construct from ISO date string to avoid timezone offset issues. - // dayjsFactory(Date) interprets the Date in local time; a UTC-midnight Date - // in western timezones would resolve to the previous local day. + // Construct from an ISO date string (YYYY-MM-DD) so the result is the + // Gregorian calendar day that corresponds to the Hijri date, at local + // midnight in whatever timezone the consumer uses. Passing a raw Date + // object to dayjsFactory() would interpret it as a UTC instant and could + // land on the previous local day for hosts west of UTC. const y = greg.getUTCFullYear(); const mo = String(greg.getUTCMonth() + 1).padStart(2, "0"); const dy = String(greg.getUTCDate()).padStart(2, "0"); diff --git a/test-cjs.cjs b/test-cjs.cjs index 29bf299..2a679da 100644 --- a/test-cjs.cjs +++ b/test-cjs.cjs @@ -59,3 +59,16 @@ describe('CJS: isValidHijri', () => { assert.equal(dayjs(D_RAMADAN_1444).isValidHijri(), true); }); }); + +describe('CJS: UTC-day boundary (regression)', () => { + it('dayjs("2025-03-01").toHijri() -> 1 Ramadan 1446', () => { + const h = dayjs('2025-03-01').toHijri(); + assert.deepEqual(h, { hy: 1446, hm: 9, hd: 1 }); + }); + + it('round-trip: fromHijri(1446,9,1) then toHijri() -> {1446,9,1}', () => { + const d = dayjs.fromHijri(1446, 9, 1); + const h = d.toHijri(); + assert.deepEqual(h, { hy: 1446, hm: 9, hd: 1 }); + }); +}); diff --git a/test.mjs b/test.mjs index 98c3f0a..6791ec2 100644 --- a/test.mjs +++ b/test.mjs @@ -102,3 +102,18 @@ describe('isValidHijri', () => { assert.equal(valid, true); }); }); + +describe('UTC-day boundary (regression)', () => { + // dayjs("YYYY-MM-DD") parses as local midnight — timezone-invariant anchor + // for toHijri now that the adapter reads the displayed calendar date. + it('dayjs("2025-03-01").toHijri() -> 1 Ramadan 1446', () => { + const h = dayjs('2025-03-01').toHijri(); + assert.deepEqual(h, { hy: 1446, hm: 9, hd: 1 }); + }); + + it('round-trip: fromHijri(1446,9,1) then toHijri() -> {1446,9,1}', () => { + const d = dayjs.fromHijri(1446, 9, 1); + const h = d.toHijri(); + assert.deepEqual(h, { hy: 1446, hm: 9, hd: 1 }); + }); +});