fix: isDaytime wrong during polar day - #81
Conversation
|
@claude Please review this PR and post your findings as a comment — cover correctness of the refinement/snap logic in Solar.swift, the test coverage, and anything the PR description overclaims. Reply with your review even if you find no issues. |
|
Claude finished @ceeK's task in 5m 46s —— View job Review:
|
6a0a7c3 to
b2a4a16
Compare
During polar day (24h daylight above the polar circles) isDaytime returned false and isNighttime true - the opposite of reality. The window-based check collapsed "sun never rises" and "sun never sets" into the same nil sunrise/sunset, so both fell through to false. isDaytime now asks the question directly: is the sun's elevation at date above the official zenith? The USNO Almanac for Computers position model moves into sunPosition(forT:), and sunIsUp(atT:above:) inverts the almanac's clock relation to run the altitude identity forward at any instant. Polar day, polar night, transition days, and daylight spanning UTC midnight need no special cases. The published events are then defined by that same predicate rather than by the almanac's one-shot anchored inversion: each is the first whole second on the far side of the horizon crossing, found by bisecting the predicate between the model's solar transit and solar midnight. Elevation is unimodal over a solar day, so a crossing exists exactly when the endpoints disagree - which also replaces the cosH polar guards. isDaytime and the published times derive from one predicate and cannot disagree, at any latitude, including grazing crossings near the polar circles where the old one-shot solve drifted by up to an hour. Deleting the inversion also deletes its failure modes: the single-wrap normalise that pushed events near longitude 180 onto the wrong UTC day, the shouldBeYesterday/Tomorrow day-shift heuristics, and the year boundary discontinuity (instants map to their own year's day-of-year via epoch arithmetic, so a fresh Solar at a published cross-year event agrees exactly). Verified: 32/32 tests, including exact isDaytime flips at both sides of every published event for all city fixtures and for grazing, near-pole, antimeridian and cross-year cases; 0 contract violations in a 16,944- check seeded fuzz (2020-2028). Accuracy vs the NOAA-derived fixtures: mean 51.8s (main: 52.2s), max 251s (main: 268s). Cost: ~14us per init vs ~6us on main. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VPzRUnPKS8j3WimEjyb2Jk
b2a4a16 to
ee2924a
Compare
Fixes #59.
Problem
isDaytimeanswered "isdatebetween today's sunrise and sunset?". Above the polar circles that question has no answer: during polar day and polar night there is no sunrise or sunset, both arenil, and the check fell through tofalse. Under the midnight sunisDaytimewasfalseandisNighttimewastrue— the opposite of reality.Fix
Two connected changes, one idea: a single elevation predicate is the source of truth for everything.
1.
isDaytimeasks the question directly. Is the sun's elevation atdateabove the official zenith? The USNO Almanac for Computers position model moves into a sharedsunPosition(forT:), andsunIsUp(atT:above:)inverts the almanac's clock relation to evaluate elevation at any instant. Polar day, polar night, transition days, and daylight spanning UTC midnight need no special cases.2. The published events are defined by that same predicate. Instead of the almanac's one-shot closed-form inversion (which freezes the sun's position at a 06:00/18:00 guess and therefore lands seconds-to-minutes away from the crossing the predicate sees), each event is the first whole second on the far side of the horizon crossing, found by bisecting
sunIsUpbetween the model's solar transit and solar midnight. Elevation rises from one solar midnight to noon and falls to the next, so a crossing exists exactly when the endpoints disagree — when they agree the sun stays on one side of the zenith all day and no event is published.isDaytimeand the published times derive from one predicate and cannot disagree, at any latitude — a sunrise is the first secondisDaytimeturns true.Deleting the closed-form inversion also deletes its failure modes:
normalisethat pushed events near longitude ±180° onto the wrong UTC day, ~11–15 min off;shouldBeYesterday/shouldBeTomorrowday-shift heuristics;Solarconstructed at a published cross-year event (a Tokyo sunrise on 31 Dec UTC) agrees exactly.Public API unchanged:
sunrise/sunsetstill returnnilon polar days;isDaytimeis now a computed property.Tests
Written first (RED → GREEN), 32/32 passing:
isDaytimeis true at the published sunrise and false one second before it; false at the published sunset and true one second before it (~950 assertions).Accuracy and performance
Against the NOAA-derived
CorrectResults.jsonfixtures (474 events):The remaining error is the almanac position model itself (truncated series, fixed refraction), which this PR doesn't change;
sunPosition(forT:)is the seam if a higher-order model is ever wanted — the bisection needs no changes to benefit from one.Cost: ~14µs per init (arm64 release) vs ~6µs on main — the bisection evaluates the predicate ~16 times per event, all pure arithmetic (no Calendar calls in the loop). A year-of-daylight plot is ~5ms.
🤖 Generated with Claude Code
https://claude.ai/code/session_01VPzRUnPKS8j3WimEjyb2Jk