Skip to content

SPACalculator: return NaN instead of running for hours when the target zenith is NaN - #299

Merged
KosherJava merged 1 commit into
KosherJava:masterfrom
Elyahu41:fix/spa-nan-zenith-hang
Nov 6, 2026
Merged

KosherJava merged 1 commit into
KosherJava:masterfrom
Elyahu41:fix/spa-nan-zenith-hang

Conversation

@Elyahu41

@Elyahu41 Elyahu41 commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

The issue

SPACalculator.getUTCSunrise / getUTCSunset never return (in practice) when the zenith they search for is NaN. The other calculators return NaN for the same input in about 10 ms.

The edge case

The target is the adjusted zenith, so it becomes NaN whenever any input to it isn't finite or makes no sense:

Input Example
a NaN or infinite zenith getUTCSunrise(date, geo, Double.POSITIVE_INFINITY, false)
a NaN refraction calc.setRefraction(Double.NaN)
an infinite solar radius calc.setSolarRadius(Double.POSITIVE_INFINITY)
a negative, infinite or NaN earth radius (with elevation) calc.setEarthRadius(-6371)

These usually come from a bug or bad configuration in the calling app (a 0 / 0 offset, an uninitialized setting), not from a deliberate request. They were found by fuzzing the library with random inputs.

What happens

bracketRiseSet searches a function of the form sin(elevation) - sin(90° - adjustedZenith), which is NaN everywhere. CrossingSearch.find discards an interval only when a comparison proves it can't contain a crossing, and every comparison with NaN is false. So nothing is ever discarded, and the search splits every interval all the way down to TIME_TOLERANCE (1 ms over a 24 h window, about 2²⁷ SPA evaluations). The call doesn't finish in any reasonable time. In an app it freezes the thread; on a server it holds a worker indefinitely.

GeoLocation geo = new GeoLocation("Lakewood, NJ", 40.0721087, -74.2400243, 0, ZoneId.of("America/New_York"));
new SPACalculator().getUTCSunrise(LocalDate.of(2026, 1, 1), geo, Double.NaN, false); // runs for hours
new NOAACalculator().getUTCSunrise(LocalDate.of(2026, 1, 1), geo, Double.NaN, false); // NaN

The fix

  • getUTCSunRiseSet: If the adjusted zenith isn't finite, return Double.NaN right away, the same "no event" result the other calculators give. The calendar then returns null as usual.
  • CrossingSearch.find: As defense in depth, return NaN when either endpoint value is NaN, so any other source of a NaN function value (a NaN latitude, for example) can't trigger the exhaustive search either.

Finite zeniths never produce NaN function values, so results for valid input are unchanged; the full test suite passes.

Tests

SPACalculatorTest.nonFiniteZenithReturnsNaNPromptly (10 s timeout) covers a NaN and an infinite zenith, a negative earth radius, a NaN refraction and an infinite solar radius. Without the fix it times out.


🤖 Generated with Claude Code

https://claude.ai/code/session_015Er4Sx7LnKzzCbgDEatxkU

…get zenith is NaN

A NaN or infinite zenith, or a NaN / infinite refraction, solar radius or a
negative / NaN earth radius (which make the adjusted zenith NaN), made the
bracketing rise / set search split every interval down to 1 ms because no
interval of a NaN function can be discarded (about 2^27 SPA evaluations).
NOAA and Meeus return NaN for the same input.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Er4Sx7LnKzzCbgDEatxkU
@KosherJava
KosherJava merged commit 46cc25f into KosherJava:master Nov 6, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants