11/*
22 * Zmanim Java API
3- * Copyright (C) 2004-2024 Eliyahu Hershfeld
3+ * Copyright (C) 2004-2025 Eliyahu Hershfeld
44 *
55 * This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General
66 * Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option)
1717
1818import java .util .Calendar ;
1919
20-
2120/**
2221 * Implementation of sunrise and sunset methods to calculate astronomical times based on the <a
2322 * href="https://noaa.gov">NOAA</a> algorithm. This calculator uses the Java algorithm based on the implementation by <a
2928 * to account for elevation. The algorithm can be found in the <a
3029 * href="https://en.wikipedia.org/wiki/Sunrise_equation">Wikipedia Sunrise Equation</a> article.
3130 *
32- * @author © Eliyahu Hershfeld 2011 - 2024
31+ * @author © Eliyahu Hershfeld 2011 - 2025
3332 */
3433public class NOAACalculator extends AstronomicalCalculator {
34+
3535 /**
3636 * The <a href="https://en.wikipedia.org/wiki/Julian_day">Julian day</a> of January 1, 2000, known as
3737 * <a href="https://en.wikipedia.org/wiki/Epoch_(astronomy)#J2000">J2000.0</a>.
@@ -44,12 +44,20 @@ public class NOAACalculator extends AstronomicalCalculator {
4444 private static final double JULIAN_DAYS_PER_CENTURY = 36525.0 ;
4545
4646 /**
47- * An enum to indicate what type of solar event is being calculated.
47+ * An <code>enum</code> to indicate what type of solar event ({@link #SUNRISE SUNRISE}, {@link #SUNSET SUNSET},
48+ * {@link #NOON NOON} or {@link #MIDNIGHT MIDNIGHT}) is being calculated.
49+ * .
4850 */
4951 protected enum SolarEvent {
50- /**SUNRISE A solar event related to sunrise*/ SUNRISE , /**SUNSET A solar event related to sunset*/ SUNSET
51- // possibly add the following in the future, if added, an IllegalArgumentException should be thrown in getSunHourAngle
52- // /**NOON A solar event related to noon*/NOON, /**MIDNIGHT A solar event related to midnight*/MIDNIGHT
52+ /**SUNRISE A solar event related to sunrise*/ SUNRISE , /**SUNSET A solar event related to sunset*/ SUNSET ,
53+ /**NOON A solar event related to noon*/ NOON , /**MIDNIGHT A solar event related to midnight*/ MIDNIGHT
54+ }
55+
56+ /**
57+ * Default constructor of the NOAACalculator.
58+ */
59+ public NOAACalculator () {
60+ super ();
5361 }
5462
5563 /**
@@ -84,7 +92,7 @@ public double getUTCSunset(Calendar calendar, GeoLocation geoLocation, double ze
8492 }
8593
8694 /**
87- * Return the <a href="https://en.wikipedia.org/wiki/Julian_day">Julian day</a> from a Java Calendar
95+ * Return the <a href="https://en.wikipedia.org/wiki/Julian_day">Julian day</a> from a Java Calendar.
8896 *
8997 * @param calendar
9098 * The Java Calendar
@@ -297,7 +305,7 @@ private static double getEquationOfTime(double julianCenturies) {
297305 * @param zenith
298306 * the zenith
299307 * @param solarEvent
300- * If the hour angle is for sunrise or sunset
308+ * If the hour angle is for {@link SolarEvent#SUNRISE SUNRISE} or {@link SolarEvent#SUNSET SUNSET}
301309 * @return hour angle of sunrise in <a href="https://en.wikipedia.org/wiki/Radian">radians</a>
302310 */
303311 private static double getSunHourAngle (double latitude , double solarDeclination , double zenith , SolarEvent solarEvent ) {
@@ -380,7 +388,7 @@ public static double getSolarAzimuth(Calendar calendar, double latitude, double
380388 * "https://kosherjava.com/2020/07/02/definition-of-chatzos/">The Definition of <em>Chatzos</em></a> for details on
381389 * solar noon calculations.
382390 * @see com.kosherjava.zmanim.util.AstronomicalCalculator#getUTCNoon(Calendar, GeoLocation)
383- * @see #getSolarNoonUTC (double, double)
391+ * @see #getSolarNoonMidnightUTC (double, double, SolarEvent )
384392 *
385393 * @param calendar
386394 * The Calendar representing the date to calculate solar noon for
@@ -390,56 +398,81 @@ public static double getSolarAzimuth(Calendar calendar, double latitude, double
390398 * @return the time in minutes from zero UTC
391399 */
392400 public double getUTCNoon (Calendar calendar , GeoLocation geoLocation ) {
393- double noon = getSolarNoonUTC (getJulianDay (calendar ), -geoLocation .getLongitude ());
401+ double noon = getSolarNoonMidnightUTC (getJulianDay (calendar ), -geoLocation .getLongitude (), SolarEvent . NOON );
394402 noon = noon / 60 ;
395403 return noon > 0 ? noon % 24 : noon % 24 + 24 ; // ensure that the time is >= 0 and < 24
396404 }
405+
406+ /**
407+ * Return the <a href="https://en.wikipedia.org/wiki/Universal_Coordinated_Time">Universal Coordinated Time</a>
408+ * (UTC) of the <a href="https://en.wikipedia.org/wiki/Midnight">solar midnight</a> for the end of the given civil
409+ * day at the given location on earth (about 12 hours after solar noon). This implementation returns true solar
410+ * midnight as opposed to the time halfway between sunrise and sunset. Other calculators may return a more
411+ * simplified calculation of halfway between sunrise and sunset. See <a href=
412+ * "https://kosherjava.com/2020/07/02/definition-of-chatzos/">The Definition of <em>Chatzos</em></a> for details on
413+ * solar noon / midnight calculations.
414+ * @see com.kosherjava.zmanim.util.AstronomicalCalculator#getUTCNoon(Calendar, GeoLocation)
415+ * @see #getSolarNoonMidnightUTC(double, double, SolarEvent)
416+ *
417+ * @param calendar
418+ * The Calendar representing the date to calculate solar noon for
419+ * @param geoLocation
420+ * The location information used for astronomical calculating sun times. This class uses only requires
421+ * the longitude for calculating noon since it is the same time anywhere along the longitude line.
422+ * @return the time in minutes from zero UTC
423+ */
424+ public double getUTCMidnight (Calendar calendar , GeoLocation geoLocation ) {
425+ double midnight = getSolarNoonMidnightUTC (getJulianDay (calendar ), -geoLocation .getLongitude (), SolarEvent .MIDNIGHT );
426+ midnight = midnight / 60 ;
427+ return midnight > 0 ? midnight % 24 : midnight % 24 + 24 ; // ensure that the time is >= 0 and < 24
428+ }
397429
398430 /**
399431 * Return the <a href="https://en.wikipedia.org/wiki/Universal_Coordinated_Time">Universal Coordinated Time</a> (UTC)
400- * of <a href="http://en.wikipedia.org/wiki/Noon#Solar_noon">solar noon</a> for the given day at the given location
401- * on earth.
402- * @todo Refactor to possibly use the getSunRiseSetUTC (to be renamed) and remove the need for this method.
432+ * of the current day <a href="http://en.wikipedia.org/wiki/Noon#Solar_noon">solar noon</a> or the the upcoming
433+ * midnight (about 12 hours after solar noon) of the given day at the given location on earth.
403434 *
404435 * @param julianDay
405- * the Julian day since <a href=
436+ * The Julian day since <a href=
406437 * "https://en.wikipedia.org/wiki/Epoch_(astronomy)#J2000">J2000.0</a>.
407438 * @param longitude
408- * the longitude of observer in degrees
409- *
439+ * The longitude of observer in degrees
440+ * @param solarEvent
441+ * If the calculation is for {@link SolarEvent#NOON NOON} or {@link SolarEvent#MIDNIGHT MIDNIGHT}
442+ *
410443 * @return the time in minutes from zero UTC
411444 *
412445 * @see com.kosherjava.zmanim.util.AstronomicalCalculator#getUTCNoon(Calendar, GeoLocation)
413446 * @see #getUTCNoon(Calendar, GeoLocation)
414447 */
415- private static double getSolarNoonUTC (double julianDay , double longitude ) {
448+ private static double getSolarNoonMidnightUTC (double julianDay , double longitude , SolarEvent solarEvent ) {
449+ julianDay = (solarEvent == SolarEvent .NOON ) ? julianDay : julianDay + 0.5 ;
416450 // First pass for approximate solar noon to calculate equation of time
417451 double tnoon = getJulianCenturiesFromJulianDay (julianDay + longitude / 360.0 );
418452 double equationOfTime = getEquationOfTime (tnoon );
419- double solNoonUTC = 720 + (longitude * 4 ) - equationOfTime ; // minutes
453+ double solNoonUTC = (longitude * 4 ) - equationOfTime ; // minutes
420454
421455 // second pass
422- double newt = getJulianCenturiesFromJulianDay (julianDay - 0.5 + solNoonUTC / 1440.0 );
456+ double newt = getJulianCenturiesFromJulianDay (julianDay + solNoonUTC / 1440.0 );
423457 equationOfTime = getEquationOfTime (newt );
424- return 720 + (longitude * 4 ) - equationOfTime ;
458+ return ( solarEvent == SolarEvent . NOON ? 720 : 1440 ) + (longitude * 4 ) - equationOfTime ;
425459 }
426460
427461 /**
428462 * Return the <a href="https://en.wikipedia.org/wiki/Universal_Coordinated_Time">Universal Coordinated Time</a> (UTC)
429463 * of sunrise or sunset in minutes for the given day at the given location on earth.
430- * @todo support solar noon and possibly increase the number of passes in the Arctic areas.
431- * This would remove the need for getSolarNoonUTC.
464+ * @todo Possibly increase the number of passes for improved accuracy, especially in the Arctic areas.
432465 *
433466 * @param calendar
434- * the calendar
467+ * The calendar
435468 * @param latitude
436- * the latitude of observer in degrees
469+ * The latitude of observer in degrees
437470 * @param longitude
438- * longitude of observer in degrees
471+ * Longitude of observer in degrees
439472 * @param zenith
440- * zenith
473+ * Zenith
441474 * @param solarEvent
442- * Is the calculation for sunrise or sunset
475+ * If the calculation is for {@link SolarEvent#SUNRISE SUNRISE} or {@link SolarEvent#SUNSET SUNSET}
443476 * @return the time in minutes from zero Universal Coordinated Time (UTC)
444477 */
445478 private static double getSunRiseSetUTC (Calendar calendar , double latitude , double longitude , double zenith ,
@@ -448,7 +481,12 @@ private static double getSunRiseSetUTC(Calendar calendar, double latitude, doubl
448481
449482 // Find the time of solar noon at the location, and use that declination.
450483 // This is better than start of the Julian day
451- double noonmin = getSolarNoonUTC (julianDay , longitude );
484+ // TODO really not needed since the Julian day starts from local fixed noon. Changing this would be more
485+ // efficient but would likely cause a very minor discrepancy in the calculated times (likely not reducing
486+ // accuracy, just slightly different, thus potentially breaking test cases). Regardless, it would be within
487+ // milliseconds.
488+ double noonmin = getSolarNoonMidnightUTC (julianDay , longitude , SolarEvent .NOON );
489+
452490 double tnoon = getJulianCenturiesFromJulianDay (julianDay + noonmin / 1440.0 );
453491
454492 // First calculates sunrise and approximate length of day
0 commit comments