Skip to content

Commit

Permalink
feat: sanity checks on result records
Browse files Browse the repository at this point in the history
Also minor improvements of sunrise/sunset tests and docs.
  • Loading branch information
klausbrunner committed Jan 10, 2024
1 parent 1436679 commit 70db663
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 39 deletions.
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,13 +99,14 @@ See the Javadoc for more methods.
### Notes on sunrise, sunset, and twilight

* Calculation is based on the usual correction of 0.833° on the zenith angle, i.e. sunrise and sunset are assumed to
occur when the center of the solar disc is 50 arc-minutes below the horizon.
occur when the center of the solar disc is 50 arc-minutes below the horizon. While commonly used, this fixed value
fails to account for the varying effects of atmospheric refraction. Calculated and apparent sunrise and sunset times
may easily differ by several minutes (cf. [Wilson 2018](https://doi.org/10.37099/mtu.dc.etdr/697)).
* As a general note on accuracy, Jean Meeus advises that "giving rising or setting times .. more accurately than to the
nearest minute makes no sense" (_Astronomical Algorithms_). Errors increase the farther the position from the equator,
i.e. values for polar regions are much less reliable.
* The SPA sunset/sunrise algorithm is one of the most accurate ones around. Results of this implementation correspond
very closely to the [NOAA calculator](http://www.esrl.noaa.gov/gmd/grad/solcalc/)'s, with maximum differences of just a
few seconds even for polar regions.
very closely to the [NOAA calculator](http://www.esrl.noaa.gov/gmd/grad/solcalc/)'s.

### What's this "delta T" thing?

Expand Down
12 changes: 11 additions & 1 deletion src/main/java/net/e175/klaus/solarpositioning/SolarPosition.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,14 @@
* @param azimuth Azimuth angle in degrees, measured from North (0°) going eastwards.
* @param zenithAngle Zenith angle in degrees, measured from zenith (0°) downwards.
*/
public record SolarPosition(double azimuth, double zenithAngle) {}
public record SolarPosition(double azimuth, double zenithAngle) {
public SolarPosition {
if (azimuth < 0 || azimuth > 360) {
throw new IllegalArgumentException("illegal value %.3f for azimuth".formatted(azimuth));
}
if (zenithAngle < 0 || zenithAngle > 180) {
throw new IllegalArgumentException(
"illegal value %.3f for zenithAngle".formatted(zenithAngle));
}
}
}
21 changes: 18 additions & 3 deletions src/main/java/net/e175/klaus/solarpositioning/SunriseResult.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package net.e175.klaus.solarpositioning;

import java.time.ZonedDateTime;
import java.util.Objects;

/** Result types for sunrise/sunset calculations. */
public sealed interface SunriseResult {
Expand All @@ -15,19 +16,33 @@ public sealed interface SunriseResult {
* @param sunset Time of sunset.
*/
record RegularDay(ZonedDateTime sunrise, ZonedDateTime transit, ZonedDateTime sunset)
implements SunriseResult {}
implements SunriseResult {
public RegularDay {
Objects.requireNonNull(sunrise);
Objects.requireNonNull(transit);
Objects.requireNonNull(sunset);
}
}

/**
* A day on which the sun is above the horizon all the time (polar day).
*
* @param transit Time of transit (culmination), i.e. when the sun is closest to the zenith.
*/
record AllDay(ZonedDateTime transit) implements SunriseResult {}
record AllDay(ZonedDateTime transit) implements SunriseResult {
public AllDay {
Objects.requireNonNull(transit);
}
}

/**
* A day on which the sun is below the horizon all the time (polar night).
*
* @param transit Time of transit (culmination), i.e. when the sun is closest to the zenith.
*/
record AllNight(ZonedDateTime transit) implements SunriseResult {}
record AllNight(ZonedDateTime transit) implements SunriseResult {
public AllNight {
Objects.requireNonNull(transit);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@

class SPASunriseTransitSetTest {

private static final TemporalUnitOffset WITHIN_A_MINUTE = within(1, ChronoUnit.MINUTES);
private static final TemporalUnitOffset REASONABLE_TOLERANCE = within(40, ChronoUnit.SECONDS);
private static final TemporalUnitOffset STRICT_TOLERANCE = within(1, ChronoUnit.SECONDS);

private static void compare(
SunriseResult result,
Expand All @@ -44,6 +45,18 @@ private static void compare(
}
}

@Test
void rejectsNullValuesInResultRecords() {
assertThrows(NullPointerException.class, () -> new SunriseResult.AllDay(null));
assertThrows(NullPointerException.class, () -> new SunriseResult.AllNight(null));

final var now = ZonedDateTime.now();

assertThrows(NullPointerException.class, () -> new SunriseResult.RegularDay(null, now, now));
assertThrows(NullPointerException.class, () -> new SunriseResult.RegularDay(now, null, now));
assertThrows(NullPointerException.class, () -> new SunriseResult.RegularDay(now, now, null));
}

@Test
void testSpaExampleSunriseTransitSet() {
ZonedDateTime time = ZonedDateTime.of(2003, 10, 17, 12, 30, 30, 0, ZoneOffset.ofHours(-7));
Expand All @@ -56,7 +69,7 @@ void testSpaExampleSunriseTransitSet() {
"2003-10-17T06:12:43-07:00",
"2003-10-17T11:46:04-07:00",
"2003-10-17T17:18:51-07:00",
WITHIN_A_MINUTE);
STRICT_TOLERANCE);
}

@Test
Expand All @@ -67,7 +80,7 @@ void testAllDay() {
var res = SPA.calculateSunriseTransitSet(time, 70.978056, 25.974722, 0);

compare(
res, SunriseResult.AllDay.class, null, "2015-06-17T12:16:55+02:00", null, WITHIN_A_MINUTE);
res, SunriseResult.AllDay.class, null, "2015-06-17T12:16:55+02:00", null, STRICT_TOLERANCE);
}

@Test
Expand All @@ -77,7 +90,7 @@ void testAllNight() {
// location is Honningsvåg, Norway (near North Cape)
var res = SPA.calculateSunriseTransitSet(time, 70.978056, 25.974722, 0);

compare(res, SunriseResult.AllNight.class, null, null, null, WITHIN_A_MINUTE);
compare(res, SunriseResult.AllNight.class, null, null, null, STRICT_TOLERANCE);
}

@Test
Expand All @@ -90,10 +103,10 @@ void testNZSunriseTransitSet() {
compare(
res,
SunriseResult.RegularDay.class,
"2015-06-17T07:32:26+12:00",
"2015-06-17T12:21:46+12:00",
"2015-06-17T17:11:03+12:00",
WITHIN_A_MINUTE);
"2015-06-17T07:32:00+12:00",
"2015-06-17T12:21:41+12:00",
"2015-06-17T17:11:00+12:00",
REASONABLE_TOLERANCE);
}

@Test
Expand All @@ -106,10 +119,10 @@ void testDSToffDayBerlin() {
compare(
res,
SunriseResult.RegularDay.class,
"2015-10-25T06:49:02+01:00",
"2015-10-25T11:50:55+01:00",
"2015-10-25T16:51:59+01:00",
WITHIN_A_MINUTE);
"2015-10-25T06:49:00+01:00",
"2015-10-25T11:50:53+01:00",
"2015-10-25T16:52:00+01:00",
REASONABLE_TOLERANCE);
}

@Test
Expand All @@ -122,10 +135,10 @@ void testDSTonDayBerlin() {
compare(
res,
SunriseResult.RegularDay.class,
"2016-03-27T06:52:19+02:00",
"2016-03-27T13:12:02+02:00",
"2016-03-27T19:32:49+02:00",
WITHIN_A_MINUTE);
"2016-03-27T06:52:00+02:00",
"2016-03-27T13:12:01+02:00",
"2016-03-27T19:33:00+02:00",
REASONABLE_TOLERANCE);
}

@Test
Expand All @@ -138,10 +151,10 @@ void testDSToffDayAuckland() {
compare(
res,
SunriseResult.RegularDay.class,
"2016-04-03T06:36:09+12:00",
"2016-04-03T06:36:00+12:00",
"2016-04-03T12:24:19+12:00",
"2016-04-03T18:11:55+12:00",
WITHIN_A_MINUTE);
"2016-04-03T18:12:00+12:00",
REASONABLE_TOLERANCE);
}

@Test
Expand All @@ -154,10 +167,10 @@ void testDSTonDayAuckland() {
compare(
res,
SunriseResult.RegularDay.class,
"2015-09-27T07:04:14+13:00",
"2015-09-27T13:12:17+13:00",
"2015-09-27T19:20:56+13:00",
WITHIN_A_MINUTE);
"2015-09-27T07:04:00+13:00",
"2015-09-27T13:12:19+13:00",
"2015-09-27T19:21:00+13:00",
REASONABLE_TOLERANCE);
}

@Test
Expand Down Expand Up @@ -248,7 +261,7 @@ void testBulkUSNOReferenceValues(
assertEquals(90.83337, pos.zenithAngle(), 0.01);
}

compare(res, dateTime, typeClass, sunrise, null, sunset, WITHIN_A_MINUTE);
compare(res, dateTime, typeClass, sunrise, null, sunset, REASONABLE_TOLERANCE);
}

@ParameterizedTest
Expand Down Expand Up @@ -340,7 +353,7 @@ void testAllHorizons() {
"2023-03-01T07:04:00Z",
null,
"2023-03-01T17:31:00Z",
WITHIN_A_MINUTE);
REASONABLE_TOLERANCE);

res = SPA.calculateSunriseTransitSet(dateTime, lat, lon, deltaT, SPA.Horizon.CIVIL_TWILIGHT);
compare(
Expand All @@ -349,7 +362,7 @@ void testAllHorizons() {
"2023-03-01T06:22:00Z",
null,
"2023-03-01T18:13:00Z",
WITHIN_A_MINUTE);
REASONABLE_TOLERANCE);

res = SPA.calculateSunriseTransitSet(dateTime, lat, lon, deltaT, SPA.Horizon.NAUTICAL_TWILIGHT);
compare(
Expand All @@ -358,7 +371,7 @@ void testAllHorizons() {
"2023-03-01T05:34:00Z",
null,
"2023-03-01T19:01:00Z",
WITHIN_A_MINUTE);
REASONABLE_TOLERANCE);

res =
SPA.calculateSunriseTransitSet(
Expand All @@ -369,7 +382,7 @@ void testAllHorizons() {
"2023-03-01T04:45:00Z",
null,
"2023-03-01T19:51:00Z",
WITHIN_A_MINUTE);
REASONABLE_TOLERANCE);
}

@Test
Expand All @@ -394,30 +407,30 @@ void testAllHorizonsWithSingleCall() {
"2023-03-01T07:04:00Z",
null,
"2023-03-01T17:31:00Z",
WITHIN_A_MINUTE);
REASONABLE_TOLERANCE);

compare(
results.get(SPA.Horizon.CIVIL_TWILIGHT),
SunriseResult.RegularDay.class,
"2023-03-01T06:22:00Z",
null,
"2023-03-01T18:13:00Z",
WITHIN_A_MINUTE);
REASONABLE_TOLERANCE);

compare(
results.get(SPA.Horizon.NAUTICAL_TWILIGHT),
SunriseResult.RegularDay.class,
"2023-03-01T05:34:00Z",
null,
"2023-03-01T19:01:00Z",
WITHIN_A_MINUTE);
REASONABLE_TOLERANCE);

compare(
results.get(SPA.Horizon.ASTRONOMICAL_TWILIGHT),
SunriseResult.RegularDay.class,
"2023-03-01T04:45:00Z",
null,
"2023-03-01T19:51:00Z",
WITHIN_A_MINUTE);
REASONABLE_TOLERANCE);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package net.e175.klaus.solarpositioning.test;

import static org.junit.jupiter.api.Assertions.assertThrows;

import net.e175.klaus.solarpositioning.SolarPosition;
import org.junit.jupiter.api.Test;

class SolarPositionTest {

@Test
public void rejectsSillyAzimuth() {
assertThrows(IllegalArgumentException.class, () -> new SolarPosition(-0.1, 90));
assertThrows(IllegalArgumentException.class, () -> new SolarPosition(360.1, 90));
}

@Test
public void rejectsSillyZenithAngle() {
assertThrows(IllegalArgumentException.class, () -> new SolarPosition(90, -0.1));
assertThrows(IllegalArgumentException.class, () -> new SolarPosition(90, 180.1));
}
}

0 comments on commit 70db663

Please sign in to comment.