mirror of
https://github.com/acamarata/moment-hijri-plus.git
synced 2026-06-30 18:54:29 +00:00
fix: convert the displayed calendar date in toHijri for hijri-core's UTC-day contract
moment.fn.toHijri now passes new Date(Date.UTC(this.year(), this.month(), this.date())) to hijri-core instead of the raw instant (this.toDate()). This converts the calendar date the moment instance displays, respecting utc() mode, rather than the underlying millisecond value — eliminating wrong-Hijri-day results around UTC-midnight for hosts east or west of UTC. Lock-step with hijri-core fix/utc-day-boundary (commit 3419378). fromHijri path was already correct; its comment updated for clarity.
This commit is contained in:
parent
1e3044c859
commit
c6e9a49d19
5 changed files with 71 additions and 5 deletions
|
|
@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- `.toHijri()` now converts the calendar date the moment instance displays (year/month/day
|
||||||
|
components, respecting `.utc()` mode) rather than passing the raw instant to hijri-core.
|
||||||
|
This eliminates wrong-Hijri-day results around UTC-midnight for hosts east or west of UTC.
|
||||||
|
Lock-step with the unreleased hijri-core fix on `fix/utc-day-boundary`.
|
||||||
|
|
||||||
## [1.0.2] - 2026-05-30
|
## [1.0.2] - 2026-05-30
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,15 @@ Pass `{ calendar: 'fcna' }` to switch from the default Umm al-Qura calendar to F
|
||||||
Full API reference, format token table, and examples are in the
|
Full API reference, format token table, and examples are in the
|
||||||
[project wiki](https://github.com/acamarata/moment-hijri-plus/wiki).
|
[project wiki](https://github.com/acamarata/moment-hijri-plus/wiki).
|
||||||
|
|
||||||
|
## Day boundaries and time zones
|
||||||
|
|
||||||
|
Conversions use the calendar date the moment instance displays, not the underlying UTC
|
||||||
|
instant. A `moment("2025-03-01")` parsed in any local timezone returns the Hijri date
|
||||||
|
for March 1st, 2025. A moment created with `.utc()` uses its UTC components.
|
||||||
|
|
||||||
|
Religious day-start at sunset is outside the scope of this package; it depends on
|
||||||
|
location and madhab, and must be handled at the application layer.
|
||||||
|
|
||||||
## Note on Moment.js
|
## Note on Moment.js
|
||||||
|
|
||||||
Moment.js is in maintenance mode. For new projects,
|
Moment.js is in maintenance mode. For new projects,
|
||||||
|
|
|
||||||
19
src/index.ts
19
src/index.ts
|
|
@ -28,8 +28,13 @@ declare module "moment" {
|
||||||
/**
|
/**
|
||||||
* Convert this moment to a Hijri date.
|
* Convert this moment to a Hijri date.
|
||||||
*
|
*
|
||||||
* Passes the underlying `Date` to hijri-core's `toHijri()`. The calendar engine
|
* Converts the calendar date this moment instance displays (year/month/day) to a
|
||||||
* performs a table lookup (UAQ) or astronomical calculation (FCNA).
|
* Hijri date via hijri-core's `toHijri()`. The conversion is independent of the
|
||||||
|
* host machine's timezone: a moment in UTC mode uses its UTC components, and a
|
||||||
|
* moment in local mode uses its local components. The raw instant (milliseconds
|
||||||
|
* since epoch) is never passed directly to the calendar engine.
|
||||||
|
*
|
||||||
|
* The calendar engine performs a table lookup (UAQ) or astronomical calculation (FCNA).
|
||||||
*
|
*
|
||||||
* @param options - Calendar selection. Default: `{ calendar: 'uaq' }`.
|
* @param options - Calendar selection. Default: `{ calendar: 'uaq' }`.
|
||||||
* @returns A `HijriDate` object `{ hy, hm, hd }`, or `null` if the date is outside
|
* @returns A `HijriDate` object `{ hy, hm, hd }`, or `null` if the date is outside
|
||||||
|
|
@ -149,7 +154,11 @@ function escapeLiteral(value: string): string {
|
||||||
*/
|
*/
|
||||||
function install(momentInstance: typeof moment): void {
|
function install(momentInstance: typeof moment): void {
|
||||||
momentInstance.fn.toHijri = function (opts?: ConversionOptions): HijriDate | null {
|
momentInstance.fn.toHijri = function (opts?: ConversionOptions): HijriDate | null {
|
||||||
return toHijri(this.toDate(), opts);
|
// Use the calendar date this instance displays rather than the raw instant.
|
||||||
|
// this.year()/.month()/.date() respect utc mode automatically, so a moment
|
||||||
|
// created with .utc() uses UTC components and a local moment uses local components.
|
||||||
|
// moment.month() is 0-based, matching Date.UTC's month parameter exactly.
|
||||||
|
return toHijri(new Date(Date.UTC(this.year(), this.month(), this.date())), opts);
|
||||||
};
|
};
|
||||||
|
|
||||||
momentInstance.fn.hijriYear = function (opts?: ConversionOptions): number | null {
|
momentInstance.fn.hijriYear = function (opts?: ConversionOptions): number | null {
|
||||||
|
|
@ -236,8 +245,8 @@ function install(momentInstance: typeof moment): void {
|
||||||
if (!greg) {
|
if (!greg) {
|
||||||
throw new Error(`Invalid or out-of-range Hijri date: ${hy}/${hm}/${hd}`);
|
throw new Error(`Invalid or out-of-range Hijri date: ${hy}/${hm}/${hd}`);
|
||||||
}
|
}
|
||||||
// Construct from explicit year/month/day to avoid UTC-to-local timezone
|
// Construct from explicit year/month/day components so the moment displays
|
||||||
// shift when the Date object represents midnight UTC.
|
// the correct calendar date regardless of the host timezone offset.
|
||||||
return momentInstance([greg.getUTCFullYear(), greg.getUTCMonth(), greg.getUTCDate()]);
|
return momentInstance([greg.getUTCFullYear(), greg.getUTCMonth(), greg.getUTCDate()]);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
19
test-cjs.cjs
19
test-cjs.cjs
|
|
@ -61,3 +61,22 @@ describe('CJS: isValidHijri', () => {
|
||||||
assert.equal(moment(new Date(2023, 2, 23, 12)).isValidHijri(), true);
|
assert.equal(moment(new Date(2023, 2, 23, 12)).isValidHijri(), true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('CJS: UTC-day boundary (regression)', () => {
|
||||||
|
it('fromHijri → toHijri round-trip: 1446/9/1', () => {
|
||||||
|
const m = moment.fromHijri(1446, 9, 1);
|
||||||
|
const h = m.toHijri();
|
||||||
|
assert.notEqual(h, null);
|
||||||
|
assert.equal(h.hy, 1446);
|
||||||
|
assert.equal(h.hm, 9);
|
||||||
|
assert.equal(h.hd, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('moment("2025-03-01") toHijri => 1446/9/1 (timezone-invariant)', () => {
|
||||||
|
const h = moment('2025-03-01').toHijri();
|
||||||
|
assert.notEqual(h, null);
|
||||||
|
assert.equal(h.hy, 1446);
|
||||||
|
assert.equal(h.hm, 9);
|
||||||
|
assert.equal(h.hd, 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
23
test.mjs
23
test.mjs
|
|
@ -110,3 +110,26 @@ describe('FCNA calendar', () => {
|
||||||
assert.equal(typeof h.hd, 'number');
|
assert.equal(typeof h.hd, 'number');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('UTC-day boundary (regression)', () => {
|
||||||
|
it('fromHijri → toHijri round-trip: 1446/9/1', () => {
|
||||||
|
// Construct from Hijri, convert back — must be exact regardless of host TZ.
|
||||||
|
const m = moment.fromHijri(1446, 9, 1);
|
||||||
|
const h = m.toHijri();
|
||||||
|
assert.notEqual(h, null);
|
||||||
|
assert.equal(h.hy, 1446);
|
||||||
|
assert.equal(h.hm, 9);
|
||||||
|
assert.equal(h.hd, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('moment("2025-03-01") toHijri => 1446/9/1 (timezone-invariant)', () => {
|
||||||
|
// moment parses date-only ISO strings as LOCAL midnight.
|
||||||
|
// toHijri must convert the displayed calendar date (2025-03-01), not the raw
|
||||||
|
// instant, so the result is the same regardless of the host timezone offset.
|
||||||
|
const h = moment('2025-03-01').toHijri();
|
||||||
|
assert.notEqual(h, null);
|
||||||
|
assert.equal(h.hy, 1446);
|
||||||
|
assert.equal(h.hm, 9);
|
||||||
|
assert.equal(h.hd, 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue