diff --git a/src/main/java/com/kosherjava/zmanim/AstronomicalCalendar.java b/src/main/java/com/kosherjava/zmanim/AstronomicalCalendar.java index dddb1178..56f6b2bc 100644 --- a/src/main/java/com/kosherjava/zmanim/AstronomicalCalendar.java +++ b/src/main/java/com/kosherjava/zmanim/AstronomicalCalendar.java @@ -553,10 +553,31 @@ protected Instant getInstantFromTime(double time, SolarEvent solarEvent) { LocalDate date = getAdjustedLocalDate(); double localTimeHours = (getGeoLocation().getLongitude() / 15) + time; + // The calculator gives only the UTC time of day, a number from 0 to 24. It doesn't say which UTC day the event + // is on, so start from the calculation date and correct it. + // + // localTimeHours is that time in local mean time, which is UTC plus the longitude's hours. It can come out below 0 or + // above 24, which means the event is really on the day before or the day after the date. Each event has a + // window of local mean time that belongs to the date, and these checks move the date by a day when the event + // falls outside it. + // + // - Sunrise belongs to the date from 18:00 the evening before through 18:00 of the date. After 18:00 it is + // really the evening before (an Arctic sunrise just after midnight, say), so go back a day. At -6:00 or + // earlier the UTC time wrapped around midnight, so go forward a day. For example, just west of the date line + // local mean time is about 12 hours behind UTC. A sunrise at noon local mean time is then 24:00 UTC, which + // arrives as 0:00 and reads as -12:00 local mean time. + // - Sunset belongs from 06:00 of the date through 06:00 the next morning. Before 06:00 go forward a day. At + // 30:00, which is 06:00 the next morning, or later the UTC time wrapped the other way, so go back a day. + // - Solar midnight is the one after the date's noon, so one before 12:00 moves forward a day. + // - Solar noon belongs between 0:00 and 24:00. Before 0:00 go forward a day, and after 24:00 go back a day. if (solarEvent == SolarEvent.SUNRISE && localTimeHours > 18) { date = date.minusDays(1); + } else if (solarEvent == SolarEvent.SUNRISE && localTimeHours <= -6) { + date = date.plusDays(1); } else if (solarEvent == SolarEvent.SUNSET && localTimeHours < 6) { date = date.plusDays(1); + } else if (solarEvent == SolarEvent.SUNSET && localTimeHours >= 30) { + date = date.minusDays(1); } else if (solarEvent == SolarEvent.MIDNIGHT && localTimeHours < 12) { date = date.plusDays(1); } else if (solarEvent == SolarEvent.NOON) { diff --git a/src/test/java/com/kosherjava/zmanim/AstronomicalCalendarTest.java b/src/test/java/com/kosherjava/zmanim/AstronomicalCalendarTest.java index 29bcafc1..8f1faba6 100644 --- a/src/test/java/com/kosherjava/zmanim/AstronomicalCalendarTest.java +++ b/src/test/java/com/kosherjava/zmanim/AstronomicalCalendarTest.java @@ -17,13 +17,23 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; import java.time.Duration; import java.time.Instant; import java.time.LocalDate; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.function.Function; import org.junit.Test; +import com.kosherjava.zmanim.util.AstronomicalCalculator; +import com.kosherjava.zmanim.util.GeoLocation; +import com.kosherjava.zmanim.util.MeeusCalculator; +import com.kosherjava.zmanim.util.NOAACalculator; +import com.kosherjava.zmanim.util.SPACalculator; import com.kosherjava.zmanim.util.SunTimesCalculator; /** @@ -118,4 +128,84 @@ public void swappingCalculatorChangesResult() { assertNotEquals(noaaSunset, sunTimesSunset); } + + /** + * Near the date line a sunrise close to local noon has a UTC time just past 24:00. It must still fall on its own + * date: Cape Schmidt, Chukotka, on 10 January 2021, where the sun is up for about ten minutes around noon. + */ + @Test + public void polarSunriseNearTheDateLineIsOnItsOwnDate() { + ZoneId anadyr = ZoneId.of("Asia/Anadyr"); + LocalDate date = LocalDate.of(2021, 1, 10); + AstronomicalCalculator[] calculators = { new NOAACalculator(), new MeeusCalculator(), new SPACalculator() }; + for (AstronomicalCalculator calculator : calculators) { + AstronomicalCalendar calendar = new AstronomicalCalendar(new GeoLocation("Cape Schmidt", 68.88, -179.37, anadyr)); + calendar.setLocalDate(date); + calendar.setAstronomicalCalculator(calculator); + ZonedDateTime sunrise = calendar.getSeaLevelSunrise().atZone(anadyr); + ZonedDateTime sunset = calendar.getSeaLevelSunset().atZone(anadyr); + assertEquals(calculator.getCalculatorName(), date, sunrise.toLocalDate()); + assertTrue(calculator.getCalculatorName(), sunrise.isBefore(sunset)); + } + } + + /** + * Asserts that {@code event} at the given place, in a zone a whole number of {@code zoneHours} from UTC, comes out + * within a second of {@code expectedMillis}. A result on the wrong day is 24 hours off. + */ + private void assertEventNear(String label, double latitude, double longitude, int zoneHours, LocalDate date, + Function event, long expectedMillis) { + AstronomicalCalendar calendar = new AstronomicalCalendar( + new GeoLocation(label, latitude, longitude, ZoneOffset.ofHours(zoneHours))); + calendar.setLocalDate(date); + Instant actual = event.apply(calendar); + Instant expected = Instant.ofEpochMilli(expectedMillis); + assertTrue(label + ": " + actual + " is not near " + expected, + Duration.between(expected, actual).abs().compareTo(Duration.ofSeconds(1)) < 0); + } + + // One test for each date adjustment in getInstantFromTime. polarSunriseNearTheDateLineIsOnItsOwnDate covers a + // sunrise at -6 hours local mean time or earlier. + + /** A sunrise just after local midnight, after 18:00 local mean time of the previous day, moves back a day. */ + @Test + public void sunriseAfterSixPmLocalMeanTimeMovesBackADay() { + assertEventNear("sunrise after 18:00", -70, 35.235, 2, LocalDate.of(2021, 1, 25), + AstronomicalCalendar::getSeaLevelSunrise, 1611527037358L); // 2021-01-25T00:23:57+02:00 + } + + /** A sunset just before local midnight, before 06:00 local mean time of the date, moves forward a day. */ + @Test + public void sunsetBeforeSixAmLocalMeanTimeMovesForwardADay() { + assertEventNear("sunset before 06:00", -70, -179.37, -12, LocalDate.of(2021, 1, 23), + AstronomicalCalendar::getSeaLevelSunset, 1611488915349L); // 2021-01-23T23:48:35-12:00 + } + + /** A sunset near local noon just east of the date line, at +30 hours local mean time or later, moves back a day. */ + @Test + public void sunsetAfterThirtyHoursLocalMeanTimeMovesBackADay() { + assertEventNear("sunset after 30:00", 68.88, 179, 12, LocalDate.of(2021, 12, 2), + AstronomicalCalendar::getSeaLevelSunset, 1638403159563L); // 2021-12-02T11:59:19+12:00 + } + + /** Solar midnight before noon local mean time moves forward a day. */ + @Test + public void solarMidnightBeforeNoonLocalMeanTimeMovesForwardADay() { + assertEventNear("midnight before 12:00", -70, -179.37, -12, LocalDate.of(2021, 1, 1), + AstronomicalCalendar::getSolarMidnight, 1609588897446L); // 2021-01-02T00:01:37-12:00 + } + + /** Solar noon before 0 hours local mean time moves forward a day. */ + @Test + public void solarNoonBeforeMidnightLocalMeanTimeMovesForwardADay() { + assertEventNear("noon before 00:00", -70, -179.37, -12, LocalDate.of(2021, 1, 1), + AstronomicalCalendar::getSunTransit, 1609545683526L); // 2021-01-01T12:01:23-12:00 + } + + /** Solar noon after 24 hours local mean time moves back a day. */ + @Test + public void solarNoonAfterTwentyFourHoursLocalMeanTimeMovesBackADay() { + assertEventNear("noon after 24:00", -70, 179, 12, LocalDate.of(2021, 9, 14), + AstronomicalCalendar::getSunTransit, 1631577581205L); // 2021-09-14T11:59:41+12:00 + } }