From d344a0b13e3f200c329304e413afc343caee5447 Mon Sep 17 00:00:00 2001 From: Zachary Weixelbaum Date: Wed, 15 Jul 2026 11:18:55 -0400 Subject: [PATCH 1/2] Add Test Suite plus Regression Value Dumper --- pom.xml | 15 + .../zmanim/AstronomicalCalendarTest.java | 121 +++++++ .../ComprehensiveZmanimCalendarTest.java | 300 +++++++++++++++++ .../zmanim/RegressionValueDumper.java | 96 ++++++ .../zmanim/SpecialDayZmanimTest.java | 150 +++++++++ .../com/kosherjava/zmanim/TestLocations.java | 121 +++++++ .../kosherjava/zmanim/ZmanimCalendarTest.java | 111 +++++++ .../JewishCalendarHolidayTest.java | 89 +++++ .../hebrewcalendar/TefilaRulesTest.java | 75 +++++ .../util/AstronomicalCalculatorTest.java | 147 +++++++++ .../zmanim/util/GeoLocationTest.java | 197 +++++++++++ .../zmanim/util/MeeusCalculatorTest.java | 256 ++++++++++++++ .../zmanim/util/NOAACalculatorTest.java | 242 ++++++++++++++ .../zmanim/util/SPACalculatorTest.java | 311 ++++++++++++++++++ .../zmanim/util/SunTimesCalculatorTest.java | 192 +++++++++++ 15 files changed, 2423 insertions(+) create mode 100644 src/test/java/com/kosherjava/zmanim/AstronomicalCalendarTest.java create mode 100644 src/test/java/com/kosherjava/zmanim/ComprehensiveZmanimCalendarTest.java create mode 100644 src/test/java/com/kosherjava/zmanim/RegressionValueDumper.java create mode 100644 src/test/java/com/kosherjava/zmanim/SpecialDayZmanimTest.java create mode 100644 src/test/java/com/kosherjava/zmanim/TestLocations.java create mode 100644 src/test/java/com/kosherjava/zmanim/ZmanimCalendarTest.java create mode 100644 src/test/java/com/kosherjava/zmanim/hebrewcalendar/JewishCalendarHolidayTest.java create mode 100644 src/test/java/com/kosherjava/zmanim/hebrewcalendar/TefilaRulesTest.java create mode 100644 src/test/java/com/kosherjava/zmanim/util/AstronomicalCalculatorTest.java create mode 100644 src/test/java/com/kosherjava/zmanim/util/GeoLocationTest.java create mode 100644 src/test/java/com/kosherjava/zmanim/util/MeeusCalculatorTest.java create mode 100644 src/test/java/com/kosherjava/zmanim/util/NOAACalculatorTest.java create mode 100644 src/test/java/com/kosherjava/zmanim/util/SPACalculatorTest.java create mode 100644 src/test/java/com/kosherjava/zmanim/util/SunTimesCalculatorTest.java diff --git a/pom.xml b/pom.xml index acc90bd2..a54eacee 100644 --- a/pom.xml +++ b/pom.xml @@ -111,6 +111,21 @@ 11 + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.5 + + + + **/Test*.java + **/*Test.java + **/*Tests.java + **/*TestCase.java + **/UT_*.java + + + diff --git a/src/test/java/com/kosherjava/zmanim/AstronomicalCalendarTest.java b/src/test/java/com/kosherjava/zmanim/AstronomicalCalendarTest.java new file mode 100644 index 00000000..29bcafc1 --- /dev/null +++ b/src/test/java/com/kosherjava/zmanim/AstronomicalCalendarTest.java @@ -0,0 +1,121 @@ +/* + * Zmanim Java API + * Copyright © 2004-2026 Eliyahu Hershfeld + * + * This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General + * Public License as published by the Free Software Foundation; version 2.1 of the License. + * + * This library is distributed in the hope that it will be useful,but WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA, + * or connect to: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html + */ +package com.kosherjava.zmanim; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNull; + +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDate; + +import org.junit.Test; + +import com.kosherjava.zmanim.util.SunTimesCalculator; + +/** + * Regression and behavioral coverage for {@link AstronomicalCalendar}. The time values are the library's own current + * output for the fixture (Lakewood, NJ on 2017-10-17); the behavioral tests exercise the polar no-event {@code null} + * contract, calculator swapping, and the twilight/offset relationships. (Complements the existing + * {@link AstronomicalCalendarRegressionTest}, which covers local-mean-time/DST edge cases.) + * + * @author Test coverage + */ +public class AstronomicalCalendarTest { + + private static final LocalDate FIXTURE_DATE = LocalDate.of(2017, 10, 17); + + private AstronomicalCalendar fixtureCalendar() { + AstronomicalCalendar calendar = new AstronomicalCalendar(TestLocations.lakewood()); + calendar.setLocalDate(FIXTURE_DATE); + return calendar; + } + + private void assertInstant(String label, String expectedIso, Instant actual) { + assertEquals(label, Instant.parse(expectedIso), actual); + } + + @Test + public void sunriseAndSunset() { + AstronomicalCalendar calendar = fixtureCalendar(); + assertInstant("sunrise", "2017-10-17T11:09:11.571783718Z", calendar.getSunrise()); + assertInstant("sunset", "2017-10-17T22:14:38.994862349Z", calendar.getSunset()); + assertInstant("seaLevelSunrise", "2017-10-17T11:09:51.403184642Z", calendar.getSeaLevelSunrise()); + assertInstant("seaLevelSunset", "2017-10-17T22:13:59.201432122Z", calendar.getSeaLevelSunset()); + } + + @Test + public void transitAndSolarMidnight() { + AstronomicalCalendar calendar = fixtureCalendar(); + assertInstant("sunTransit", "2017-10-17T16:42:12.781249470Z", calendar.getSunTransit()); + assertInstant("solarMidnight", "2017-10-18T04:42:06.833038724Z", calendar.getSolarMidnight()); + } + + @Test + public void twilights() { + AstronomicalCalendar calendar = fixtureCalendar(); + assertInstant("beginCivil", "2017-10-17T10:42:27.439221886Z", calendar.getBeginCivilTwilight()); + assertInstant("beginNautical", "2017-10-17T10:10:57.242471901Z", calendar.getBeginNauticalTwilight()); + assertInstant("beginAstronomical", "2017-10-17T09:39:33.523030241Z", calendar.getBeginAstronomicalTwilight()); + assertInstant("endCivil", "2017-10-17T22:41:21.435143220Z", calendar.getEndCivilTwilight()); + assertInstant("endNautical", "2017-10-17T23:12:49.151356447Z", calendar.getEndNauticalTwilight()); + assertInstant("endAstronomical", "2017-10-17T23:44:09.707296295Z", calendar.getEndAstronomicalTwilight()); + } + + @Test + public void temporalHour() { + assertEquals(Duration.parse("PT55M20.649853956S"), fixtureCalendar().getTemporalHour()); + } + + /** + * The offset-by-degrees helpers underpin the named twilight methods, so passing the civil zenith (96°) must + * reproduce civil twilight exactly. + */ + @Test + public void offsetByDegreesMatchesNamedTwilight() { + AstronomicalCalendar calendar = fixtureCalendar(); + assertEquals(calendar.getBeginCivilTwilight(), calendar.getSunriseOffsetByDegrees(96.0)); + assertEquals(calendar.getEndCivilTwilight(), calendar.getSunsetOffsetByDegrees(96.0)); + } + + /** + * Inside the Arctic Circle the sun neither rises nor sets on the summer solstice, so the rise/set based times must + * be {@code null} rather than throw. + */ + @Test + public void polarNoEventReturnsNull() { + AstronomicalCalendar calendar = new AstronomicalCalendar(TestLocations.norway(TestLocations.UTC)); + calendar.setLocalDate(LocalDate.of(2017, 6, 21)); + assertNull("polar sunrise", calendar.getSunrise()); + assertNull("polar sunset", calendar.getSunset()); + } + + /** + * The calculation engine is pluggable; the NOAA and USNO algorithms differ slightly, so swapping the calculator + * must change the result. + */ + @Test + public void swappingCalculatorChangesResult() { + AstronomicalCalendar noaa = fixtureCalendar(); + Instant noaaSunset = noaa.getSunset(); + + AstronomicalCalendar sunTimes = fixtureCalendar(); + sunTimes.setAstronomicalCalculator(new SunTimesCalculator()); + Instant sunTimesSunset = sunTimes.getSunset(); + + assertNotEquals(noaaSunset, sunTimesSunset); + } +} diff --git a/src/test/java/com/kosherjava/zmanim/ComprehensiveZmanimCalendarTest.java b/src/test/java/com/kosherjava/zmanim/ComprehensiveZmanimCalendarTest.java new file mode 100644 index 00000000..1a970cab --- /dev/null +++ b/src/test/java/com/kosherjava/zmanim/ComprehensiveZmanimCalendarTest.java @@ -0,0 +1,300 @@ +/* + * Zmanim Java API + * Copyright © 2004-2026 Eliyahu Hershfeld + * + * This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General + * Public License as published by the Free Software Foundation; version 2.1 of the License. + * + * This library is distributed in the hope that it will be useful,but WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA, + * or connect to: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html + */ +package com.kosherjava.zmanim; + +import static org.junit.Assert.assertEquals; + +import java.lang.reflect.Method; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDate; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.TreeSet; + +import org.junit.Test; + +/** + * A comprehensive regression net over every zero-argument zman method of {@link ComprehensiveZmanimCalendar}. Because + * the zmanim here encode particular halachic opinions (rather than a single verifiable astronomical value), these are + * pinned against the library's current output for a fixed fixture (Lakewood, NJ on 2017-10-17): any future change + * that shifts a result will be caught. Values that are {@code null} for this date (e.g. the chametz and Kidush Levana + * times, which only apply on specific days) are asserted to remain {@code null}. + *

+ * The test reflects over the calendar's public {@code get*}/{@code is*} methods returning {@link Instant} or + * {@link Duration} and compares each to the golden table below. It also asserts that the set of such methods + * matches the table, so a newly added or removed zman method is flagged (and its expected value must be recorded here). + * The golden values were generated from the library itself; regenerate with {@code RegressionValueDumper} if the + * fixture or a calculation intentionally changes. + * + * @author Test coverage + */ +public class ComprehensiveZmanimCalendarTest { + + private static final LocalDate FIXTURE_DATE = LocalDate.of(2017, 10, 17); + + /** Golden output: method name -> expected {@link Instant}/{@link Duration} string, or {@code null}. */ + private static final Object[][] EXPECTED = { + { "getAlos120Minutes", "2017-10-17T09:09:51.403184642Z" }, + { "getAlos120Zmanis", "2017-10-17T09:19:10.103476730Z" }, + { "getAlos16Point1Degrees", "2017-10-17T09:49:30.219906135Z" }, + { "getAlos18Degrees", "2017-10-17T09:39:33.523030241Z" }, + { "getAlos19Degrees", "2017-10-17T09:34:19.165263704Z" }, + { "getAlos19Point8Degrees", "2017-10-17T09:30:07.461349050Z" }, + { "getAlos26Degrees", "2017-10-17T08:57:25.521762580Z" }, + { "getAlos60Minutes", "2017-10-17T10:09:51.403184642Z" }, + { "getAlos72Minutes", "2017-10-17T09:57:51.403184642Z" }, + { "getAlos72Zmanis", "2017-10-17T10:03:26.623359895Z" }, + { "getAlos90Minutes", "2017-10-17T09:39:51.403184642Z" }, + { "getAlos90Zmanis", "2017-10-17T09:46:50.428403708Z" }, + { "getAlos96Minutes", "2017-10-17T09:33:51.403184642Z" }, + { "getAlos96Zmanis", "2017-10-17T09:41:18.363418313Z" }, + { "getAlosBaalHatanya", "2017-10-17T09:45:19.050001055Z" }, + { "getBainHashmashosRT13Point24Degrees", "2017-10-17T23:19:17.914866983Z" }, + { "getBainHashmashosRT13Point5MinutesBefore7Point083Degrees", "2017-10-17T22:33:33.446858525Z" }, + { "getBainHashmashosRT2Stars", "2017-10-17T22:41:41.407497564Z" }, + { "getBainHashmashosRT58Point5Minutes", "2017-10-17T23:12:29.201432122Z" }, + { "getBainHashmashosYereim13Point5Minutes", "2017-10-17T22:00:29.201432122Z" }, + { "getBainHashmashosYereim16Point875Minutes", "2017-10-17T21:57:06.701432122Z" }, + { "getBainHashmashosYereim18Minutes", "2017-10-17T21:55:59.201432122Z" }, + { "getBainHashmashosYereim2Point1Degrees", "2017-10-17T21:58:15.259245832Z" }, + { "getBainHashmashosYereim2Point8Degrees", "2017-10-17T21:54:28.539077180Z" }, + { "getBainHashmashosYereim3Point05Degrees", "2017-10-17T21:53:07.410874002Z" }, + { "getBeginAstronomicalTwilight", "2017-10-17T09:39:33.523030241Z" }, + { "getBeginCivilTwilight", "2017-10-17T10:42:27.439221886Z" }, + { "getBeginNauticalTwilight", "2017-10-17T10:10:57.242471901Z" }, + { "getCandleLighting", "2017-10-17T21:55:59.201432122Z" }, + { "getChatzosHalayla", "2017-10-18T04:42:06.833038724Z" }, + { "getChatzosHayom", "2017-10-17T16:42:12.781249470Z" }, + { "getChatzosHayomAsHalfDay", "2017-10-17T16:41:55.302308378Z" }, + { "getEndAstronomicalTwilight", "2017-10-17T23:44:09.707296295Z" }, + { "getEndCivilTwilight", "2017-10-17T22:41:21.435143220Z" }, + { "getEndNauticalTwilight", "2017-10-17T23:12:49.151356447Z" }, + { "getFixedLocalChatzosHayom", "2017-10-17T16:56:57.605832Z" }, + { "getMinchaGedola16Point1Degrees", "2017-10-17T17:16:13.988705527Z" }, + { "getMinchaGedola30Minutes", "2017-10-17T17:12:12.781249470Z" }, + { "getMinchaGedola72Minutes", "2017-10-17T17:15:35.627235356Z" }, + { "getMinchaGedolaAhavatShalom", "2017-10-17T17:13:52.057872856Z" }, + { "getMinchaGedolaAteretTorah", "2017-10-17T17:00:49.269815683Z" }, + { "getMinchaGedolaBaalHatanya", "2017-10-17T17:09:55.476611672Z" }, + { "getMinchaGedolaGRA", "2017-10-17T17:09:35.627235356Z" }, + { "getMinchaGedolaGRAFixedLocalChatzos30Minutes", "2017-10-17T17:26:57.605832Z" }, + { "getMinchaGedolaGRAGreaterThan30", "2017-10-17T17:12:12.781249470Z" }, + { "getMinchaKetana16Point1Degrees", "2017-10-17T20:42:24.958920631Z" }, + { "getMinchaKetana72Minutes", "2017-10-17T20:37:37.576797224Z" }, + { "getMinchaKetanaAhavatShalom", "2017-10-17T19:51:21.615491489Z" }, + { "getMinchaKetanaAteretTorah", "2017-10-17T20:13:27.414333739Z" }, + { "getMinchaKetanaBaalHatanya", "2017-10-17T19:57:57.216239627Z" }, + { "getMinchaKetanaGRA", "2017-10-17T19:55:37.576797224Z" }, + { "getMinchaKetanaGRAFixedLocalChatzosToSunset", "2017-10-17T20:01:53.536598735Z" }, + { "getMisheyakir10Point2Degrees", "2017-10-17T10:20:22.960720797Z" }, + { "getMisheyakir11Degrees", "2017-10-17T10:16:11.433376090Z" }, + { "getMisheyakir11Point5Degrees", "2017-10-17T10:13:34.311221155Z" }, + { "getMisheyakir12Point85Degrees", "2017-10-17T10:06:30.324568897Z" }, + { "getMisheyakir7Point65Degrees", "2017-10-17T10:33:46.155182911Z" }, + { "getMisheyakir9Point5Degrees", "2017-10-17T10:24:03.203710034Z" }, + { "getPlagAhavatShalom", "2017-10-17T21:10:33.114910614Z" }, + { "getPlagAlos16Point1DegreesToTzaisGeonim7Point083Degrees", "2017-10-17T21:26:03.735717649Z" }, + { "getPlagAlosToSunset", "2017-10-17T20:56:26.182523158Z" }, + { "getPlagHamincha120Minutes", "2017-10-17T22:39:48.389114669Z" }, + { "getPlagHamincha120MinutesZmanis", "2017-10-17T22:32:26.084716766Z" }, + { "getPlagHamincha16Point1Degrees", "2017-10-17T22:08:19.529843591Z" }, + { "getPlagHamincha18Degrees", "2017-10-17T22:16:10.938101909Z" }, + { "getPlagHamincha19Point8Degrees", "2017-10-17T22:23:38.065797743Z" }, + { "getPlagHamincha26Degrees", "2017-10-17T22:49:27.074658511Z" }, + { "getPlagHamincha60Minutes", "2017-10-17T21:52:18.389114669Z" }, + { "getPlagHamincha72Minutes", "2017-10-17T22:01:48.389114669Z" }, + { "getPlagHamincha72MinutesZmanis", "2017-10-17T21:57:23.006475925Z" }, + { "getPlagHamincha90Minutes", "2017-10-17T22:16:03.389114669Z" }, + { "getPlagHamincha90MinutesZmanis", "2017-10-17T22:10:31.660816241Z" }, + { "getPlagHamincha96Minutes", "2017-10-17T22:20:48.389114669Z" }, + { "getPlagHamincha96MinutesZmanis", "2017-10-17T22:14:54.545596351Z" }, + { "getPlagHaminchaAteretTorah", "2017-10-17T21:33:43.307882929Z" }, + { "getPlagHaminchaBaalHatanya", "2017-10-17T21:07:57.941084608Z" }, + { "getPlagHaminchaGRA", "2017-10-17T21:04:48.389114669Z" }, + { "getPlagHaminchaGRAFixedLocalChatzosToSunset", "2017-10-17T21:07:56.369015426Z" }, + { "getPolarPlagHaminchaBenIshChai", null }, + { "getPolarPlagHaminchaTeshuvosVehanhagos", null }, + { "getPolarStartOfDayTeshuvosVehanhagos", null }, + { "getPolarSunriseBenIshChai", null }, + { "getPolarSunsetBenIshChai", null }, + { "getSamuchLeMinchaKetana16Point1Degrees", "2017-10-17T20:08:03.130551447Z" }, + { "getSamuchLeMinchaKetana72Minutes", "2017-10-17T20:03:57.251870246Z" }, + { "getSamuchLeMinchaKetanaGRA", "2017-10-17T19:27:57.251870246Z" }, + { "getSeaLevelSunrise", "2017-10-17T11:09:51.403184642Z" }, + { "getSeaLevelSunset", "2017-10-17T22:13:59.201432122Z" }, + { "getShaahZmanis120Minutes", "PT1H15M20.649853956S" }, + { "getShaahZmanis120MinutesZmanis", "PT1H13M47.533138608S" }, + { "getShaahZmanis16Point1Degrees", "PT1H8M43.656738368S" }, + { "getShaahZmanis18Degrees", "PT1H10M23.015355504S" }, + { "getShaahZmanis19Point8Degrees", "PT1H11M57.265530111S" }, + { "getShaahZmanis26Degrees", "PT1H17M23.865385668S" }, + { "getShaahZmanis60Minutes", "PT1H5M20.649853956S" }, + { "getShaahZmanis72Minutes", "PT1H7M20.649853956S" }, + { "getShaahZmanis72MinutesZmanis", "PT1H6M24.779824747S" }, + { "getShaahZmanis90Minutes", "PT1H10M20.649853956S" }, + { "getShaahZmanis90MinutesZmanis", "PT1H9M10.812317445S" }, + { "getShaahZmanis96Minutes", "PT1H11M20.649853956S" }, + { "getShaahZmanis96MinutesZmanis", "PT1H10M6.156481678S" }, + { "getShaahZmanisAlos16Point1DegreesToTzaisGeonim3Point7Degrees", "PT1H3M18.553246773S" }, + { "getShaahZmanisAlos16Point1DegreesToTzaisGeonim3Point8Degrees", "PT1H3M21.1995353S" }, + { "getShaahZmanisAlos16Point1DegreesToTzaisGeonim7Point083Degrees", "PT1H4M47.768912699S" }, + { "getShaahZmanisAteretTorah", "PT1H4M12.714839352S" }, + { "getShaahZmanisBaalHatanya", "PT56M0.579875985S" }, + { "getShaahZmanisGRA", "PT55M20.649853956S" }, + { "getSofZmanAchilasChametzBaalHatanya", null }, + { "getSofZmanAchilasChametzGRA", null }, + { "getSofZmanAchilasChametzMGA16Point1Degrees", null }, + { "getSofZmanAchilasChametzMGA72Minutes", null }, + { "getSofZmanAchilasChametzMGA72MinutesZmanis", null }, + { "getSofZmanBiurChametzBaalHatanya", null }, + { "getSofZmanBiurChametzGRA", null }, + { "getSofZmanBiurChametzMGA16Point1Degrees", null }, + { "getSofZmanBiurChametzMGA72Minutes", null }, + { "getSofZmanBiurChametzMGA72MinutesZmanis", null }, + { "getSofZmanKidushLevana15Days", null }, + { "getSofZmanKidushLevanaBetweenMoldos", null }, + { "getSofZmanShma3HoursBeforeChatzos", "2017-10-17T13:42:12.781249470Z" }, + { "getSofZmanShmaAlos16Point1DegreesToTzaisGeonim7Point083Degrees", "2017-10-17T13:03:53.526644232Z" }, + { "getSofZmanShmaAlos16Point1ToSunset", "2017-10-17T12:55:37.465287630Z" }, + { "getSofZmanShmaAteretTorah", "2017-10-17T13:16:04.767877951Z" }, + { "getSofZmanShmaBaalHatanya", "2017-10-17T13:53:53.447045725Z" }, + { "getSofZmanShmaGRA", "2017-10-17T13:55:53.352746510Z" }, + { "getSofZmanShmaGRASunriseToFixedLocalChatzos", "2017-10-17T14:03:24.504508319Z" }, + { "getSofZmanShmaMGA120Minutes", "2017-10-17T12:55:53.352746510Z" }, + { "getSofZmanShmaMGA16Point1Degrees", "2017-10-17T13:15:41.190121239Z" }, + { "getSofZmanShmaMGA16Point1DegreesToFixedLocalChatzos", "2017-10-17T13:23:13.912869066Z" }, + { "getSofZmanShmaMGA18Degrees", "2017-10-17T13:10:42.569096753Z" }, + { "getSofZmanShmaMGA18DegreesToFixedLocalChatzos", "2017-10-17T13:18:15.564431120Z" }, + { "getSofZmanShmaMGA19Point8Degrees", "2017-10-17T13:05:59.257939383Z" }, + { "getSofZmanShmaMGA72Minutes", "2017-10-17T13:19:53.352746510Z" }, + { "getSofZmanShmaMGA72MinutesToFixedLocalChatzos", "2017-10-17T13:27:24.504508319Z" }, + { "getSofZmanShmaMGA72MinutesZmanis", "2017-10-17T13:22:40.962834136Z" }, + { "getSofZmanShmaMGA90Minutes", "2017-10-17T13:10:53.352746510Z" }, + { "getSofZmanShmaMGA90MinutesToFixedLocalChatzos", "2017-10-17T13:18:24.504508319Z" }, + { "getSofZmanShmaMGA90MinutesZmanis", "2017-10-17T13:14:22.865356043Z" }, + { "getSofZmanShmaMGA96Minutes", "2017-10-17T13:07:53.352746510Z" }, + { "getSofZmanShmaMGA96MinutesZmanis", "2017-10-17T13:11:36.832863347Z" }, + { "getSofZmanTfila2HoursBeforeChatzos", "2017-10-17T14:42:12.781249470Z" }, + { "getSofZmanTfilaAteretTorah", "2017-10-17T14:20:17.482717303Z" }, + { "getSofZmanTfilaBaalHatanya", "2017-10-17T14:49:54.026921710Z" }, + { "getSofZmanTfilaGRA", "2017-10-17T14:51:14.002600466Z" }, + { "getSofZmanTfilaGRASunriseToFixedLocalChatzos", "2017-10-17T15:01:15.538282878Z" }, + { "getSofZmanTfilaMGA120Minutes", "2017-10-17T14:11:14.002600466Z" }, + { "getSofZmanTfilaMGA16Point1Degrees", "2017-10-17T14:24:24.846859607Z" }, + { "getSofZmanTfilaMGA18Degrees", "2017-10-17T14:21:05.584452257Z" }, + { "getSofZmanTfilaMGA19Point8Degrees", "2017-10-17T14:17:56.523469494Z" }, + { "getSofZmanTfilaMGA72Minutes", "2017-10-17T14:27:14.002600466Z" }, + { "getSofZmanTfilaMGA72MinutesZmanis", "2017-10-17T14:29:05.742658883Z" }, + { "getSofZmanTfilaMGA90Minutes", "2017-10-17T14:21:14.002600466Z" }, + { "getSofZmanTfilaMGA90MinutesZmanis", "2017-10-17T14:23:33.677673488Z" }, + { "getSofZmanTfilaMGA96Minutes", "2017-10-17T14:19:14.002600466Z" }, + { "getSofZmanTfilaMGA96MinutesZmanis", "2017-10-17T14:21:42.989345025Z" }, + { "getSolarMidnight", "2017-10-18T04:42:06.833038724Z" }, + { "getSunTransit", "2017-10-17T16:42:12.781249470Z" }, + { "getSunrise", "2017-10-17T11:09:11.571783718Z" }, + { "getSunset", "2017-10-17T22:14:38.994862349Z" }, + { "getTchilasZmanKidushLevana3Days", null }, + { "getTchilasZmanKidushLevana7Days", null }, + { "getTemporalHour", "PT55M20.649853956S" }, + { "getTzais120Minutes", "2017-10-18T00:13:59.201432122Z" }, + { "getTzais120Zmanis", "2017-10-18T00:04:40.501140034Z" }, + { "getTzais16Point1Degrees", "2017-10-17T23:34:14.100766559Z" }, + { "getTzais18Degrees", "2017-10-17T23:44:09.707296295Z" }, + { "getTzais19Point8Degrees", "2017-10-17T23:53:34.647710389Z" }, + { "getTzais26Degrees", "2017-10-18T00:26:11.906390602Z" }, + { "getTzais50Minutes", "2017-10-17T23:03:59.201432122Z" }, + { "getTzais60Minutes", "2017-10-17T23:13:59.201432122Z" }, + { "getTzais72Minutes", "2017-10-17T23:25:59.201432122Z" }, + { "getTzais72Zmanis", "2017-10-17T23:20:23.981256869Z" }, + { "getTzais90Minutes", "2017-10-17T23:43:59.201432122Z" }, + { "getTzais90Zmanis", "2017-10-17T23:37:00.176213056Z" }, + { "getTzais96Minutes", "2017-10-17T23:49:59.201432122Z" }, + { "getTzais96Zmanis", "2017-10-17T23:42:32.241198451Z" }, + { "getTzaisAteretTorah", "2017-10-17T22:53:59.201432122Z" }, + { "getTzaisBaalHatanya", "2017-10-17T22:41:21.435143220Z" }, + { "getTzaisGeonim3Point7Degrees", "2017-10-17T22:29:12.858867416Z" }, + { "getTzaisGeonim3Point8Degrees", "2017-10-17T22:29:44.614329739Z" }, + { "getTzaisGeonim4Point42Degrees", "2017-10-17T22:33:01.330786672Z" }, + { "getTzaisGeonim4Point66Degrees", "2017-10-17T22:34:17.404148028Z" }, + { "getTzaisGeonim4Point8Degrees", "2017-10-17T22:35:01.761613150Z" }, + { "getTzaisGeonim5Point95Degrees", "2017-10-17T22:41:05.633594744Z" }, + { "getTzaisGeonim6Point45Degrees", "2017-10-17T22:43:43.582328133Z" }, + { "getTzaisGeonim7Point083Degrees", "2017-10-17T22:47:03.446858525Z" }, + { "getTzaisGeonim7Point67Degrees", "2017-10-17T22:50:08.395485971Z" }, + { "getTzaisGeonim8Point5Degrees", "2017-10-17T22:54:29.772724455Z" }, + { "getTzaisGeonim9Point3Degrees", "2017-10-17T22:58:41.421314719Z" }, + { "getTzaisGeonim9Point75Degrees", "2017-10-17T23:01:02.865384009Z" }, + { "getZmanMolad", null }, + }; + + private ComprehensiveZmanimCalendar fixtureCalendar() { + ComprehensiveZmanimCalendar calendar = new ComprehensiveZmanimCalendar(TestLocations.lakewood()); + calendar.setLocalDate(FIXTURE_DATE); + return calendar; + } + + /** Invokes {@code methodName} on the calendar and returns its value as a comparable string, or {@code null}. */ + private String actual(ComprehensiveZmanimCalendar calendar, String methodName) throws Exception { + Object value = calendar.getClass().getMethod(methodName).invoke(calendar); + if (value == null) { + return null; + } + return value instanceof Instant ? ((Instant) value).toString() : ((Duration) value).toString(); + } + + @Test + public void allZmanimMatchGoldenOutput() throws Exception { + ComprehensiveZmanimCalendar calendar = fixtureCalendar(); + + Map expected = new LinkedHashMap<>(); + for (Object[] row : EXPECTED) { + expected.put((String) row[0], (String) row[1]); + } + + for (Map.Entry entry : expected.entrySet()) { + assertEquals(entry.getKey(), entry.getValue(), actual(calendar, entry.getKey())); + } + } + + /** + * Guards the golden table against drift: the set of zero-argument {@code get*}/{@code is*} methods returning an + * {@link Instant} or {@link Duration} must exactly match the recorded table. If a zman method is added or removed, + * this fails and the table above must be updated (regenerate with {@code RegressionValueDumper}). + */ + @Test + public void goldenTableCoversEveryZmanMethod() { + TreeSet reflected = new TreeSet<>(); + for (Method method : ComprehensiveZmanimCalendar.class.getMethods()) { + if (method.getParameterCount() != 0) { + continue; + } + if (!(method.getName().startsWith("get") || method.getName().startsWith("is"))) { + continue; + } + Class returnType = method.getReturnType(); + if (returnType == Instant.class || returnType == Duration.class) { + reflected.add(method.getName()); + } + } + + TreeSet recorded = new TreeSet<>(); + for (Object[] row : EXPECTED) { + recorded.add((String) row[0]); + } + + assertEquals(recorded, reflected); + } +} diff --git a/src/test/java/com/kosherjava/zmanim/RegressionValueDumper.java b/src/test/java/com/kosherjava/zmanim/RegressionValueDumper.java new file mode 100644 index 00000000..92888c41 --- /dev/null +++ b/src/test/java/com/kosherjava/zmanim/RegressionValueDumper.java @@ -0,0 +1,96 @@ +package com.kosherjava.zmanim; + +import java.lang.reflect.Method; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; + +import com.kosherjava.zmanim.hebrewcalendar.JewishDate; +import com.kosherjava.zmanim.util.GeoLocation; + +/** + * Not a test (no "Test" suffix, so Surefire ignores it). A throwaway generator that dumps the library's current output + * for every zero-arg zman method so the values can be baked into the regression tests. Run via: + * {@code java -cp target/test-classes:target/classes com.kosherjava.zmanim.RegressionValueDumper <class>}, or with a + * Jewish date to exercise the date-gated zmanim: + * {@code ... RegressionValueDumper comprehensive jewish <year> <month> <day>}. + */ +public final class RegressionValueDumper { + + public static void main(String[] args) throws Exception { + String which = args.length > 0 ? args[0] : "comprehensive"; + GeoLocation lakewood = TestLocations.lakewood(); + LocalDate date = LocalDate.of(2017, 10, 17); + if (args.length >= 5 && "jewish".equals(args[1])) { + date = new JewishDate(Integer.parseInt(args[2]), Integer.parseInt(args[3]), Integer.parseInt(args[4])) + .getLocalDate(); + } + if (args.length >= 3 && "scan".equals(args[1])) { + // scan [startIsoDate] [days] [location]: print each date where the method is non-null + String methodName = args[2]; + LocalDate start = args.length >= 4 ? LocalDate.parse(args[3]) : LocalDate.of(2017, 10, 1); + int days = args.length >= 5 ? Integer.parseInt(args[4]) : 40; + GeoLocation scanLocation = args.length >= 6 && "norway".equals(args[5]) + ? TestLocations.norway(java.time.ZoneId.of("Europe/Oslo")) : lakewood; + ComprehensiveZmanimCalendar scanCal = new ComprehensiveZmanimCalendar(scanLocation); + for (int i = 0; i < days; i++) { + LocalDate d = start.plusDays(i); + scanCal.setLocalDate(d); + Object value = scanCal.getClass().getMethod(methodName).invoke(scanCal); + if (value != null) { + System.out.println(d + "\t" + methodName + "\t" + value); + } + } + return; + } + + AstronomicalCalendar calendar; + switch (which) { + case "astronomical": + calendar = new AstronomicalCalendar(lakewood); + break; + case "zmanim": + calendar = new ZmanimCalendar(lakewood); + break; + default: + calendar = new ComprehensiveZmanimCalendar(lakewood); + break; + } + calendar.setLocalDate(date); + + List names = new ArrayList<>(); + for (Method method : calendar.getClass().getMethods()) { + if (method.getParameterCount() != 0) { + continue; + } + if (!(method.getName().startsWith("get") || method.getName().startsWith("is"))) { + continue; + } + Class returnType = method.getReturnType(); + if (returnType != Instant.class && returnType != Duration.class) { + continue; + } + names.add(method.getName()); + } + names.sort(String::compareTo); + + for (String name : names) { + Object value = calendar.getClass().getMethod(name).invoke(calendar); + String formatted; + if (value == null) { + formatted = "null"; + } else if (value instanceof Instant) { + formatted = ((Instant) value).toString(); + } else { + formatted = ((Duration) value).toString(); + } + System.out.println(name + "\t" + formatted); + } + System.out.println("# total: " + names.size()); + } + + private RegressionValueDumper() { + } +} diff --git a/src/test/java/com/kosherjava/zmanim/SpecialDayZmanimTest.java b/src/test/java/com/kosherjava/zmanim/SpecialDayZmanimTest.java new file mode 100644 index 00000000..b8410d43 --- /dev/null +++ b/src/test/java/com/kosherjava/zmanim/SpecialDayZmanimTest.java @@ -0,0 +1,150 @@ +/* + * Zmanim Java API + * Copyright © 2004-2026 Eliyahu Hershfeld + * + * This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General + * Public License as published by the Free Software Foundation; version 2.1 of the License. + * + * This library is distributed in the hope that it will be useful,but WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA, + * or connect to: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html + */ +package com.kosherjava.zmanim; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +import java.time.Instant; +import java.time.LocalDate; +import java.time.ZoneId; + +import org.junit.Test; + +/** + * Coverage for the location- and date-gated zmanim of {@link ComprehensiveZmanimCalendar} that only produce a value + * under specific conditions: the chametz times (only on Erev Pesach, 14 Nissan), the Kidush Levana / + * molad times (only within their lunar-month windows), and the polar zmanim (only inside the Arctic/Antarctic + * circle during polar day/night, when there is no ordinary sunrise or sunset). The broader + * {@link ComprehensiveZmanimCalendarTest} net pins these as {@code null} for its temperate ordinary-day fixture; this + * test verifies they return the correct times under the conditions they actually apply. + * + * @author Test coverage + */ +public class SpecialDayZmanimTest { + + private ComprehensiveZmanimCalendar calendarFor(LocalDate date) { + ComprehensiveZmanimCalendar calendar = new ComprehensiveZmanimCalendar(TestLocations.lakewood()); + calendar.setLocalDate(date); + return calendar; + } + + private void assertInstant(String label, String expectedIso, Instant actual) { + assertEquals(label, Instant.parse(expectedIso), actual); + } + + /** 14 Nissan 5777 (Erev Pesach) falls on 2017-04-10. */ + private static final LocalDate EREV_PESACH = LocalDate.of(2017, 4, 10); + + @Test + public void chametzTimesOnErevPesach() { + ComprehensiveZmanimCalendar calendar = calendarFor(EREV_PESACH); + + // All ten sof zman achilas/biur chametz opinions must produce a time on Erev Pesach. + assertNotNull(calendar.getSofZmanAchilasChametzGRA()); + assertNotNull(calendar.getSofZmanAchilasChametzMGA72Minutes()); + assertNotNull(calendar.getSofZmanAchilasChametzMGA72MinutesZmanis()); + assertNotNull(calendar.getSofZmanAchilasChametzMGA16Point1Degrees()); + assertNotNull(calendar.getSofZmanAchilasChametzBaalHatanya()); + assertNotNull(calendar.getSofZmanBiurChametzGRA()); + assertNotNull(calendar.getSofZmanBiurChametzMGA72Minutes()); + assertNotNull(calendar.getSofZmanBiurChametzMGA72MinutesZmanis()); + assertNotNull(calendar.getSofZmanBiurChametzMGA16Point1Degrees()); + assertNotNull(calendar.getSofZmanBiurChametzBaalHatanya()); + + // Regression on the GRA opinions; biur (end of the 5th hour) is one shaah zmanis after achilas (end of the 4th). + assertInstant("achilasGRA", "2017-04-10T14:47:45.675920561Z", calendar.getSofZmanAchilasChametzGRA()); + assertInstant("biurGRA", "2017-04-10T15:53:07.762314340Z", calendar.getSofZmanBiurChametzGRA()); + } + + @Test + public void chametzTimesAreNullOnAnOrdinaryDay() { + ComprehensiveZmanimCalendar calendar = calendarFor(LocalDate.of(2017, 10, 17)); + assertNull(calendar.getSofZmanAchilasChametzGRA()); + assertNull(calendar.getSofZmanBiurChametzGRA()); + } + + @Test + public void zmanMoladAndTchilasKidushLevana() { + // The molad of Cheshvan 5778 (the moment itself falls on 2017-10-20). + assertInstant("zmanMolad", "2017-10-20T09:52:00.170666666Z", calendarFor(LocalDate.of(2017, 10, 20)).getZmanMolad()); + + // Earliest Kidush Levana: 3 days and 7 days after the molad. + assertInstant("tchilas3Days", "2017-10-23T09:52:00.170666666Z", + calendarFor(LocalDate.of(2017, 10, 23)).getTchilasZmanKidushLevana3Days()); + assertInstant("tchilas7Days", "2017-10-27T09:52:00.170666666Z", + calendarFor(LocalDate.of(2017, 10, 27)).getTchilasZmanKidushLevana7Days()); + } + + @Test + public void sofZmanKidushLevana() { + // Latest Kidush Levana for the month whose molad was in late September 2017 (sayable through 2017-10-05). + assertInstant("sof15Days", "2017-10-05T21:07:56.837333333Z", + calendarFor(LocalDate.of(2017, 10, 5)).getSofZmanKidushLevana15Days()); + assertInstant("sofBetweenMoldos", "2017-10-05T15:29:58.503333333Z", + calendarFor(LocalDate.of(2017, 10, 5)).getSofZmanKidushLevanaBetweenMoldos()); + } + + @Test + public void kidushLevanaIsNullOutsideItsWindow() { + // 2017-10-17 is past the previous month's sof zman and before the next month's tchilas zman. + ComprehensiveZmanimCalendar calendar = calendarFor(LocalDate.of(2017, 10, 17)); + assertNull(calendar.getZmanMolad()); + assertNull(calendar.getTchilasZmanKidushLevana3Days()); + assertNull(calendar.getSofZmanKidushLevana15Days()); + } + + private ComprehensiveZmanimCalendar norwayCalendarFor(LocalDate date) { + ComprehensiveZmanimCalendar calendar = + new ComprehensiveZmanimCalendar(TestLocations.norway(ZoneId.of("Europe/Oslo"))); + calendar.setLocalDate(date); + return calendar; + } + + /** 2017-06-21 (summer solstice) is inside the polar day at the Norway fixture: no ordinary sunrise or sunset. */ + private static final LocalDate NORWAY_POLAR_DAY = LocalDate.of(2017, 6, 21); + + @Test + public void polarZmanimDuringPolarDay() { + ComprehensiveZmanimCalendar calendar = norwayCalendarFor(NORWAY_POLAR_DAY); + + // Precondition: there is genuinely no sunrise/sunset here, which is what activates the polar alternatives. + assertNull(calendar.getSunrise()); + assertNull(calendar.getSunset()); + + // Ben Ish Chai: use the sun's crossing of due east (90) / due west (270) as the "sunrise"/"sunset". + assertInstant("polarSunriseBenIshChai", "2017-06-21T06:01:13.021342105Z", calendar.getPolarSunriseBenIshChai()); + assertInstant("polarSunsetBenIshChai", "2017-06-21T16:49:17.587975180Z", calendar.getPolarSunsetBenIshChai()); + assertInstant("polarPlagBenIshChai", "2017-06-21T15:41:47.112284232Z", calendar.getPolarPlagHaminchaBenIshChai()); + + // Teshuvos Vehanhagos: during polar summer the start of day is chatzos halayla. + assertInstant("polarStartOfDayTeshuvos", "2017-06-21T23:25:21.765329979Z", + calendar.getPolarStartOfDayTeshuvosVehanhagos()); + assertInstant("polarPlagTeshuvos", "2017-06-21T20:55:21.765329979Z", + calendar.getPolarPlagHaminchaTeshuvosVehanhagos()); + } + + @Test + public void polarZmanimAreNullWhereSunRisesAndSets() { + // At the temperate Lakewood fixture the sun rises and sets normally, so the polar alternatives do not apply. + ComprehensiveZmanimCalendar calendar = calendarFor(LocalDate.of(2017, 10, 17)); + assertNull(calendar.getPolarSunriseBenIshChai()); + assertNull(calendar.getPolarSunsetBenIshChai()); + assertNull(calendar.getPolarPlagHaminchaBenIshChai()); + assertNull(calendar.getPolarStartOfDayTeshuvosVehanhagos()); + assertNull(calendar.getPolarPlagHaminchaTeshuvosVehanhagos()); + } +} diff --git a/src/test/java/com/kosherjava/zmanim/TestLocations.java b/src/test/java/com/kosherjava/zmanim/TestLocations.java new file mode 100644 index 00000000..a1e00f1c --- /dev/null +++ b/src/test/java/com/kosherjava/zmanim/TestLocations.java @@ -0,0 +1,121 @@ +/* + * Zmanim Java API + * Copyright © 2004-2026 Eliyahu Hershfeld + * + * This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General + * Public License as published by the Free Software Foundation; version 2.1 of the License. + * + * This library is distributed in the hope that it will be useful,but WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA, + * or connect to: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html + */ +package com.kosherjava.zmanim; + +import java.time.ZoneId; + +import com.kosherjava.zmanim.util.GeoLocation; + +/** + * Shared test fixture holding the reference locations used across the calculator and calendar test suites. The + * locations span both hemispheres, the equator, the dateline and the Arctic Circle to exercise a range of edge cases. + * Each location carries its real {@link ZoneId} for the calendar-level tests (the low-level calculator tests operate + * purely in UTC hours and are unaffected by the zone). + * + * @author Test coverage + */ +public final class TestLocations { + + private TestLocations() { + // utility holder; not instantiable + } + + /** Woodcliff Lake area, New Jersey. */ + public static final double NJ_LAT = 41.1181036; + public static final double NJ_LON = -74.0840691; + public static final double NJ_ELEV = 167; + + /** Los Angeles area, California. */ + public static final double LA_LAT = 34.0201613; + public static final double LA_LON = -118.6919095; + public static final double LA_ELEV = 71; + + /** Jerusalem, Israel. */ + public static final double JERUSALEM_LAT = 31.7962994; + public static final double JERUSALEM_LON = 35.1053185; + public static final double JERUSALEM_ELEV = 754; + + /** Northern Norway (inside the Arctic Circle - polar day/night edge cases). */ + public static final double NORWAY_LAT = 70.1498248; + public static final double NORWAY_LON = 9.1456867; + public static final double NORWAY_ELEV = 0; + + /** Sydney, Australia (southern hemisphere). */ + public static final double SYDNEY_LAT = -33.8688; + public static final double SYDNEY_LON = 151.2093; + public static final double SYDNEY_ELEV = 58; + + /** Macapá, Brazil (on the equator). */ + public static final double MACAPA_LAT = 0.0349; + public static final double MACAPA_LON = -51.0694; + public static final double MACAPA_ELEV = 15; + + /** Suva, Fiji (near the dateline). */ + public static final double SUVA_LAT = -18.1416; + public static final double SUVA_LON = 178.4419; + public static final double SUVA_ELEV = 6; + + /** Ushuaia, Argentina (far southern latitude). */ + public static final double USHUAIA_LAT = -54.8019; + public static final double USHUAIA_LON = -68.3030; + public static final double USHUAIA_ELEV = 23; + + /** Lakewood, New Jersey - the library's canonical reference location. */ + public static final double LAKEWOOD_LAT = 40.0721087; + public static final double LAKEWOOD_LON = -74.2400243; + public static final double LAKEWOOD_ELEV = 15; + + /** + * A {@link ZoneId} of {@code UTC}, used for the low-level calculator tests where the calculation is done entirely in + * UTC hours and the zone is irrelevant. + */ + public static final ZoneId UTC = ZoneId.of("UTC"); + + public static GeoLocation nj(ZoneId zoneId) { + return new GeoLocation("Woodcliff Lake, NJ", NJ_LAT, NJ_LON, NJ_ELEV, zoneId); + } + + public static GeoLocation la(ZoneId zoneId) { + return new GeoLocation("Los Angeles, CA", LA_LAT, LA_LON, LA_ELEV, zoneId); + } + + public static GeoLocation jerusalem(ZoneId zoneId) { + return new GeoLocation("Jerusalem, Israel", JERUSALEM_LAT, JERUSALEM_LON, JERUSALEM_ELEV, zoneId); + } + + public static GeoLocation norway(ZoneId zoneId) { + return new GeoLocation("Northern Norway", NORWAY_LAT, NORWAY_LON, NORWAY_ELEV, zoneId); + } + + public static GeoLocation sydney(ZoneId zoneId) { + return new GeoLocation("Sydney, Australia", SYDNEY_LAT, SYDNEY_LON, SYDNEY_ELEV, zoneId); + } + + public static GeoLocation macapa(ZoneId zoneId) { + return new GeoLocation("Macapá, Brazil", MACAPA_LAT, MACAPA_LON, MACAPA_ELEV, zoneId); + } + + public static GeoLocation suva(ZoneId zoneId) { + return new GeoLocation("Suva, Fiji", SUVA_LAT, SUVA_LON, SUVA_ELEV, zoneId); + } + + public static GeoLocation ushuaia(ZoneId zoneId) { + return new GeoLocation("Ushuaia, Argentina", USHUAIA_LAT, USHUAIA_LON, USHUAIA_ELEV, zoneId); + } + + public static GeoLocation lakewood() { + return new GeoLocation("Lakewood, NJ", LAKEWOOD_LAT, LAKEWOOD_LON, LAKEWOOD_ELEV, ZoneId.of("America/New_York")); + } +} diff --git a/src/test/java/com/kosherjava/zmanim/ZmanimCalendarTest.java b/src/test/java/com/kosherjava/zmanim/ZmanimCalendarTest.java new file mode 100644 index 00000000..be041f45 --- /dev/null +++ b/src/test/java/com/kosherjava/zmanim/ZmanimCalendarTest.java @@ -0,0 +1,111 @@ +/* + * Zmanim Java API + * Copyright © 2004-2026 Eliyahu Hershfeld + * + * This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General + * Public License as published by the Free Software Foundation; version 2.1 of the License. + * + * This library is distributed in the hope that it will be useful,but WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA, + * or connect to: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html + */ +package com.kosherjava.zmanim; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; + +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDate; + +import org.junit.Test; + +/** + * Regression and behavioral coverage for {@link ZmanimCalendar}, the standard-zmanim entry point. Every zero-argument + * zman getter is pinned against the library's own current output for the fixture (Lakewood, NJ on 2017-10-17), and the + * elevation toggle is verified to actually move the sunrise-based zmanim. (The full opinion-heavy surface is covered by + * {@link ComprehensiveZmanimCalendarTest}.) + * + * @author Test coverage + */ +public class ZmanimCalendarTest { + + private static final LocalDate FIXTURE_DATE = LocalDate.of(2017, 10, 17); + + private ZmanimCalendar fixtureCalendar() { + ZmanimCalendar calendar = new ZmanimCalendar(TestLocations.lakewood()); + calendar.setLocalDate(FIXTURE_DATE); + return calendar; + } + + private void assertInstant(String label, String expectedIso, Instant actual) { + assertEquals(label, Instant.parse(expectedIso), actual); + } + + @Test + public void alosAndMisheyakir() { + ZmanimCalendar calendar = fixtureCalendar(); + assertInstant("alos16.1", "2017-10-17T09:49:30.219906135Z", calendar.getAlos16Point1Degrees()); + assertInstant("alos72", "2017-10-17T09:57:51.403184642Z", calendar.getAlos72Minutes()); + } + + @Test + public void sofZmanShmaAndTfila() { + ZmanimCalendar calendar = fixtureCalendar(); + assertInstant("sofZmanShmaGRA", "2017-10-17T13:55:53.352746510Z", calendar.getSofZmanShmaGRA()); + assertInstant("sofZmanShmaMGA72", "2017-10-17T13:19:53.352746510Z", calendar.getSofZmanShmaMGA72Minutes()); + assertInstant("sofZmanTfilaGRA", "2017-10-17T14:51:14.002600466Z", calendar.getSofZmanTfilaGRA()); + assertInstant("sofZmanTfilaMGA72", "2017-10-17T14:27:14.002600466Z", calendar.getSofZmanTfilaMGA72Minutes()); + } + + @Test + public void chatzos() { + ZmanimCalendar calendar = fixtureCalendar(); + assertInstant("chatzosHayom", "2017-10-17T16:42:12.781249470Z", calendar.getChatzosHayom()); + assertInstant("chatzosHayomAsHalfDay", "2017-10-17T16:41:55.302308378Z", calendar.getChatzosHayomAsHalfDay()); + assertInstant("chatzosHalayla", "2017-10-18T04:42:06.833038724Z", calendar.getChatzosHalayla()); + } + + @Test + public void minchaAndPlag() { + ZmanimCalendar calendar = fixtureCalendar(); + assertInstant("minchaGedolaGRA", "2017-10-17T17:09:35.627235356Z", calendar.getMinchaGedolaGRA()); + assertInstant("minchaKetanaGRA", "2017-10-17T19:55:37.576797224Z", calendar.getMinchaKetanaGRA()); + assertInstant("plagHaminchaGRA", "2017-10-17T21:04:48.389114669Z", calendar.getPlagHaminchaGRA()); + } + + @Test + public void candleLightingAndTzais() { + ZmanimCalendar calendar = fixtureCalendar(); + assertInstant("candleLighting", "2017-10-17T21:55:59.201432122Z", calendar.getCandleLighting()); + assertInstant("tzais72", "2017-10-17T23:25:59.201432122Z", calendar.getTzais72Minutes()); + assertInstant("tzaisGeonim8.5", "2017-10-17T22:54:29.772724455Z", calendar.getTzaisGeonim8Point5Degrees()); + } + + @Test + public void shaahZmanis() { + ZmanimCalendar calendar = fixtureCalendar(); + assertEquals("shaahZmanisGRA", Duration.parse("PT55M20.649853956S"), calendar.getShaahZmanisGRA()); + assertEquals("shaahZmanis72", Duration.parse("PT1H7M20.649853956S"), calendar.getShaahZmanis72Minutes()); + } + + /** + * Toggling {@link ZmanimCalendar#setUseElevation(boolean)} must change the sunrise/sunset-based zmanim, since the + * elevation adjustment shifts the underlying sunrise. Written relative to the default so it does not depend on what + * that default currently is. + */ + @Test + public void elevationToggleAffectsZmanim() { + ZmanimCalendar calendar = fixtureCalendar(); + boolean defaultSetting = calendar.isUseElevation(); + Instant atDefault = calendar.getSofZmanShmaGRA(); + + calendar.setUseElevation(!defaultSetting); + Instant toggled = calendar.getSofZmanShmaGRA(); + + assertNotEquals(atDefault, toggled); + } +} diff --git a/src/test/java/com/kosherjava/zmanim/hebrewcalendar/JewishCalendarHolidayTest.java b/src/test/java/com/kosherjava/zmanim/hebrewcalendar/JewishCalendarHolidayTest.java new file mode 100644 index 00000000..abd7090a --- /dev/null +++ b/src/test/java/com/kosherjava/zmanim/hebrewcalendar/JewishCalendarHolidayTest.java @@ -0,0 +1,89 @@ +/* + * Zmanim Java API + * Copyright © 2004-2026 Eliyahu Hershfeld + * + * This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General + * Public License as published by the Free Software Foundation; version 2.1 of the License. + * + * This library is distributed in the hope that it will be useful,but WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA, + * or connect to: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html + */ +package com.kosherjava.zmanim.hebrewcalendar; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.time.LocalDate; + +import org.junit.Test; + +/** + * Coverage for {@link JewishCalendar} holiday / special-day predicates and {@link HebrewDateFormatter#formatParsha}. + * These are deterministic halachic classifications asserted against known dates. + * + * @author Test coverage + */ +public class JewishCalendarHolidayTest { + + @Test + public void roshHashanaAndTzomGedaliah() { + JewishCalendar date = new JewishCalendar(LocalDate.of(2023, 9, 16)); + assertTrue(date.isRoshHashana()); + + date.plusDays(1); + assertTrue(date.isRoshHashana()); + + date.plusDays(1); // Tzom Gedaliah (3 Tishrei) + assertFalse(date.isRoshHashana()); + assertTrue(date.isTaanis()); + } + + @Test + public void parsha() { + HebrewDateFormatter formatter = new HebrewDateFormatter(); + assertEquals("Bereshis", formatter.formatParsha(new JewishCalendar(LocalDate.of(2023, 10, 14)))); + + HebrewDateFormatter hebrew = new HebrewDateFormatter(); + hebrew.setHebrewFormat(true); + assertEquals("בראשית", hebrew.formatParsha(new JewishCalendar(LocalDate.of(2023, 10, 14)))); + + assertEquals("Terumah", formatter.formatParsha(new JewishCalendar(LocalDate.of(2024, 2, 17)))); + } + + @Test + public void purimAndShushanPurim() { + JewishCalendar date = new JewishCalendar(); + + // 14 Adar in a non-leap year: Purim in unwalled cities, but Shushan Purim (15th) in walled cities. + date.setJewishDate(5783, JewishDate.ADAR, 14); + assertTrue(date.isPurim()); + date.setIsMukafChoma(true); + assertFalse(date.isPurim()); + + // 15 Adar: Shushan Purim, observed as Purim in walled cities. + date.plusDays(1); + assertTrue(date.isPurim()); + date.setIsMukafChoma(false); + assertFalse(date.isPurim()); + + // In a leap year Purim falls in Adar II, so Adar (I) 14 is not Purim. + date.setJewishDate(5784, JewishDate.ADAR, 14); + assertFalse(date.isPurim()); + } + + @Test + public void assurBemelacha() { + // Yom Kippur (10 Tishrei) is assur bemelacha. + JewishCalendar yomKippur = new JewishCalendar(5784, JewishDate.TISHREI, 10); + assertTrue(yomKippur.isAssurBemelacha()); + + // An ordinary weekday is not. + JewishCalendar weekday = new JewishCalendar(5784, JewishDate.CHESHVAN, 5); + assertFalse(weekday.isAssurBemelacha()); + } +} diff --git a/src/test/java/com/kosherjava/zmanim/hebrewcalendar/TefilaRulesTest.java b/src/test/java/com/kosherjava/zmanim/hebrewcalendar/TefilaRulesTest.java new file mode 100644 index 00000000..fa1837c1 --- /dev/null +++ b/src/test/java/com/kosherjava/zmanim/hebrewcalendar/TefilaRulesTest.java @@ -0,0 +1,75 @@ +/* + * Zmanim Java API + * Copyright © 2004-2026 Eliyahu Hershfeld + * + * This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General + * Public License as published by the Free Software Foundation; version 2.1 of the License. + * + * This library is distributed in the hope that it will be useful,but WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA, + * or connect to: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html + */ +package com.kosherjava.zmanim.hebrewcalendar; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.time.LocalDate; + +import org.junit.Test; + +/** + * Coverage for the {@link TefilaRules} daily-tefila predicates on two representative dates: an ordinary weekday during + * the summer, and the first day of Sukkos. + * + * @author Test coverage + */ +public class TefilaRulesTest { + + private final TefilaRules rules = new TefilaRules(); + + @Test + public void ordinarySummerWeekday() { + JewishCalendar date = new JewishCalendar(LocalDate.of(2023, 8, 21)); + + assertTrue(rules.isTachanunRecitedShacharis(date)); + assertTrue(rules.isTachanunRecitedMincha(date)); + assertFalse(rules.isVeseinTalUmatarStartDate(date)); + assertFalse(rules.isVeseinTalUmatarStartingTonight(date)); + assertFalse(rules.isVeseinTalUmatarRecited(date)); + assertTrue(rules.isVeseinBerachaRecited(date)); + assertFalse(rules.isMashivHaruachStartDate(date)); + assertFalse(rules.isMashivHaruachEndDate(date)); + assertFalse(rules.isMashivHaruachRecited(date)); + assertTrue(rules.isMoridHatalRecited(date)); + assertFalse(rules.isHallelRecited(date)); + assertFalse(rules.isHallelShalemRecited(date)); + assertFalse(rules.isAlHanissimRecited(date)); + assertFalse(rules.isYaalehVeyavoRecited(date)); + assertTrue(rules.isMizmorLesodaRecited(date)); + } + + @Test + public void firstDayOfSukkos() { + JewishCalendar date = new JewishCalendar(LocalDate.of(2023, 10, 7)); + + assertFalse(rules.isTachanunRecitedShacharis(date)); + assertFalse(rules.isTachanunRecitedMincha(date)); + assertFalse(rules.isVeseinTalUmatarStartDate(date)); + assertFalse(rules.isVeseinTalUmatarStartingTonight(date)); + assertFalse(rules.isVeseinTalUmatarRecited(date)); + assertTrue(rules.isVeseinBerachaRecited(date)); + assertTrue(rules.isMashivHaruachStartDate(date)); + assertFalse(rules.isMashivHaruachEndDate(date)); + assertFalse(rules.isMashivHaruachRecited(date)); + assertTrue(rules.isMoridHatalRecited(date)); + assertTrue(rules.isHallelRecited(date)); + assertTrue(rules.isHallelShalemRecited(date)); + assertFalse(rules.isAlHanissimRecited(date)); + assertTrue(rules.isYaalehVeyavoRecited(date)); + assertFalse(rules.isMizmorLesodaRecited(date)); + } +} diff --git a/src/test/java/com/kosherjava/zmanim/util/AstronomicalCalculatorTest.java b/src/test/java/com/kosherjava/zmanim/util/AstronomicalCalculatorTest.java new file mode 100644 index 00000000..adbb8919 --- /dev/null +++ b/src/test/java/com/kosherjava/zmanim/util/AstronomicalCalculatorTest.java @@ -0,0 +1,147 @@ +/* + * Zmanim Java API + * Copyright © 2004-2026 Eliyahu Hershfeld + * + * This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General + * Public License as published by the Free Software Foundation; version 2.1 of the License. + * + * This library is distributed in the hope that it will be useful,but WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA, + * or connect to: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html + */ +package com.kosherjava.zmanim.util; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertTrue; + +import java.time.LocalDate; + +import org.junit.Test; + +/** + * Coverage for the {@link AstronomicalCalculator} abstract base: the pluggable-calculator contract ({@code getDefault}, + * naming, {@code equals}/{@code clone}), the refraction / solar-radius / earth-radius configuration, the date-based + * {@link AstronomicalCalculator#getApparentSolarRadius(LocalDate) apparent solar radius}, and the shared degree-based + * trig helpers. The trig helpers are Tier-A checks verified against {@link Math}; this test lives in the + * {@code util} package so it can reach those {@code protected static} helpers. + * + * @author Test coverage + */ +public class AstronomicalCalculatorTest { + + @Test + public void getDefaultReturnsNoaaCalculator() { + AstronomicalCalculator calculator = AstronomicalCalculator.getDefault(); + assertTrue(calculator instanceof NOAACalculator); + } + + @Test + public void calculatorNames() { + assertEquals("US National Oceanic and Atmospheric Administration Algorithm", + new NOAACalculator().getCalculatorName()); + assertEquals("US Naval Almanac Algorithm", new SunTimesCalculator().getCalculatorName()); + assertEquals("Jean Meeus Higher-Accuracy (VSOP87) Algorithm", new MeeusCalculator().getCalculatorName()); + assertEquals("NREL Solar Position Algorithm", new SPACalculator().getCalculatorName()); + } + + @Test + public void defaultConfiguration() { + AstronomicalCalculator calculator = new NOAACalculator(); + assertEquals(34 / 60d, calculator.getRefraction(), 0); + assertEquals(16 / 60d, calculator.getSolarRadius(), 0); + assertEquals(6371.0088, calculator.getEarthRadius(), 0); + assertTrue(calculator.isUseApparentSolarRadius()); + } + + @Test + public void refractionAndEarthRadiusSetters() { + AstronomicalCalculator calculator = new NOAACalculator(); + calculator.setRefraction(0.5); + calculator.setEarthRadius(6371.0); + assertEquals(0.5, calculator.getRefraction(), 0); + assertEquals(6371.0, calculator.getEarthRadius(), 0); + } + + @Test + public void setSolarRadiusDisablesApparentRadius() { + AstronomicalCalculator calculator = new NOAACalculator(); + calculator.setSolarRadius(16 / 60d); + assertEquals(16 / 60d, calculator.getSolarRadius(), 0); + assertFalse(calculator.isUseApparentSolarRadius()); + } + + @Test(expected = IllegalArgumentException.class) + public void setSolarRadiusRejectsNegative() { + new NOAACalculator().setSolarRadius(-1); + } + + @Test + public void apparentSolarRadius() { + AstronomicalCalculator calculator = new NOAACalculator(); + // null date falls back to the fixed 16' radius + assertEquals(16 / 60d, calculator.getApparentSolarRadius(null), 0); + // table is keyed by day-of-year against the (common-year) reference year 2050 + assertEquals(0.27108024, calculator.getApparentSolarRadius(LocalDate.of(2050, 1, 1)), 1e-8); + // the perihelion (~Jan 3) is near the annual maximum, the aphelion (~Jul 5) near the minimum + assertTrue(calculator.getApparentSolarRadius(LocalDate.of(2020, 1, 3)) + > calculator.getApparentSolarRadius(LocalDate.of(2020, 7, 5))); + // Feb 29 resolves to Feb 28's value via withYear(2050) + assertEquals(calculator.getApparentSolarRadius(LocalDate.of(2020, 2, 28)), + calculator.getApparentSolarRadius(LocalDate.of(2020, 2, 29)), 0); + } + + @Test + public void equalsAndHashCode() { + AstronomicalCalculator a = new NOAACalculator(); + AstronomicalCalculator b = new NOAACalculator(); + assertEquals(a, b); + assertEquals(a.hashCode(), b.hashCode()); + + b.setRefraction(0.5); + assertNotEquals(a, b); + + // different concrete classes with identical settings are not equal + assertNotEquals(new NOAACalculator(), new SunTimesCalculator()); + } + + @Test + public void cloneProducesEqualIndependentCopy() { + AstronomicalCalculator original = new NOAACalculator(); + AstronomicalCalculator clone = original.clone(); + assertNotSame(original, clone); + assertEquals(original, clone); + + clone.setRefraction(0.1); + assertNotEquals(original, clone); + } + + /* + * Tier-A: the degree-based trig helpers must agree with java.lang.Math. They are protected static, reachable here + * because this test shares the com.kosherjava.zmanim.util package. + */ + @Test + public void degreeTrigHelpersMatchMath() { + double[] angles = {0, 30, 45, 60, 90, 123.456, -17.5, 200}; + for (double angle : angles) { + assertEquals("sin " + angle, Math.sin(Math.toRadians(angle)), + AstronomicalCalculator.sinDegrees(angle), 1e-12); + assertEquals("cos " + angle, Math.cos(Math.toRadians(angle)), + AstronomicalCalculator.cosDegrees(angle), 1e-12); + assertEquals("tan " + angle, Math.tan(Math.toRadians(angle)), + AstronomicalCalculator.tanDegrees(angle), 1e-9); + } + double[] ratios = {-1, -0.5, 0, 0.25, 0.5, 1}; + for (double ratio : ratios) { + assertEquals("asin " + ratio, Math.toDegrees(Math.asin(ratio)), + AstronomicalCalculator.asinDegrees(ratio), 1e-12); + assertEquals("acos " + ratio, Math.toDegrees(Math.acos(ratio)), + AstronomicalCalculator.acosDegrees(ratio), 1e-12); + } + } +} diff --git a/src/test/java/com/kosherjava/zmanim/util/GeoLocationTest.java b/src/test/java/com/kosherjava/zmanim/util/GeoLocationTest.java new file mode 100644 index 00000000..8d489c9e --- /dev/null +++ b/src/test/java/com/kosherjava/zmanim/util/GeoLocationTest.java @@ -0,0 +1,197 @@ +/* + * Zmanim Java API + * Copyright © 2004-2026 Eliyahu Hershfeld + * + * This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General + * Public License as published by the Free Software Foundation; version 2.1 of the License. + * + * This library is distributed in the hope that it will be useful,but WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA, + * or connect to: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html + */ +package com.kosherjava.zmanim.util; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertTrue; + +import java.time.Instant; +import java.time.ZoneId; + +import org.junit.Test; + +/** + * Coverage for {@link GeoLocation}. The geodesic (Vincenty) and rhumb-line results are a Tier-A independently + * verifiable anchor: the coordinates are the classic Ordnance Survey reference points (Cornwall and Scotland). The + * rest is regression / behavioral coverage of construction, validation, the local-mean-time / antimeridian helpers, and + * {@code equals}/{@code hashCode}/{@code clone}/{@code toXML}. + * + * @author Test coverage + */ +public class GeoLocationTest { + + private static final ZoneId NY = ZoneId.of("America/New_York"); + private static final Instant JAN_1_2017 = Instant.parse("2017-01-01T00:00:00Z"); + + @Test + public void constructWithDefaults() { + GeoLocation geoLocation = new GeoLocation(); + assertEquals(51.4772, geoLocation.getLatitude(), 0); + assertEquals(0.0, geoLocation.getLongitude(), 0); + assertEquals(ZoneId.of("GMT"), geoLocation.getZoneId()); + assertEquals(0.0, geoLocation.getElevation(), 0); + assertEquals("Greenwich, England", geoLocation.getLocationName()); + } + + @Test + public void constructWithData() { + GeoLocation geoLocation = new GeoLocation("Lakewood, NJ", 40.0828, -74.2094, 20, NY); + assertEquals(40.0828, geoLocation.getLatitude(), 0); + assertEquals(-74.2094, geoLocation.getLongitude(), 0); + assertEquals(NY, geoLocation.getZoneId()); + assertEquals(20.0, geoLocation.getElevation(), 0); + assertEquals("Lakewood, NJ", geoLocation.getLocationName()); + } + + @Test + public void settersUpdateValues() { + GeoLocation geoLocation = new GeoLocation(); + geoLocation.setLatitude(41.1181036); + geoLocation.setLongitude(-74.2094); + geoLocation.setZoneId(NY); + geoLocation.setElevation(20); + geoLocation.setLocationName("Lakewood, NJ"); + + assertEquals(41.1181036, geoLocation.getLatitude(), 0); + assertEquals(-74.2094, geoLocation.getLongitude(), 0); + assertEquals(NY, geoLocation.getZoneId()); + assertEquals(20.0, geoLocation.getElevation(), 0); + assertEquals("Lakewood, NJ", geoLocation.getLocationName()); + } + + @Test(expected = IllegalArgumentException.class) + public void setLatitudeRejectsOutOfRange() { + new GeoLocation().setLatitude(90.1); + } + + @Test(expected = IllegalArgumentException.class) + public void setLatitudeRejectsNaN() { + new GeoLocation().setLatitude(Double.NaN); + } + + @Test(expected = IllegalArgumentException.class) + public void setLongitudeRejectsOutOfRange() { + new GeoLocation().setLongitude(-180.1); + } + + @Test(expected = IllegalArgumentException.class) + public void setLongitudeRejectsNaN() { + new GeoLocation().setLongitude(Double.NaN); + } + + @Test(expected = IllegalArgumentException.class) + public void setElevationRejectsNegative() { + new GeoLocation().setElevation(-1); + } + + @Test(expected = IllegalArgumentException.class) + public void setElevationRejectsInfinite() { + new GeoLocation().setElevation(Double.POSITIVE_INFINITY); + } + + @Test(expected = IllegalArgumentException.class) + public void constructorValidatesLatitude() { + new GeoLocation("bad", 100, 0, ZoneId.of("GMT")); + } + + @Test + public void getLocalMeanTimeOffset() { + assertEquals(0L, new GeoLocation().getLocalMeanTimeOffset(JAN_1_2017)); + + GeoLocation lakewood = new GeoLocation("Lakewood, NJ", 40.0828, -74.2094, 20, NY); + assertEquals(189744L, lakewood.getLocalMeanTimeOffset(JAN_1_2017)); + } + + @Test + public void antimeridianAdjustment() { + assertEquals(0, new GeoLocation().getAntimeridianAdjustment(JAN_1_2017)); + + GeoLocation ny = new GeoLocation("Lakewood, NJ", 40.0828, -74.2094, 20, NY); + assertEquals(0, ny.getAntimeridianAdjustment(JAN_1_2017)); + + // Apia, Samoa (UTC+14 in January) is well east of its longitude-implied zone: roll the date back a day. + GeoLocation apia = new GeoLocation("Apia, Samoa", -13.8599098, -171.8031745, 1858, ZoneId.of("Pacific/Apia")); + assertEquals(-1, apia.getAntimeridianAdjustment(JAN_1_2017)); + + // A location east of the dateline forced onto a UTC-12 zone: roll the date forward a day. + GeoLocation westOfLine = new GeoLocation("GMT +12", -13.8599098, 179, 0, ZoneId.of("Etc/GMT+12")); + assertEquals(1, westOfLine.getAntimeridianAdjustment(JAN_1_2017)); + } + + /* + * Tier-A: Vincenty and rhumb-line results against the classic Ordnance Survey reference points. + */ + @Test + public void vincentyFormulae() { + GeoLocation pointA = new GeoLocation("A", 50.06632222222222, -5.71475, ZoneId.of("Etc/GMT+12")); + GeoLocation pointB = new GeoLocation("B", 58.64402222222222, -3.0700944444444445, ZoneId.of("Etc/GMT+12")); + + assertEquals(9.14186191, pointA.getGeodesicInitialBearing(pointB), 1e-8); + assertEquals(11.29720127, pointA.getGeodesicFinalBearing(pointB), 1e-8); + assertEquals(969954.11445043, pointA.getGeodesicDistance(pointB), 1e-5); + } + + @Test + public void geodesicDistanceForCoincidentPointsIsZero() { + GeoLocation point = new GeoLocation("Lakewood, NJ", 40.0828, -74.2094, NY); + assertEquals(0.0, point.getGeodesicDistance(point), 0); + } + + @Test + public void rhumbLine() { + GeoLocation pointA = new GeoLocation("A", 50.06638888888889, -5.714722222222222, ZoneId.of("Etc/GMT+12")); + GeoLocation pointB = new GeoLocation("B", 58.64388888888889, -3.07, ZoneId.of("Etc/GMT+12")); + + assertEquals(10.14069288, pointA.getRhumbLineBearing(pointB), 1e-8); + assertEquals(969995.8368008, pointA.getRhumbLineDistance(pointB), 1e-5); + } + + @Test + public void equalsAndHashCode() { + GeoLocation a = new GeoLocation("Lakewood, NJ", 40.0828, -74.2094, 20, NY); + GeoLocation b = new GeoLocation("Lakewood, NJ", 40.0828, -74.2094, 20, NY); + assertEquals(a, b); + assertEquals(a.hashCode(), b.hashCode()); + + GeoLocation c = new GeoLocation("Lakewood, NJ", 41.0, -74.2094, 20, NY); + assertNotEquals(a, c); + } + + @Test + public void cloneProducesIndependentCopy() { + GeoLocation geoLocation = new GeoLocation("Lakewood, NJ", 40.0828, -74.2094, 20, NY); + GeoLocation copy = (GeoLocation) geoLocation.clone(); + + assertNotSame(geoLocation, copy); + assertEquals(geoLocation, copy); + + copy.setLatitude(10); + assertEquals(40.0828, geoLocation.getLatitude(), 0); + assertEquals(10.0, copy.getLatitude(), 0); + } + + @Test + public void toXmlContainsFields() { + GeoLocation geoLocation = new GeoLocation("Lakewood, NJ", 40.0828, -74.2094, 20, NY); + String xml = geoLocation.toXML(); + assertTrue(xml.contains("Lakewood, NJ")); + assertTrue(xml.contains("40.0828")); + assertTrue(xml.contains("-74.2094")); + assertTrue(xml.contains("20.0 Meters")); + assertTrue(xml.contains("America/New_York")); + } +} diff --git a/src/test/java/com/kosherjava/zmanim/util/MeeusCalculatorTest.java b/src/test/java/com/kosherjava/zmanim/util/MeeusCalculatorTest.java new file mode 100644 index 00000000..cfb14f01 --- /dev/null +++ b/src/test/java/com/kosherjava/zmanim/util/MeeusCalculatorTest.java @@ -0,0 +1,256 @@ +/* + * Zmanim Java API + * Copyright © 2004-2026 Eliyahu Hershfeld + * + * This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General + * Public License as published by the Free Software Foundation; version 2.1 of the License. + * + * This library is distributed in the hope that it will be useful,but WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA, + * or connect to: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html + */ +package com.kosherjava.zmanim.util; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; + +import org.junit.Test; + +import com.kosherjava.zmanim.TestLocations; + +/** + * Regression coverage for {@link MeeusCalculator}, the higher-accuracy Jean Meeus (VSOP87) engine with ΔT + * support. Expected values are the library's own current output (defaults, ΔT on) asserted to 8 decimals, plus a + * ΔT-disabled variant via {@link MeeusCalculator#setApplyDeltaT(boolean)}. + * + * @author Test coverage + */ +public class MeeusCalculatorTest { + + private static final double DELTA = 1e-8; + private static final double ZENITH = 90.0; + private static final double NO_EVENT = Double.NaN; + + private static final DateTimeFormatter DATE_TIME = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + private MeeusCalculator calculator() { + return new MeeusCalculator(); + } + + private GeoLocation geo(String location) { + switch (location) { + case "NJ": return TestLocations.nj(TestLocations.UTC); + case "LA": return TestLocations.la(TestLocations.UTC); + case "JERUSALEM": return TestLocations.jerusalem(TestLocations.UTC); + case "NORWAY": return TestLocations.norway(TestLocations.UTC); + case "SYDNEY": return TestLocations.sydney(TestLocations.UTC); + case "MACAPA": return TestLocations.macapa(TestLocations.UTC); + case "SUVA": return TestLocations.suva(TestLocations.UTC); + case "USHUAIA": return TestLocations.ushuaia(TestLocations.UTC); + default: throw new IllegalArgumentException("Unknown location: " + location); + } + } + + private LocalDate date(String iso) { + return LocalDate.parse(iso); + } + + private Instant instant(String isoDateTime) { + return LocalDateTime.parse(isoDateTime, DATE_TIME).toInstant(ZoneOffset.UTC); + } + + private void assertUtcHour(String label, double expected, double actual) { + if (Double.isNaN(expected)) { + assertTrue(label + " should have no event (NaN)", Double.isNaN(actual)); + } else { + assertEquals(label, expected, actual, DELTA); + } + } + + @Test + public void getUTCSunrise() { + Object[][] cases = { + {"NJ Standard Date", "2017-10-17", "NJ", 11.13565507}, + {"LA Standard Date", "2017-10-17", "LA", 14.00730639}, + {"Jerusalem Hist. Date", "1955-02-26", "JERUSALEM", 4.11849856}, + {"Norway Polar Solstice", "2017-06-21", "NORWAY", NO_EVENT}, + {"Sydney Leap Day", "2020-02-29", "SYDNEY", 19.68266925}, + {"Macapa Equator New Year", "2000-01-01", "MACAPA", 9.38974837}, + {"Suva Fiji Solstice", "2023-06-21", "SUVA", 18.60510228}, + {"Ushuaia Southern Winter", "2017-06-21", "USHUAIA", 12.95697826}, + {"NJ Future DeltaT", "2100-07-04", "NJ", 9.44873166}, + }; + for (Object[] c : cases) { + double actual = calculator().getUTCSunrise(date((String) c[1]), geo((String) c[2]), ZENITH, true); + assertUtcHour((String) c[0], (double) c[3], actual); + } + } + + @Test + public void getUTCSunset() { + Object[][] cases = { + {"NJ Standard Date", "2017-10-17", "NJ", 22.24115497}, + {"LA Standard Date", "2017-10-17", "LA", 1.31857880}, + {"Jerusalem Hist. Date", "1955-02-26", "JERUSALEM", 15.64527430}, + {"Norway Polar Solstice", "2017-06-21", "NORWAY", NO_EVENT}, + {"Sydney Leap Day", "2020-02-29", "SYDNEY", 8.56381339}, + {"Macapa Equator New Year", "2000-01-01", "MACAPA", 21.53114700}, + {"Suva Fiji Solstice", "2023-06-21", "SUVA", 5.65803778}, + {"Ushuaia Southern Winter", "2017-06-21", "USHUAIA", 20.21223110}, + {"NJ Future DeltaT", "2100-07-04", "NJ", 0.58039250}, + }; + for (Object[] c : cases) { + double actual = calculator().getUTCSunset(date((String) c[1]), geo((String) c[2]), ZENITH, true); + assertUtcHour((String) c[0], (double) c[3], actual); + } + } + + @Test + public void getUTCNoon() { + Object[][] cases = { + {"NJ Standard Date", "2017-10-17", "NJ", 16.69344845}, + {"LA Standard Date", "2017-10-17", "LA", 19.66689969}, + {"Jerusalem Hist. Date", "1955-02-26", "JERUSALEM", 9.87812628}, + {"Norway Polar Solstice", "2017-06-21", "NORWAY", 11.42050168}, + {"Sydney Leap Day", "2020-02-29", "SYDNEY", 2.12793408}, + {"Macapa Equator New Year", "2000-01-01", "MACAPA", 15.46045983}, + {"Suva Fiji Solstice", "2023-06-21", "SUVA", 0.13159547}, + {"Ushuaia Southern Winter", "2017-06-21", "USHUAIA", 16.58453220}, + {"NJ Future DeltaT", "2100-07-04", "NJ", 17.01674900}, + }; + for (Object[] c : cases) { + double actual = calculator().getUTCNoon(date((String) c[1]), geo((String) c[2])); + assertUtcHour((String) c[0], (double) c[3], actual); + } + } + + @Test + public void getUTCMidnight() { + Object[][] cases = { + {"NJ Standard Date", "2017-10-17", "NJ", 4.69183111}, + {"LA Standard Date", "2017-10-17", "LA", 7.66529295}, + {"Jerusalem Hist. Date", "1955-02-26", "JERUSALEM", 21.87675832}, + {"Norway Polar Solstice", "2017-06-21", "NORWAY", 23.42232487}, + {"Sydney Leap Day", "2020-02-29", "SYDNEY", 14.12636235}, + {"Macapa Equator New Year", "2000-01-01", "MACAPA", 3.46440271}, + {"Suva Fiji Solstice", "2023-06-21", "SUVA", 12.13342359}, + {"Ushuaia Southern Winter", "2017-06-21", "USHUAIA", 4.58635433}, + {"NJ Future DeltaT", "2100-07-04", "NJ", 5.01821298}, + }; + for (Object[] c : cases) { + double actual = calculator().getUTCMidnight(date((String) c[1]), geo((String) c[2])); + assertUtcHour((String) c[0], (double) c[3], actual); + } + } + + @Test + public void getTimeAtAzimuth() { + Object[][] cases = { + {"NJ East", "2017-10-17", "NJ", 90.0, 9.96630242}, + {"NJ West", "2017-10-17", "NJ", 270.0, 23.43696025}, + {"LA East", "2017-10-17", "LA", 90.0, 12.71802243}, + {"LA West", "2017-10-17", "LA", 270.0, 2.63792382}, + {"Sydney East", "2020-02-29", "SYDNEY", 90.0, 20.92601400}, + {"Sydney West", "2020-02-29", "SYDNEY", 270.0, 7.34685529}, + {"Macapa East No Cross", "2000-01-01", "MACAPA", 90.0, NO_EVENT}, + {"Polar East", "2017-06-21", "NORWAY", 90.0, 6.01986159}, + }; + for (Object[] c : cases) { + double actual = calculator().getTimeAtAzimuth(date((String) c[1]), geo((String) c[2]), (double) c[3]); + assertUtcHour((String) c[0], (double) c[4], actual); + } + } + + @Test + public void getSolarElevation() { + Object[][] cases = { + {"NJ Noon", "2017-10-17 12:00:00", "NJ", 8.24234881}, + {"NJ Evening", "2017-10-17 18:30:00", "NJ", 33.57213757}, + {"LA Evening", "2017-10-17 20:00:00", "LA", 46.19473579}, + {"Polar Midnight", "2017-06-21 00:00:00", "NORWAY", 3.98711013}, + {"Sydney Morning", "2020-02-29 20:00:00", "SYDNEY", 2.89445709}, + {"Macapa Local Noon", "2000-01-01 15:00:00", "MACAPA", 65.99208585}, + {"Suva Afternoon", "2023-06-21 02:00:00", "SUVA", 40.24688524}, + }; + for (Object[] c : cases) { + double actual = calculator().getSolarElevation(instant((String) c[1]), geo((String) c[2])); + assertEquals((String) c[0], (double) c[3], actual, DELTA); + } + } + + @Test + public void getSolarAzimuth() { + Object[][] cases = { + {"NJ Noon", "2017-10-17 12:00:00", "NJ", 110.13820751}, + {"NJ Evening", "2017-10-17 18:30:00", "NJ", 212.62255322}, + {"LA Evening", "2017-10-17 20:00:00", "LA", 187.12661462}, + {"Polar Midnight", "2017-06-21 00:00:00", "NORWAY", 8.01211355}, + {"Sydney Morning", "2020-02-29 20:00:00", "SYDNEY", 97.32257044}, + {"Macapa Local Noon", "2000-01-01 15:00:00", "MACAPA", 164.22488663}, + {"Suva Afternoon", "2023-06-21 02:00:00", "SUVA", 325.62695543}, + }; + for (Object[] c : cases) { + double actual = calculator().getSolarAzimuth(instant((String) c[1]), geo((String) c[2])); + assertEquals((String) c[0], (double) c[3], actual, DELTA); + } + } + + @Test + public void getUTCSunriseAtZenith() { + Object[][] cases = { + {"NJ Civil", "2017-10-17", "NJ", 96.0, 10.70916170}, + {"NJ Nautical", "2017-10-17", "NJ", 102.0, 10.17586674}, + {"NJ Astronomical", "2017-10-17", "NJ", 108.0, 9.64430764}, + {"Sydney Civil", "2020-02-29", "SYDNEY", 96.0, 19.27848095}, + }; + for (Object[] c : cases) { + double actual = calculator().getUTCSunrise(date((String) c[1]), geo((String) c[2]), (double) c[3], true); + assertUtcHour((String) c[0], (double) c[4], actual); + } + } + + @Test + public void getUTCSunriseWithoutElevationAdjustment() { + Object[][] cases = { + {"NJ No Elev Adj", "2017-10-17", "NJ", 11.17316120}, + {"Jerusalem No Elev Adj", "1955-02-26", "JERUSALEM", 4.18861444}, + }; + for (Object[] c : cases) { + double actual = calculator().getUTCSunrise(date((String) c[1]), geo((String) c[2]), ZENITH, false); + assertUtcHour((String) c[0], (double) c[3], actual); + } + } + + @Test + public void getUTCSunriseWithDeltaTDisabled() { + Object[][] cases = { + {"NJ dT off", "2017-10-17", "NJ", 11.13563996}, + {"Jerusalem dT off", "1955-02-26", "JERUSALEM", 4.11850671}, + }; + for (Object[] c : cases) { + MeeusCalculator calc = calculator(); + calc.setApplyDeltaT(false); + double actual = calc.getUTCSunrise(date((String) c[1]), geo((String) c[2]), ZENITH, true); + assertUtcHour((String) c[0], (double) c[3], actual); + } + } + + @Test(expected = IllegalArgumentException.class) + public void getTimeAtAzimuthRejectsUnsupportedAzimuth() { + calculator().getTimeAtAzimuth(date("2017-10-17"), geo("NJ"), 123.0); + } + + @Test + public void getCalculatorName() { + assertEquals("Jean Meeus Higher-Accuracy (VSOP87) Algorithm", calculator().getCalculatorName()); + } +} diff --git a/src/test/java/com/kosherjava/zmanim/util/NOAACalculatorTest.java b/src/test/java/com/kosherjava/zmanim/util/NOAACalculatorTest.java new file mode 100644 index 00000000..d18c4626 --- /dev/null +++ b/src/test/java/com/kosherjava/zmanim/util/NOAACalculatorTest.java @@ -0,0 +1,242 @@ +/* + * Zmanim Java API + * Copyright © 2004-2026 Eliyahu Hershfeld + * + * This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General + * Public License as published by the Free Software Foundation; version 2.1 of the License. + * + * This library is distributed in the hope that it will be useful,but WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA, + * or connect to: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html + */ +package com.kosherjava.zmanim.util; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; + +import org.junit.Test; + +import com.kosherjava.zmanim.TestLocations; + +/** + * Regression coverage for {@link NOAACalculator}, the library's default sunrise/sunset engine. Expected values are the + * library's own current output asserted to 8 decimals. Cases span both hemispheres, the equator, the dateline, a polar + * no-event, a leap day, a year boundary and a future ΔT date. Each test iterates a labeled case table. + * + * @author Test coverage + */ +public class NOAACalculatorTest { + + private static final double DELTA = 1e-8; + private static final double ZENITH = 90.0; // AstronomicalCalculator.GEOMETRIC_ZENITH + private static final double NO_EVENT = Double.NaN; + + private static final DateTimeFormatter DATE_TIME = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + private NOAACalculator calculator() { + return new NOAACalculator(); + } + + private GeoLocation geo(String location) { + switch (location) { + case "NJ": return TestLocations.nj(TestLocations.UTC); + case "LA": return TestLocations.la(TestLocations.UTC); + case "JERUSALEM": return TestLocations.jerusalem(TestLocations.UTC); + case "NORWAY": return TestLocations.norway(TestLocations.UTC); + case "SYDNEY": return TestLocations.sydney(TestLocations.UTC); + case "MACAPA": return TestLocations.macapa(TestLocations.UTC); + case "SUVA": return TestLocations.suva(TestLocations.UTC); + case "USHUAIA": return TestLocations.ushuaia(TestLocations.UTC); + default: throw new IllegalArgumentException("Unknown location: " + location); + } + } + + private LocalDate date(String iso) { + return LocalDate.parse(iso); + } + + private Instant instant(String isoDateTime) { + return LocalDateTime.parse(isoDateTime, DATE_TIME).toInstant(ZoneOffset.UTC); + } + + private void assertUtcHour(String label, double expected, double actual) { + if (Double.isNaN(expected)) { + assertTrue(label + " should have no event (NaN)", Double.isNaN(actual)); + } else { + assertEquals(label, expected, actual, DELTA); + } + } + + @Test + public void getUTCSunrise() { + Object[][] cases = { + {"NJ Standard Date", "2017-10-17", "NJ", 11.13540831}, + {"LA Standard Date", "2017-10-17", "LA", 14.00704301}, + {"Jerusalem Hist. Date", "1955-02-26", "JERUSALEM", 4.11872483}, + {"Norway Polar Solstice", "2017-06-21", "NORWAY", NO_EVENT}, + {"Sydney Leap Day", "2020-02-29", "SYDNEY", 19.68246214}, + {"Macapa Equator New Year", "2000-01-01", "MACAPA", 9.39008078}, + {"Suva Fiji Solstice", "2023-06-21", "SUVA", 18.60488873}, + {"Ushuaia Southern Winter", "2017-06-21", "USHUAIA", 12.95743787}, + {"NJ Future DeltaT", "2100-07-04", "NJ", 9.44903860}, + }; + for (Object[] c : cases) { + double actual = calculator().getUTCSunrise(date((String) c[1]), geo((String) c[2]), ZENITH, true); + assertUtcHour((String) c[0], (double) c[3], actual); + } + } + + @Test + public void getUTCSunset() { + Object[][] cases = { + {"NJ Standard Date", "2017-10-17", "NJ", 22.24081501}, + {"LA Standard Date", "2017-10-17", "LA", 1.31823827}, + {"Jerusalem Hist. Date", "1955-02-26", "JERUSALEM", 15.64544001}, + {"Norway Polar Solstice", "2017-06-21", "NORWAY", NO_EVENT}, + {"Sydney Leap Day", "2020-02-29", "SYDNEY", 8.56373515}, + {"Macapa Equator New Year", "2000-01-01", "MACAPA", 21.53146217}, + {"Suva Fiji Solstice", "2023-06-21", "SUVA", 5.65781259}, + {"Ushuaia Southern Winter", "2017-06-21", "USHUAIA", 20.21256620}, + {"NJ Future DeltaT", "2100-07-04", "NJ", 0.58065990}, + }; + for (Object[] c : cases) { + double actual = calculator().getUTCSunset(date((String) c[1]), geo((String) c[2]), ZENITH, true); + assertUtcHour((String) c[0], (double) c[3], actual); + } + } + + @Test + public void getUTCNoon() { + Object[][] cases = { + {"NJ Standard Date", "2017-10-17", "NJ", 16.69315478}, + {"LA Standard Date", "2017-10-17", "LA", 19.66659743}, + {"Jerusalem Hist. Date", "1955-02-26", "JERUSALEM", 9.87832210}, + {"Norway Polar Solstice", "2017-06-21", "NORWAY", 11.42090460}, + {"Sydney Leap Day", "2020-02-29", "SYDNEY", 2.12779094}, + {"Macapa Equator New Year", "2000-01-01", "MACAPA", 15.46078241}, + {"Suva Fiji Solstice", "2023-06-21", "SUVA", 0.13137581}, + {"Ushuaia Southern Winter", "2017-06-21", "USHUAIA", 16.58492880}, + {"NJ Future DeltaT", "2100-07-04", "NJ", 17.01703523}, + }; + for (Object[] c : cases) { + double actual = calculator().getUTCNoon(date((String) c[1]), geo((String) c[2])); + assertUtcHour((String) c[0], (double) c[3], actual); + } + } + + @Test + public void getUTCMidnight() { + Object[][] cases = { + {"NJ Standard Date", "2017-10-17", "NJ", 4.69150247}, + {"LA Standard Date", "2017-10-17", "LA", 7.66495567}, + {"Jerusalem Hist. Date", "1955-02-26", "JERUSALEM", 21.87692914}, + {"Norway Polar Solstice", "2017-06-21", "NORWAY", 23.42271259}, + {"Sydney Leap Day", "2020-02-29", "SYDNEY", 14.12619634}, + {"Macapa Equator New Year", "2000-01-01", "MACAPA", 3.46470789}, + {"Suva Fiji Solstice", "2023-06-21", "SUVA", 12.13318970}, + {"Ushuaia Southern Winter", "2017-06-21", "USHUAIA", 4.58673539}, + {"NJ Future DeltaT", "2100-07-04", "NJ", 5.01848806}, + }; + for (Object[] c : cases) { + double actual = calculator().getUTCMidnight(date((String) c[1]), geo((String) c[2])); + assertUtcHour((String) c[0], (double) c[3], actual); + } + } + + @Test + public void getTimeAtAzimuth() { + Object[][] cases = { + {"NJ East", "2017-10-17", "NJ", 90.0, 9.96599151}, + {"NJ West", "2017-10-17", "NJ", 270.0, 23.43667951}, + {"LA East", "2017-10-17", "LA", 90.0, 12.71769344}, + {"LA West", "2017-10-17", "LA", 270.0, 2.63764230}, + {"Sydney East", "2020-02-29", "SYDNEY", 90.0, 20.92605878}, + {"Sydney West", "2020-02-29", "SYDNEY", 270.0, 7.34651923}, + {"Macapa East No Cross", "2000-01-01", "MACAPA", 90.0, NO_EVENT}, + {"Polar East", "2017-06-21", "NORWAY", 90.0, 6.02028371}, + }; + for (Object[] c : cases) { + double actual = calculator().getTimeAtAzimuth(date((String) c[1]), geo((String) c[2]), (double) c[3]); + assertUtcHour((String) c[0], (double) c[4], actual); + } + } + + @Test + public void getSolarElevation() { + Object[][] cases = { + {"NJ Noon", "2017-10-17 12:00:00", "NJ", 8.24496867}, + {"NJ Evening", "2017-10-17 18:30:00", "NJ", 33.56992906}, + {"LA Evening", "2017-10-17 20:00:00", "LA", 46.19384884}, + {"Polar Midnight", "2017-06-21 00:00:00", "NORWAY", 3.98724677}, + {"Sydney Morning", "2020-02-29 20:00:00", "SYDNEY", 2.89748529}, + {"Macapa Local Noon", "2000-01-01 15:00:00", "MACAPA", 65.99072692}, + {"Suva Afternoon", "2023-06-21 02:00:00", "SUVA", 40.24513182}, + }; + for (Object[] c : cases) { + double actual = calculator().getSolarElevation(instant((String) c[1]), geo((String) c[2])); + assertEquals((String) c[0], (double) c[3], actual, DELTA); + } + } + + @Test + public void getSolarAzimuth() { + Object[][] cases = { + {"NJ Noon", "2017-10-17 12:00:00", "NJ", 110.14145579}, + {"NJ Evening", "2017-10-17 18:30:00", "NJ", 212.62717267}, + {"LA Evening", "2017-10-17 20:00:00", "LA", 187.13298966}, + {"Polar Midnight", "2017-06-21 00:00:00", "NORWAY", 8.00635444}, + {"Sydney Morning", "2020-02-29 20:00:00", "SYDNEY", 97.32262775}, + {"Macapa Local Noon", "2000-01-01 15:00:00", "MACAPA", 164.21440268}, + {"Suva Afternoon", "2023-06-21 02:00:00", "SUVA", 325.62368833}, + }; + for (Object[] c : cases) { + double actual = calculator().getSolarAzimuth(instant((String) c[1]), geo((String) c[2])); + assertEquals((String) c[0], (double) c[3], actual, DELTA); + } + } + + @Test + public void getUTCSunriseAtZenith() { + Object[][] cases = { + {"NJ Civil", "2017-10-17", "NJ", 96.0, 10.70891556}, + {"NJ Nautical", "2017-10-17", "NJ", 102.0, 10.17562194}, + {"NJ Astronomical", "2017-10-17", "NJ", 108.0, 9.64406486}, + {"Sydney Civil", "2020-02-29", "SYDNEY", 96.0, 19.27827196}, + }; + for (Object[] c : cases) { + double actual = calculator().getUTCSunrise(date((String) c[1]), geo((String) c[2]), (double) c[3], true); + assertUtcHour((String) c[0], (double) c[4], actual); + } + } + + @Test + public void getUTCSunriseWithoutElevationAdjustment() { + Object[][] cases = { + {"NJ No Elev Adj", "2017-10-17", "NJ", 11.17291440}, + {"Jerusalem No Elev Adj", "1955-02-26", "JERUSALEM", 4.18884065}, + }; + for (Object[] c : cases) { + double actual = calculator().getUTCSunrise(date((String) c[1]), geo((String) c[2]), ZENITH, false); + assertUtcHour((String) c[0], (double) c[3], actual); + } + } + + @Test(expected = IllegalArgumentException.class) + public void getTimeAtAzimuthRejectsUnsupportedAzimuth() { + calculator().getTimeAtAzimuth(date("2017-10-17"), geo("NJ"), 123.0); + } + + @Test + public void getCalculatorName() { + assertEquals("US National Oceanic and Atmospheric Administration Algorithm", calculator().getCalculatorName()); + } +} diff --git a/src/test/java/com/kosherjava/zmanim/util/SPACalculatorTest.java b/src/test/java/com/kosherjava/zmanim/util/SPACalculatorTest.java new file mode 100644 index 00000000..18cc1357 --- /dev/null +++ b/src/test/java/com/kosherjava/zmanim/util/SPACalculatorTest.java @@ -0,0 +1,311 @@ +/* + * Zmanim Java API + * Copyright © 2004-2026 Eliyahu Hershfeld + * + * This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General + * Public License as published by the Free Software Foundation; version 2.1 of the License. + * + * This library is distributed in the hope that it will be useful,but WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA, + * or connect to: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html + */ +package com.kosherjava.zmanim.util; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; + +import org.junit.Test; + +import com.kosherjava.zmanim.TestLocations; + +/** + * Coverage for {@link SPACalculator}, the NREL Solar Position Algorithm engine. Most cases are regression against the + * library's own current output (8-decimal delta), but this suite also includes a Tier-A independently verifiable + * anchor: the published NREL reference case (Reda & Andreas, NREL/TP-560-34302), plus coverage of the + * pressure/temperature/ΔT configuration knobs. + * + * @author Test coverage + */ +public class SPACalculatorTest { + + private static final double DELTA = 1e-8; + private static final double ZENITH = 90.0; + private static final double NO_EVENT = Double.NaN; + + private static final DateTimeFormatter DATE_TIME = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + private SPACalculator calculator() { + return new SPACalculator(); + } + + private GeoLocation geo(String location) { + switch (location) { + case "NJ": return TestLocations.nj(TestLocations.UTC); + case "LA": return TestLocations.la(TestLocations.UTC); + case "JERUSALEM": return TestLocations.jerusalem(TestLocations.UTC); + case "NORWAY": return TestLocations.norway(TestLocations.UTC); + case "SYDNEY": return TestLocations.sydney(TestLocations.UTC); + case "MACAPA": return TestLocations.macapa(TestLocations.UTC); + case "SUVA": return TestLocations.suva(TestLocations.UTC); + case "USHUAIA": return TestLocations.ushuaia(TestLocations.UTC); + default: throw new IllegalArgumentException("Unknown location: " + location); + } + } + + private LocalDate date(String iso) { + return LocalDate.parse(iso); + } + + private Instant instant(String isoDateTime) { + return LocalDateTime.parse(isoDateTime, DATE_TIME).toInstant(ZoneOffset.UTC); + } + + private void assertUtcHour(String label, double expected, double actual) { + if (Double.isNaN(expected)) { + assertTrue(label + " should have no event (NaN)", Double.isNaN(actual)); + } else { + assertEquals(label, expected, actual, DELTA); + } + } + + @Test + public void getUTCSunrise() { + Object[][] cases = { + {"NJ Standard Date", "2017-10-17", "NJ", 11.13593689}, + {"LA Standard Date", "2017-10-17", "LA", 14.00756754}, + {"Jerusalem Hist. Date", "1955-02-26", "JERUSALEM", 4.11873932}, + {"Norway Polar Solstice", "2017-06-21", "NORWAY", NO_EVENT}, + {"Sydney Leap Day", "2020-02-29", "SYDNEY", 19.68293143}, + {"Macapa Equator New Year", "2000-01-01", "MACAPA", 9.38998685}, + {"Suva Fiji Solstice", "2023-06-21", "SUVA", 18.60534965}, + {"Ushuaia Southern Winter", "2017-06-21", "USHUAIA", 12.95741199}, + {"NJ Future DeltaT", "2100-07-04", "NJ", 9.44912615}, + }; + for (Object[] c : cases) { + double actual = calculator().getUTCSunrise(date((String) c[1]), geo((String) c[2]), ZENITH, true); + assertUtcHour((String) c[0], (double) c[3], actual); + } + } + + @Test + public void getUTCSunset() { + Object[][] cases = { + {"NJ Standard Date", "2017-10-17", "NJ", 22.24099415}, + {"LA Standard Date", "2017-10-17", "LA", 1.31843857}, + {"Jerusalem Hist. Date", "1955-02-26", "JERUSALEM", 15.64512300}, + {"Norway Polar Solstice", "2017-06-21", "NORWAY", NO_EVENT}, + {"Sydney Leap Day", "2020-02-29", "SYDNEY", 8.56367402}, + {"Macapa Equator New Year", "2000-01-01", "MACAPA", 21.53102552}, + {"Suva Fiji Solstice", "2023-06-21", "SUVA", 5.65791427}, + {"Ushuaia Southern Winter", "2017-06-21", "USHUAIA", 20.21191830}, + {"NJ Future DeltaT", "2100-07-04", "NJ", 0.58028674}, + }; + for (Object[] c : cases) { + double actual = calculator().getUTCSunset(date((String) c[1]), geo((String) c[2]), ZENITH, true); + assertUtcHour((String) c[0], (double) c[3], actual); + } + } + + @Test + public void getUTCNoon() { + Object[][] cases = { + {"NJ Standard Date", "2017-10-17", "NJ", 16.69350885}, + {"LA Standard Date", "2017-10-17", "LA", 19.66696009}, + {"Jerusalem Hist. Date", "1955-02-26", "JERUSALEM", 9.87817106}, + {"Norway Polar Solstice", "2017-06-21", "NORWAY", 11.42056214}, + {"Sydney Leap Day", "2020-02-29", "SYDNEY", 2.12799525}, + {"Macapa Equator New Year", "2000-01-01", "MACAPA", 15.46051831}, + {"Suva Fiji Solstice", "2023-06-21", "SUVA", 0.13165740}, + {"Ushuaia Southern Winter", "2017-06-21", "USHUAIA", 16.58459266}, + {"NJ Future DeltaT", "2100-07-04", "NJ", 17.01689317}, + }; + for (Object[] c : cases) { + double actual = calculator().getUTCNoon(date((String) c[1]), geo((String) c[2])); + assertUtcHour((String) c[0], (double) c[3], actual); + } + } + + @Test + public void getUTCMidnight() { + Object[][] cases = { + {"NJ Standard Date", "2017-10-17", "NJ", 4.69189152}, + {"LA Standard Date", "2017-10-17", "LA", 7.66535335}, + {"Jerusalem Hist. Date", "1955-02-26", "JERUSALEM", 21.87680310}, + {"Norway Polar Solstice", "2017-06-21", "NORWAY", 23.42238534}, + {"Sydney Leap Day", "2020-02-29", "SYDNEY", 14.12642351}, + {"Macapa Equator New Year", "2000-01-01", "MACAPA", 3.46446119}, + {"Suva Fiji Solstice", "2023-06-21", "SUVA", 12.13348552}, + {"Ushuaia Southern Winter", "2017-06-21", "USHUAIA", 4.58641480}, + {"NJ Future DeltaT", "2100-07-04", "NJ", 5.01835716}, + }; + for (Object[] c : cases) { + double actual = calculator().getUTCMidnight(date((String) c[1]), geo((String) c[2])); + assertUtcHour((String) c[0], (double) c[3], actual); + } + } + + @Test + public void getTimeAtAzimuth() { + Object[][] cases = { + {"NJ East", "2017-10-17", "NJ", 90.0, 9.96630382}, + {"NJ West", "2017-10-17", "NJ", 270.0, 23.43696229}, + {"LA East", "2017-10-17", "LA", 90.0, 12.71802376}, + {"LA West", "2017-10-17", "LA", 270.0, 2.63792595}, + {"Sydney East", "2020-02-29", "SYDNEY", 90.0, 20.92601636}, + {"Sydney West", "2020-02-29", "SYDNEY", 270.0, 7.34685680}, + {"Macapa East No Cross", "2000-01-01", "MACAPA", 90.0, NO_EVENT}, + {"Polar East", "2017-06-21", "NORWAY", 90.0, 6.01986373}, + }; + for (Object[] c : cases) { + double actual = calculator().getTimeAtAzimuth(date((String) c[1]), geo((String) c[2]), (double) c[3]); + assertUtcHour((String) c[0], (double) c[4], actual); + } + } + + @Test + public void getSolarElevation() { + Object[][] cases = { + {"NJ Noon", "2017-10-17 12:00:00", "NJ", 8.24142367}, + {"NJ Evening", "2017-10-17 18:30:00", "NJ", 33.57165658}, + {"LA Evening", "2017-10-17 20:00:00", "LA", 46.19391692}, + {"Polar Midnight", "2017-06-21 00:00:00", "NORWAY", 3.98549481}, + {"Sydney Morning", "2020-02-29 20:00:00", "SYDNEY", 2.89087872}, + {"Macapa Local Noon", "2000-01-01 15:00:00", "MACAPA", 65.99118480}, + {"Suva Afternoon", "2023-06-21 02:00:00", "SUVA", 40.24649656}, + }; + for (Object[] c : cases) { + double actual = calculator().getSolarElevation(instant((String) c[1]), geo((String) c[2])); + assertEquals((String) c[0], (double) c[3], actual, DELTA); + } + } + + @Test + public void getSolarAzimuth() { + Object[][] cases = { + {"NJ Noon", "2017-10-17 12:00:00", "NJ", 110.13757333}, + {"NJ Evening", "2017-10-17 18:30:00", "NJ", 212.62157928}, + {"LA Evening", "2017-10-17 20:00:00", "LA", 187.12533185}, + {"Polar Midnight", "2017-06-21 00:00:00", "NORWAY", 8.01127958}, + {"Sydney Morning", "2020-02-29 20:00:00", "SYDNEY", 97.32308776}, + {"Macapa Local Noon", "2000-01-01 15:00:00", "MACAPA", 164.22300003}, + {"Suva Afternoon", "2023-06-21 02:00:00", "SUVA", 325.62786587}, + }; + for (Object[] c : cases) { + double actual = calculator().getSolarAzimuth(instant((String) c[1]), geo((String) c[2])); + assertEquals((String) c[0], (double) c[3], actual, DELTA); + } + } + + @Test + public void getUTCSunriseAtZenith() { + Object[][] cases = { + {"NJ Civil", "2017-10-17", "NJ", 96.0, 10.70943974}, + {"NJ Nautical", "2017-10-17", "NJ", 102.0, 10.17613961}, + {"NJ Astronomical", "2017-10-17", "NJ", 108.0, 9.64457480}, + {"Sydney Civil", "2020-02-29", "SYDNEY", 96.0, 19.27874463}, + }; + for (Object[] c : cases) { + double actual = calculator().getUTCSunrise(date((String) c[1]), geo((String) c[2]), (double) c[3], true); + assertUtcHour((String) c[0], (double) c[4], actual); + } + } + + @Test + public void getUTCSunriseWithoutElevationAdjustment() { + Object[][] cases = { + {"NJ No Elev Adj", "2017-10-17", "NJ", 11.17344334}, + {"Jerusalem No Elev Adj", "1955-02-26", "JERUSALEM", 4.18885558}, + }; + for (Object[] c : cases) { + double actual = calculator().getUTCSunrise(date((String) c[1]), geo((String) c[2]), ZENITH, false); + assertUtcHour((String) c[0], (double) c[3], actual); + } + } + + @Test + public void getUTCSunriseWithDeltaTDisabled() { + Object[][] cases = { + {"NJ dT off", "2017-10-17", "NJ", 11.13586818}, + {"Jerusalem dT off", "1955-02-26", "JERUSALEM", 4.11871824}, + }; + for (Object[] c : cases) { + SPACalculator calc = calculator(); + calc.setApplyDeltaT(false); + double actual = calc.getUTCSunrise(date((String) c[1]), geo((String) c[2]), ZENITH, true); + assertUtcHour((String) c[0], (double) c[3], actual); + } + } + + @Test(expected = IllegalArgumentException.class) + public void getTimeAtAzimuthRejectsUnsupportedAzimuth() { + calculator().getTimeAtAzimuth(date("2017-10-17"), geo("NJ"), 123.0); + } + + /* + * Tier-A independently verifiable reference: the published NREL Solar Position Algorithm example + * (Reda & Andreas, NREL/TP-560-34302). 2003-10-17 12:30:30 local (tz -7 => 19:30:30 UTC), + * longitude -105.1786, latitude 39.742476, elevation 1830.14 m, pressure 820 mb, temperature 11 C, + * delta-T 67 s. The paper reports topocentric elevation 39.888378 and azimuth 194.340241. + */ + private SPACalculator nrelCalculator() { + SPACalculator calc = new SPACalculator(); + calc.setDeltaTOverride(67.0); + calc.setPressure(820); + calc.setTemperature(11); + return calc; + } + + private Instant nrelInstant() { + return instant("2003-10-17 19:30:30"); + } + + private GeoLocation nrelLocation() { + return new GeoLocation("NREL Reference", 39.742476, -105.1786, 1830.14, TestLocations.UTC); + } + + @Test + public void getSolarElevationNrelReference() { + double elevation = nrelCalculator().getSolarElevation(nrelInstant(), nrelLocation()); + assertEquals("NREL published topocentric elevation", 39.888378, elevation, 0.001); + } + + @Test + public void getSolarAzimuthNrelReference() { + double azimuth = nrelCalculator().getSolarAzimuth(nrelInstant(), nrelLocation()); + assertEquals("NREL published topocentric azimuth", 194.340241, azimuth, 0.001); + } + + @Test + public void defaultsAndSetters() { + SPACalculator calc = new SPACalculator(); + assertTrue(calc.isApplyDeltaT()); + assertNull(calc.getDeltaTOverride()); + assertEquals(1013.25, calc.getPressure(), 0); + assertEquals(10.0, calc.getTemperature(), 0); + + calc.setDeltaTOverride(67.0); + calc.setPressure(820); + calc.setTemperature(11); + calc.setApplyDeltaT(false); + + assertEquals(Double.valueOf(67.0), calc.getDeltaTOverride()); + assertEquals(820.0, calc.getPressure(), 0); + assertEquals(11.0, calc.getTemperature(), 0); + assertTrue(!calc.isApplyDeltaT()); + } + + @Test + public void getCalculatorName() { + assertEquals("NREL Solar Position Algorithm", calculator().getCalculatorName()); + } +} diff --git a/src/test/java/com/kosherjava/zmanim/util/SunTimesCalculatorTest.java b/src/test/java/com/kosherjava/zmanim/util/SunTimesCalculatorTest.java new file mode 100644 index 00000000..97f92e4d --- /dev/null +++ b/src/test/java/com/kosherjava/zmanim/util/SunTimesCalculatorTest.java @@ -0,0 +1,192 @@ +/* + * Zmanim Java API + * Copyright © 2004-2026 Eliyahu Hershfeld + * + * This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General + * Public License as published by the Free Software Foundation; version 2.1 of the License. + * + * This library is distributed in the hope that it will be useful,but WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA, + * or connect to: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html + */ +package com.kosherjava.zmanim.util; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.time.Instant; +import java.time.LocalDate; + +import org.junit.Test; + +import com.kosherjava.zmanim.TestLocations; + +/** + * Regression coverage for {@link SunTimesCalculator} (the older USNO / "Almanac for Computers" algorithm). Expected + * values are the library's own current output asserted to 8 decimals. Unlike {@link NOAACalculator}, this calculator + * returns {@link Double#NaN} for solar noon/midnight at the polar location and does not implement solar + * elevation/azimuth or time-at-azimuth (those throw {@link UnsupportedOperationException}). + * + * @author Test coverage + */ +public class SunTimesCalculatorTest { + + private static final double DELTA = 1e-8; + private static final double ZENITH = 90.0; + private static final double NO_EVENT = Double.NaN; + + private SunTimesCalculator calculator() { + return new SunTimesCalculator(); + } + + private GeoLocation geo(String location) { + switch (location) { + case "NJ": return TestLocations.nj(TestLocations.UTC); + case "LA": return TestLocations.la(TestLocations.UTC); + case "JERUSALEM": return TestLocations.jerusalem(TestLocations.UTC); + case "NORWAY": return TestLocations.norway(TestLocations.UTC); + case "SYDNEY": return TestLocations.sydney(TestLocations.UTC); + case "MACAPA": return TestLocations.macapa(TestLocations.UTC); + case "SUVA": return TestLocations.suva(TestLocations.UTC); + case "USHUAIA": return TestLocations.ushuaia(TestLocations.UTC); + default: throw new IllegalArgumentException("Unknown location: " + location); + } + } + + private LocalDate date(String iso) { + return LocalDate.parse(iso); + } + + private void assertUtcHour(String label, double expected, double actual) { + if (Double.isNaN(expected)) { + assertTrue(label + " should have no event (NaN)", Double.isNaN(actual)); + } else { + assertEquals(label, expected, actual, DELTA); + } + } + + @Test + public void getUTCSunrise() { + Object[][] cases = { + {"NJ Standard Date", "2017-10-17", "NJ", 11.12643745}, + {"LA Standard Date", "2017-10-17", "LA", 14.00076197}, + {"Jerusalem Hist. Date", "1955-02-26", "JERUSALEM", 4.11037446}, + {"Norway Polar Solstice", "2017-06-21", "NORWAY", NO_EVENT}, + {"Sydney Leap Day", "2020-02-29", "SYDNEY", 19.68724563}, + {"Macapa Equator New Year", "2000-01-01", "MACAPA", 9.39464043}, + {"Suva Fiji Solstice", "2023-06-21", "SUVA", 18.60351329}, + {"Ushuaia Southern Winter", "2017-06-21", "USHUAIA", 12.95527929}, + {"NJ Future DeltaT", "2100-07-04", "NJ", 9.43972880}, + }; + for (Object[] c : cases) { + double actual = calculator().getUTCSunrise(date((String) c[1]), geo((String) c[2]), ZENITH, true); + assertUtcHour((String) c[0], (double) c[3], actual); + } + } + + @Test + public void getUTCSunset() { + Object[][] cases = { + {"NJ Standard Date", "2017-10-17", "NJ", 22.25383390}, + {"LA Standard Date", "2017-10-17", "LA", 1.32889810}, + {"Jerusalem Hist. Date", "1955-02-26", "JERUSALEM", 15.65101844}, + {"Norway Polar Solstice", "2017-06-21", "NORWAY", NO_EVENT}, + {"Sydney Leap Day", "2020-02-29", "SYDNEY", 8.56114598}, + {"Macapa Equator New Year", "2000-01-01", "MACAPA", 21.53593703}, + {"Suva Fiji Solstice", "2023-06-21", "SUVA", 5.65641338}, + {"Ushuaia Southern Winter", "2017-06-21", "USHUAIA", 20.20896383}, + {"NJ Future DeltaT", "2100-07-04", "NJ", 0.57971462}, + }; + for (Object[] c : cases) { + double actual = calculator().getUTCSunset(date((String) c[1]), geo((String) c[2]), ZENITH, true); + assertUtcHour((String) c[0], (double) c[3], actual); + } + } + + @Test + public void getUTCNoon() { + Object[][] cases = { + {"NJ Standard Date", "2017-10-17", "NJ", 16.69011947}, + {"LA Standard Date", "2017-10-17", "LA", 19.66482200}, + {"Jerusalem Hist. Date", "1955-02-26", "JERUSALEM", 9.88071908}, + {"Norway Polar No Noon", "2017-06-21", "NORWAY", NO_EVENT}, + {"Sydney Leap Day", "2020-02-29", "SYDNEY", 2.12420286}, + {"Macapa Equator New Year", "2000-01-01", "MACAPA", 15.46529012}, + {"Suva Fiji Solstice", "2023-06-21", "SUVA", 0.12996327}, + {"Ushuaia Southern Winter", "2017-06-21", "USHUAIA", 16.58212152}, + {"NJ Future DeltaT", "2100-07-04", "NJ", 17.00973685}, + }; + for (Object[] c : cases) { + double actual = calculator().getUTCNoon(date((String) c[1]), geo((String) c[2])); + assertUtcHour((String) c[0], (double) c[3], actual); + } + } + + @Test + public void getUTCMidnight() { + Object[][] cases = { + {"NJ Standard Date", "2017-10-17", "NJ", 4.69011947}, + {"LA Standard Date", "2017-10-17", "LA", 7.66482200}, + {"Jerusalem Hist. Date", "1955-02-26", "JERUSALEM", 21.88071908}, + {"Norway Polar No Midnight", "2017-06-21", "NORWAY", NO_EVENT}, + {"Sydney Leap Day", "2020-02-29", "SYDNEY", 14.12420286}, + {"Macapa Equator New Year", "2000-01-01", "MACAPA", 3.46529012}, + {"Suva Fiji Solstice", "2023-06-21", "SUVA", 12.12996327}, + {"Ushuaia Southern Winter", "2017-06-21", "USHUAIA", 4.58212152}, + {"NJ Future DeltaT", "2100-07-04", "NJ", 5.00973685}, + }; + for (Object[] c : cases) { + double actual = calculator().getUTCMidnight(date((String) c[1]), geo((String) c[2])); + assertUtcHour((String) c[0], (double) c[3], actual); + } + } + + @Test + public void getUTCSunriseAtZenith() { + Object[][] cases = { + {"NJ Civil", "2017-10-17", "NJ", 96.0, 10.70056986}, + {"NJ Nautical", "2017-10-17", "NJ", 102.0, 10.16784691}, + {"NJ Astronomical", "2017-10-17", "NJ", 108.0, 9.63665090}, + {"Sydney Civil", "2020-02-29", "SYDNEY", 96.0, 19.28346936}, + }; + for (Object[] c : cases) { + double actual = calculator().getUTCSunrise(date((String) c[1]), geo((String) c[2]), (double) c[3], true); + assertUtcHour((String) c[0], (double) c[4], actual); + } + } + + @Test + public void getUTCSunriseWithoutElevationAdjustment() { + Object[][] cases = { + {"NJ No Elev Adj", "2017-10-17", "NJ", 11.16388056}, + {"Jerusalem No Elev Adj", "1955-02-26", "JERUSALEM", 4.18050398}, + }; + for (Object[] c : cases) { + double actual = calculator().getUTCSunrise(date((String) c[1]), geo((String) c[2]), ZENITH, false); + assertUtcHour((String) c[0], (double) c[3], actual); + } + } + + @Test(expected = UnsupportedOperationException.class) + public void getTimeAtAzimuthUnsupported() { + calculator().getTimeAtAzimuth(date("2017-10-17"), geo("NJ"), 90.0); + } + + @Test(expected = UnsupportedOperationException.class) + public void getSolarElevationUnsupported() { + calculator().getSolarElevation(Instant.parse("2017-10-17T12:00:00Z"), geo("NJ")); + } + + @Test(expected = UnsupportedOperationException.class) + public void getSolarAzimuthUnsupported() { + calculator().getSolarAzimuth(Instant.parse("2017-10-17T12:00:00Z"), geo("NJ")); + } + + @Test + public void getCalculatorName() { + assertEquals("US Naval Almanac Algorithm", calculator().getCalculatorName()); + } +} From dc760276a8255b37a8f64035afe3568d3291286c Mon Sep 17 00:00:00 2001 From: Zachary Weixelbaum Date: Wed, 15 Jul 2026 14:18:32 -0400 Subject: [PATCH 2/2] update PR workflow --- .github/workflows/pull_request_worklow.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pull_request_worklow.yml b/.github/workflows/pull_request_worklow.yml index ed9af0bd..68659bff 100644 --- a/.github/workflows/pull_request_worklow.yml +++ b/.github/workflows/pull_request_worklow.yml @@ -10,8 +10,9 @@ jobs: - name: Set up JDK uses: actions/setup-java@v5 with: - distribution: 'adopt' + distribution: 'temurin' java-version: 11 + cache: 'maven' - name: Run tests - run: mvn test + run: ./mvnw test