mirror of
https://github.com/acamarata/hijri-core-dart.git
synced 2026-06-30 18:54:27 +00:00
fix: interpret DateTime by UTC calendar day for exact round-trips
toHijri in both UAQ and FCNA engines read .year/.month/.day from the input DateTime — local calendar components — then passed them to DateTime.utc(). On hosts west of UTC a UTC-midnight local DateTime resolves to the previous local day, causing toHijri(toGregorian(y,m,d)) to return the wrong Hijri day. Fix: call date.toUtc() first in both engines before extracting calendar components. This is symmetric with toGregorian which always returns DateTime.utc(). Adds 3 regression tests covering the round-trip and the local-DateTime-on-UTC-west-host case.
This commit is contained in:
parent
5dd4cecc12
commit
c79368a4ee
4 changed files with 57 additions and 5 deletions
10
CHANGELOG.md
10
CHANGELOG.md
|
|
@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
|
|||
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
This project adheres to [Semantic Versioning](https://semver.org/).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- `toHijri` now normalizes the input `DateTime` to its UTC calendar day before
|
||||
lookup (via `date.toUtc()`), matching the UTC-midnight contract of `toGregorian`.
|
||||
Previously, passing a local `DateTime` on a host west of UTC could return the
|
||||
previous Hijri day, breaking `toHijri(toGregorian(y, m, d))` round-trips.
|
||||
Applies to both the UAQ and FCNA engines.
|
||||
|
||||
## [1.0.0] - 2026-05-25
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -234,12 +234,15 @@ int _fcnaDaysInMonth(int hy, int hm) {
|
|||
// ---- FCNA Gregorian -> Hijri ----
|
||||
|
||||
HijriDate? _fcnaToHijri(DateTime date) {
|
||||
// FCNA criterion is UTC-based, so UTC date components ensure correct round-trips.
|
||||
// Normalize to UTC calendar day so the result is deterministic regardless of
|
||||
// the host timezone and whether the caller passes a UTC or local DateTime.
|
||||
// This is symmetric with toGregorian, which always returns DateTime.utc().
|
||||
final d = date.toUtc();
|
||||
final inputMs =
|
||||
DateTime.utc(
|
||||
date.year,
|
||||
date.month,
|
||||
date.day,
|
||||
d.year,
|
||||
d.month,
|
||||
d.day,
|
||||
).millisecondsSinceEpoch.toDouble();
|
||||
|
||||
final kApprox = _utcMsToKApprox(inputMs - 15 * msPerDay);
|
||||
|
|
|
|||
|
|
@ -36,7 +36,11 @@ int _dateUtcMs(int year, int month, int day) {
|
|||
}
|
||||
|
||||
HijriDate? _uaqToHijri(DateTime date) {
|
||||
final inputUtc = _dateUtcMs(date.year, date.month, date.day);
|
||||
// Normalize to UTC calendar day so the result is deterministic regardless of
|
||||
// the host timezone and whether the caller passes a UTC or local DateTime.
|
||||
// This is symmetric with toGregorian, which always returns DateTime.utc().
|
||||
final d = date.toUtc();
|
||||
final inputUtc = _dateUtcMs(d.year, d.month, d.day);
|
||||
|
||||
// Binary search: find the last table entry whose Gregorian start date <= input.
|
||||
int lo = 0;
|
||||
|
|
|
|||
|
|
@ -93,6 +93,41 @@ void main() {
|
|||
});
|
||||
});
|
||||
|
||||
// ---- UAQ round-trips (UTC day boundary) ----
|
||||
|
||||
group('UAQ round-trips', () {
|
||||
test('toHijri(toGregorian(1446,9,1)) == 1446/9/1', () {
|
||||
final greg = toGregorian(1446, 9, 1);
|
||||
expect(greg, isNotNull);
|
||||
final h = toHijri(greg!);
|
||||
expect(h, isNotNull);
|
||||
expect(h!.hy, equals(1446));
|
||||
expect(h.hm, equals(9));
|
||||
expect(h.hd, equals(1));
|
||||
});
|
||||
test('local DateTime with same UTC day round-trips correctly', () {
|
||||
// toGregorian returns DateTime.utc(); converting it to local must still
|
||||
// produce the correct Hijri day (regression for UTC-west hosts).
|
||||
final greg = toGregorian(1446, 9, 1);
|
||||
expect(greg, isNotNull);
|
||||
final localGreg = greg!.toLocal();
|
||||
final h = toHijri(localGreg);
|
||||
expect(h, isNotNull);
|
||||
expect(h!.hy, equals(1446));
|
||||
expect(h.hm, equals(9));
|
||||
expect(h.hd, equals(1));
|
||||
});
|
||||
test('toHijri(toGregorian(1318,1,1)) == 1318/1/1 (table lower bound)', () {
|
||||
final greg = toGregorian(1318, 1, 1);
|
||||
expect(greg, isNotNull);
|
||||
final h = toHijri(greg!);
|
||||
expect(h, isNotNull);
|
||||
expect(h!.hy, equals(1318));
|
||||
expect(h.hm, equals(1));
|
||||
expect(h.hd, equals(1));
|
||||
});
|
||||
});
|
||||
|
||||
// ---- UAQ isValid ----
|
||||
|
||||
group('UAQ isValid', () {
|
||||
|
|
|
|||
Loading…
Reference in a new issue