diff --git a/src/main/java/com/kosherjava/zmanim/util/SPACalculator.java b/src/main/java/com/kosherjava/zmanim/util/SPACalculator.java index 93651f86..77964252 100644 --- a/src/main/java/com/kosherjava/zmanim/util/SPACalculator.java +++ b/src/main/java/com/kosherjava/zmanim/util/SPACalculator.java @@ -79,6 +79,15 @@ public class SPACalculator extends AstronomicalCalculator { /** Julian days per century. */ private static final double JULIAN_DAYS_PER_CENTURY = 36525.0; + /** + * The largest difference, in degrees, between the Sun's zenith at the solved time and the requested zenith that is still + * accepted as a sunrise or sunset. When the secant iteration in {@link #solveRiseSet} ends further off than this, it did + * not converge, and there is no event on that date. Converged solutions end below 1e-6° (floating-point noise in the + * Julian day), and failed ones almost always above 1e-3°. 1e-4° leaves a wide margin above the noise and is still + * below the SPA's stated accuracy of ±0.0003°. + */ + private static final double MAX_ZENITH_RESIDUAL = 1e-4; + /** Whether to apply the ΔT (TT−UT) correction. Defaults to {@code true}. */ private boolean applyDeltaT = true; @@ -331,6 +340,11 @@ private double solveRiseSet(LocalDate localDate, GeoLocation geoLocation, double t1 = t2; f1 = topocentricTrueZenith(jdDay + t1 / 1440.0, lat, geoLocation.getLongitude(), elevation) - adjustedZenith; } + // Near the poles the closed-form guess can report an event the topocentric Sun never reaches. The iteration then + // wanders off without converging, and the last guess is an arbitrary time, not a solution. + if (!(Math.abs(f1) <= MAX_ZENITH_RESIDUAL)) { + return Double.NaN; + } return t1; } diff --git a/src/test/java/com/kosherjava/zmanim/util/SPACalculatorTest.java b/src/test/java/com/kosherjava/zmanim/util/SPACalculatorTest.java index 18cc1357..b9f8c58b 100644 --- a/src/test/java/com/kosherjava/zmanim/util/SPACalculatorTest.java +++ b/src/test/java/com/kosherjava/zmanim/util/SPACalculatorTest.java @@ -116,6 +116,19 @@ public void getUTCSunset() { } } + /** + * Near the South Pole in March the closed-form first guess says the Sun reaches 10.2° below the horizon, but the + * topocentric Sun only gets to about 10.04°. The secant iteration never converges and used to return an arbitrary + * time. There is no event, so the result must be NaN. + */ + @Test + public void unconvergedRiseSetIsNoEvent() { + GeoLocation location = new GeoLocation("Antarctica", -77.26876171057897, -92.86928302120835, 3965.3333110036533, + java.time.ZoneId.of("Etc/GMT+6")); + double actual = calculator().getUTCSunrise(date("2127-03-14"), location, 100.2, false); + assertTrue("Sun never reaches 100.2° zenith; expected NaN but got " + actual, Double.isNaN(actual)); + } + @Test public void getUTCNoon() { Object[][] cases = {