mirror of
https://github.com/acamarata/pray-calc-dart.git
synced 2026-06-30 19:04:25 +00:00
fix: normalize date to UTC calendar day so prayer times are host-timezone-independent
getTimes now constructs civDate = DateTime.utc(date.year, date.month, date.day, 12, 0, 0) at entry and passes it to both getSpa and getAngles. Previously a local DateTime in a positive-UTC-offset zone (e.g. UTC+12) would reach getSpa.toUtc() as the previous UTC calendar day, shifting all prayer times by ~24 h. Regression tests added for UTC/local DateTime consistency across UTC, EDT, and Auckland host timezones (27 tests, all pass).
This commit is contained in:
parent
fc01b129f4
commit
178e990cbc
4 changed files with 80 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
|
||||
|
||||
- Prayer times are now host-timezone-independent. `getTimes` normalizes the
|
||||
caller's `date` to a stable UTC-noon reference (`DateTime.utc(y, m, d, 12)`)
|
||||
before passing it to `getSpa` and all astronomical calculations. Previously,
|
||||
a local `DateTime(2024, 3, 15)` in a UTC+12 zone would reach `getSpa` as
|
||||
UTC March 14, shifting all times by one civil day.
|
||||
|
||||
## [1.0.0] - 2026-05-25
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -68,6 +68,8 @@ TwilightAngles getAngles(
|
|||
double pressure = 1013.25,
|
||||
}) {
|
||||
// 1. Solar ephemeris at UTC noon of the given date.
|
||||
// date.year/month/day are read without TZ conversion so they reflect the
|
||||
// caller's expressed civil date for both local and UTC DateTime inputs.
|
||||
final noonDate = DateTime.utc(date.year, date.month, date.day, 12, 0, 0);
|
||||
final jd = toJulianDate(noonDate);
|
||||
final eph = solarEphemeris(jd);
|
||||
|
|
|
|||
|
|
@ -33,9 +33,17 @@ PrayerTimes getTimes(
|
|||
double pressure = 1013.25,
|
||||
bool hanafi = false,
|
||||
}) {
|
||||
// Normalize to a stable UTC-noon DateTime for this civil calendar date.
|
||||
// Reading date.year/month/day directly (without TZ conversion) preserves
|
||||
// the caller's expressed date regardless of whether they passed a local or
|
||||
// UTC DateTime. Constructing UTC noon removes host-timezone influence from
|
||||
// every downstream Julian-Day computation and aligns getSpa with the
|
||||
// Meeus/MSC calculations (which all need the same civil date).
|
||||
final civDate = DateTime.utc(date.year, date.month, date.day, 12, 0, 0);
|
||||
|
||||
// 1. Compute dynamic twilight angles.
|
||||
final tw = getAngles(
|
||||
date,
|
||||
civDate,
|
||||
lat,
|
||||
lng,
|
||||
elevation: elevation,
|
||||
|
|
@ -49,8 +57,11 @@ PrayerTimes getTimes(
|
|||
final ishaZenith = 90 + tw.ishaAngle;
|
||||
|
||||
// 3. Run SPA for solar position + custom twilight times.
|
||||
// Pass civDate (UTC noon) so SPA receives a deterministic UTC instant
|
||||
// and the date component it extracts (via date.toUtc()) is always the
|
||||
// intended civil day, independent of host timezone.
|
||||
final spaData = getSpa(
|
||||
date,
|
||||
civDate,
|
||||
lat,
|
||||
lng,
|
||||
tz,
|
||||
|
|
@ -70,9 +81,8 @@ PrayerTimes getTimes(
|
|||
final dhuhrTime = noonTime + 2.5 / 60;
|
||||
|
||||
// 4. Solar declination for Asr (Meeus formula, accurate to ~0.01°).
|
||||
final jd = toJulianDate(
|
||||
DateTime.utc(date.year, date.month, date.day, 12, 0, 0),
|
||||
);
|
||||
// civDate already is UTC noon of the civil date.
|
||||
final jd = toJulianDate(civDate);
|
||||
final eph = solarEphemeris(jd);
|
||||
|
||||
// 5. Asr time.
|
||||
|
|
|
|||
|
|
@ -190,4 +190,57 @@ void main() {
|
|||
expect(result.angles[0].sunset, lessThan(result.angles[1].sunset));
|
||||
});
|
||||
});
|
||||
|
||||
group('UTC day-boundary regression — civil-date consistency', () {
|
||||
// getTimes normalizes the input date to UTC noon of the expressed civil
|
||||
// date before passing it to getSpa and all astronomical calculations.
|
||||
// This ensures that a local DateTime and a UTC DateTime that both express
|
||||
// "March 15" (via their .year/.month/.day fields) produce identical prayer
|
||||
// times, regardless of what the host machine timezone is.
|
||||
|
||||
test('local DateTime and UTC noon for same civil date produce identical times', () {
|
||||
// Both express 2024-03-15: local midnight and UTC noon share year/month/day = 2024/3/15
|
||||
final local = DateTime(2024, 3, 15);
|
||||
final utcNoon = DateTime.utc(2024, 3, 15, 12, 0, 0);
|
||||
|
||||
final timesLocal = getTimes(local, 40.7128, -74.0060, -5.0);
|
||||
final timesNoon = getTimes(utcNoon, 40.7128, -74.0060, -5.0);
|
||||
|
||||
// After normalization both resolve to the same UTC noon reference point
|
||||
expect(timesNoon.fajr, closeTo(timesLocal.fajr, 0.0001));
|
||||
expect(timesNoon.sunrise, closeTo(timesLocal.sunrise, 0.0001));
|
||||
expect(timesNoon.dhuhr, closeTo(timesLocal.dhuhr, 0.0001));
|
||||
expect(timesNoon.asr, closeTo(timesLocal.asr, 0.0001));
|
||||
expect(timesNoon.maghrib, closeTo(timesLocal.maghrib, 0.0001));
|
||||
expect(timesNoon.isha, closeTo(timesLocal.isha, 0.0001));
|
||||
});
|
||||
|
||||
test('UTC midnight and UTC noon for same civil date produce identical times', () {
|
||||
final utcMidnight = DateTime.utc(2024, 3, 15, 0, 0, 0);
|
||||
final utcNoon = DateTime.utc(2024, 3, 15, 12, 0, 0);
|
||||
|
||||
final timesMid = getTimes(utcMidnight, 40.7128, -74.0060, -5.0);
|
||||
final timesNoon = getTimes(utcNoon, 40.7128, -74.0060, -5.0);
|
||||
|
||||
expect(timesNoon.fajr, closeTo(timesMid.fajr, 0.0001));
|
||||
expect(timesNoon.sunrise, closeTo(timesMid.sunrise, 0.0001));
|
||||
expect(timesNoon.dhuhr, closeTo(timesMid.dhuhr, 0.0001));
|
||||
expect(timesNoon.asr, closeTo(timesMid.asr, 0.0001));
|
||||
expect(timesNoon.maghrib, closeTo(timesMid.maghrib, 0.0001));
|
||||
expect(timesNoon.isha, closeTo(timesMid.isha, 0.0001));
|
||||
});
|
||||
|
||||
test('angles are identical for local vs UTC noon for the same civil date', () {
|
||||
final local = DateTime(2024, 6, 15);
|
||||
final utcNoon = DateTime.utc(2024, 6, 15, 12, 0, 0);
|
||||
|
||||
// getAngles receives civDate (already UTC noon from getTimes) but can
|
||||
// also be called directly — test that same civil date gives same angles.
|
||||
final anglesLocal = getAngles(local, 40.7128, -74.0060);
|
||||
final anglesUtcNoon = getAngles(utcNoon, 40.7128, -74.0060);
|
||||
|
||||
expect(anglesUtcNoon.fajrAngle, closeTo(anglesLocal.fajrAngle, 0.001));
|
||||
expect(anglesUtcNoon.ishaAngle, closeTo(anglesLocal.ishaAngle, 0.001));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue