11/*
22 * Zmanim Java API
3- * Copyright (C) 2004-2025 Eliyahu Hershfeld
3+ * Copyright (C) 2004-2026 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)
1515 */
1616package com .kosherjava .zmanim .util ;
1717
18- import java .util .Calendar ;
18+ import java .time .ZoneOffset ;
19+ import java .time .ZonedDateTime ;
1920
2021/**
2122 * Implementation of sunrise and sunset methods to calculate astronomical times based on the <a
2829 * to account for elevation. The algorithm can be found in the <a
2930 * href="https://en.wikipedia.org/wiki/Sunrise_equation">Wikipedia Sunrise Equation</a> article.
3031 *
31- * @author © Eliyahu Hershfeld 2011 - 2025
32+ * @author © Eliyahu Hershfeld 2011 - 2026
3233 */
3334public class NOAACalculator extends AstronomicalCalculator {
3435
@@ -70,10 +71,10 @@ public String getCalculatorName() {
7071 /**
7172 * @see com.kosherjava.zmanim.util.AstronomicalCalculator#getUTCSunrise(Calendar, GeoLocation, double, boolean)
7273 */
73- public double getUTCSunrise (Calendar calendar , GeoLocation geoLocation , double zenith , boolean adjustForElevation ) {
74+ public double getUTCSunrise (ZonedDateTime zdt , GeoLocation geoLocation , double zenith , boolean adjustForElevation ) {
7475 double elevation = adjustForElevation ? geoLocation .getElevation () : 0 ;
7576 double adjustedZenith = adjustZenith (zenith , elevation );
76- double sunrise = getSunRiseSetUTC (calendar , geoLocation .getLatitude (), -geoLocation .getLongitude (),
77+ double sunrise = getSunRiseSetUTC (zdt , geoLocation .getLatitude (), -geoLocation .getLongitude (),
7778 adjustedZenith , SolarEvent .SUNRISE );
7879 sunrise = sunrise / 60 ;
7980 return sunrise > 0 ? sunrise % 24 : sunrise % 24 + 24 ; // ensure that the time is >= 0 and < 24
@@ -82,10 +83,10 @@ public double getUTCSunrise(Calendar calendar, GeoLocation geoLocation, double z
8283 /**
8384 * @see com.kosherjava.zmanim.util.AstronomicalCalculator#getUTCSunset(Calendar, GeoLocation, double, boolean)
8485 */
85- public double getUTCSunset (Calendar calendar , GeoLocation geoLocation , double zenith , boolean adjustForElevation ) {
86+ public double getUTCSunset (ZonedDateTime zdt , GeoLocation geoLocation , double zenith , boolean adjustForElevation ) {
8687 double elevation = adjustForElevation ? geoLocation .getElevation () : 0 ;
8788 double adjustedZenith = adjustZenith (zenith , elevation );
88- double sunset = getSunRiseSetUTC (calendar , geoLocation .getLatitude (), -geoLocation .getLongitude (),
89+ double sunset = getSunRiseSetUTC (zdt , geoLocation .getLatitude (), -geoLocation .getLongitude (),
8990 adjustedZenith , SolarEvent .SUNSET );
9091 sunset = sunset / 60 ;
9192 return sunset > 0 ? sunset % 24 : sunset % 24 + 24 ; // ensure that the time is >= 0 and < 24
@@ -94,22 +95,25 @@ public double getUTCSunset(Calendar calendar, GeoLocation geoLocation, double ze
9495 /**
9596 * Return the <a href="https://en.wikipedia.org/wiki/Julian_day">Julian day</a> from a Java Calendar.
9697 *
97- * @param calendar
98- * The Java Calendar
98+ * @param zonedDateTime
99+ * The ZonedDateTime
99100 * @return the Julian day corresponding to the date Note: Number is returned for the start of the Julian
100101 * day. Fractional days / time should be added later.
101102 */
102- private static double getJulianDay (Calendar calendar ) {
103- int year = calendar .get (Calendar .YEAR );
104- int month = calendar .get (Calendar .MONTH ) + 1 ;
105- int day = calendar .get (Calendar .DAY_OF_MONTH );
106- if (month <= 2 ) {
107- year -= 1 ;
108- month += 12 ;
109- }
110- int a = year / 100 ;
111- int b = 2 - a + a / 4 ;
112- return Math .floor (365.25 * (year + 4716 )) + Math .floor (30.6001 * (month + 1 )) + day + b - 1524.5 ;
103+ private static double getJulianDay (ZonedDateTime zonedDateTime ) {
104+ int year = zonedDateTime .getYear ();
105+ int month = zonedDateTime .getMonthValue ();
106+ int day = zonedDateTime .getDayOfMonth ();
107+
108+ if (month <= 2 ) {
109+ year -= 1 ;
110+ month += 12 ;
111+ }
112+
113+ int a = year / 100 ;
114+ int b = 2 - a + a / 4 ;
115+
116+ return Math .floor (365.25 * (year + 4716 )) + Math .floor (30.6001 * (month + 1 )) + day + b - 1524.5 ;
113117 }
114118
115119 /**
@@ -309,16 +313,16 @@ private static double getSunHourAngle(double latitude, double solarDeclination,
309313 /**
310314 * @see com.kosherjava.zmanim.util.AstronomicalCalculator#getSolarElevation(Calendar, GeoLocation)
311315 */
312- public double getSolarElevation (Calendar calendar , GeoLocation geoLocation ) {
313- return getSolarElevationAzimuth (calendar , geoLocation , false );
316+ public double getSolarElevation (ZonedDateTime zdt , GeoLocation geoLocation ) {
317+ return getSolarElevationAzimuth (zdt , geoLocation , false );
314318
315319 }
316320
317321 /**
318322 * @see com.kosherjava.zmanim.util.AstronomicalCalculator#getSolarAzimuth(Calendar, GeoLocation)
319323 */
320- public double getSolarAzimuth (Calendar calendar , GeoLocation geoLocation ) {
321- return getSolarElevationAzimuth (calendar , geoLocation , true );
324+ public double getSolarAzimuth (ZonedDateTime zdt , GeoLocation geoLocation ) {
325+ return getSolarElevationAzimuth (zdt , geoLocation , true );
322326 }
323327
324328 /**
@@ -327,7 +331,7 @@ public double getSolarAzimuth(Calendar calendar, GeoLocation geoLocation) {
327331 * and time. Can be negative if the sun is below the horizon. Elevation is based on sea-level and is not
328332 * adjusted for altitude.
329333 *
330- * @param calendar
334+ * @param zonedDateTime
331335 * time of calculation
332336 * @param geoLocation
333337 * The location for calculating the elevation or azimuth.
@@ -338,59 +342,50 @@ public double getSolarAzimuth(Calendar calendar, GeoLocation geoLocation) {
338342 * @see #getSolarElevation(Calendar, GeoLocation)
339343 * @see #getSolarAzimuth(Calendar, GeoLocation)
340344 */
341- private double getSolarElevationAzimuth (Calendar calendar , GeoLocation geoLocation , boolean isAzimuth ) {
342- double latitude = geoLocation .getLatitude ();
343- double longitude = geoLocation .getLongitude ();
344-
345- Calendar cloned = (Calendar ) calendar .clone ();
346- int offset = - adjustHourForTimeZone (cloned );
347- cloned .add (Calendar .MILLISECOND , offset );
348- int minute = cloned .get (Calendar .MINUTE );
349- int second = cloned .get (Calendar .SECOND );
350- int hour = cloned .get (Calendar .HOUR_OF_DAY );
351- int milli = cloned .get (Calendar .MILLISECOND );
352-
353- double time = (hour + (minute + (second + (milli / 1000.0 )) / 60.0 ) / 60.0 ) / 24.0 ;
354- double julianDay = getJulianDay (cloned ) + time ;
355- double julianCenturies = getJulianCenturiesFromJulianDay (julianDay );
356- double eot = getEquationOfTime (julianCenturies );
357- double theta = getSunDeclination (julianCenturies );
358-
359- double adjustment = time + eot / 1440 ;
360- double trueSolarTime = ((adjustment + longitude / 360 ) + 2 ) % 1 ; // adding 2 to ensure that it never ends up negative
361- double hourAngelRad = trueSolarTime * Math .PI * 2 - Math .PI ;
362- double cosZenith = Math .sin (Math .toRadians (latitude )) * Math .sin (Math .toRadians (theta ))
363- + Math .cos (Math .toRadians (latitude )) * Math .cos (Math .toRadians (theta )) * Math .cos (hourAngelRad );
364- double zenith = Math .toDegrees (Math .acos (cosZenith > 1 ? 1 : cosZenith < -1 ? -1 : cosZenith ));
365- double azDenom = Math .cos (Math .toRadians (latitude )) * Math .sin (Math .toRadians (zenith ));
366- double refractionAdjustment = 0 ;
367- double elevation = 90.0 - (zenith - refractionAdjustment );
368- double azimuth = 0 ;
369- double azRad = (Math .sin (Math .toRadians (latitude )) * Math .cos (Math .toRadians (zenith ))
370- - Math .sin (Math .toRadians (theta ))) / azDenom ;
371- if (Math .abs (azDenom ) > 0.001 ) {
372- azimuth = 180 - Math .toDegrees (Math .acos (azRad > 1 ? 1 : azRad < -1 ? -1 : azRad )) * (hourAngelRad > 0 ? -1 : 1 ) ;
373- } else {
374- azimuth = latitude > 0 ? 180 : 0 ;
375- }
376- return isAzimuth ? azimuth % 360 : elevation ;
345+ private double getSolarElevationAzimuth (ZonedDateTime zonedDateTime , GeoLocation geoLocation , boolean isAzimuth ) {
346+
347+ double lat = Math .toRadians (geoLocation .getLatitude ());
348+ double lon = geoLocation .getLongitude ();
349+
350+ ZonedDateTime utc = zonedDateTime .withZoneSameInstant (ZoneOffset .UTC );
351+
352+ double fractionalDay =
353+ (utc .getHour ()
354+ + (utc .getMinute ()
355+ + (utc .getSecond () + utc .getNano () / 1_000_000_000.0 ) / 60.0 ) / 60.0 ) / 24.0 ;
356+
357+ double jd = getJulianDay (utc ) + fractionalDay ;
358+ double jc = getJulianCenturiesFromJulianDay (jd );
359+
360+ double decl = Math .toRadians (getSunDeclination (jc ));
361+ double eot = getEquationOfTime (jc );
362+
363+ double trueSolarTime = ((fractionalDay + eot / 1440.0 + lon / 360.0 ) + 2 ) % 1 ;
364+ double hourAngle = trueSolarTime * 2 * Math .PI - Math .PI ;
365+
366+ double cosZenith =
367+ Math .sin (lat ) * Math .sin (decl )
368+ + Math .cos (lat ) * Math .cos (decl ) * Math .cos (hourAngle );
369+
370+ double zenith = Math .acos (Math .max (-1 , Math .min (1 , cosZenith )));
371+ double elevation = 90 - Math .toDegrees (zenith );
372+
373+ double azimuth ;
374+ double azDenom = Math .cos (lat ) * Math .sin (zenith );
375+
376+ if (Math .abs (azDenom ) > 0.001 ) {
377+ double az =
378+ (Math .sin (lat ) * Math .cos (zenith ) - Math .sin (decl )) / azDenom ;
379+
380+ azimuth = 180 - Math .toDegrees (Math .acos (Math .max (-1 , Math .min (1 , az ))))
381+ * (hourAngle > 0 ? -1 : 1 );
382+ } else {
383+ azimuth = geoLocation .getLatitude () > 0 ? 180 : 0 ;
384+ }
385+
386+ return isAzimuth ? (azimuth + 360 ) % 360 : elevation ;
377387 }
378388
379- /**
380- * Returns the hour of day adjusted for the timezone and DST. This is needed for the azimuth and elevation
381- * calculations.
382- * @param calendar the Calendar to extract the hour from. This must have the timezone set to the proper timezone.
383- * @return the adjusted hour corrected for timezone and DST offset.
384- */
385- private int adjustHourForTimeZone (Calendar calendar ) {
386- int offset = calendar .getTimeZone ().getRawOffset ();
387- int dstOffset = calendar .getTimeZone ().getDSTSavings ();
388- if (calendar .getTimeZone ().inDaylightTime (calendar .getTime ())) {
389- offset = offset + dstOffset ;
390- }
391- return offset ;
392- }
393-
394389 /**
395390 * Return the <a href="https://en.wikipedia.org/wiki/Universal_Coordinated_Time">Universal Coordinated Time</a> (UTC)
396391 * of <a href="https://en.wikipedia.org/wiki/Noon#Solar_noon">solar noon</a> for the given day at the given location
@@ -401,15 +396,15 @@ private int adjustHourForTimeZone(Calendar calendar) {
401396 * @see com.kosherjava.zmanim.util.AstronomicalCalculator#getUTCNoon(Calendar, GeoLocation)
402397 * @see #getSolarNoonMidnightUTC(double, double, SolarEvent)
403398 *
404- * @param calendar
405- * The Calendar representing the date to calculate solar noon for
399+ * @param zonedDateTime
400+ * The zonedDateTime representing the date to calculate solar noon for
406401 * @param geoLocation
407402 * The location information used for astronomical calculating sun times. This class uses only requires
408403 * the longitude for calculating noon since it is the same time anywhere along the longitude line.
409404 * @return the time in minutes from zero UTC
410405 */
411- public double getUTCNoon (Calendar calendar , GeoLocation geoLocation ) {
412- double noon = getSolarNoonMidnightUTC (getJulianDay (calendar ), -geoLocation .getLongitude (), SolarEvent .NOON );
406+ public double getUTCNoon (ZonedDateTime zonedDateTime , GeoLocation geoLocation ) {
407+ double noon = getSolarNoonMidnightUTC (getJulianDay (zonedDateTime ), -geoLocation .getLongitude (), SolarEvent .NOON );
413408 noon = noon / 60 ;
414409 return noon > 0 ? noon % 24 : noon % 24 + 24 ; // ensure that the time is >= 0 and < 24
415410 }
@@ -425,15 +420,15 @@ public double getUTCNoon(Calendar calendar, GeoLocation geoLocation) {
425420 * @see com.kosherjava.zmanim.util.AstronomicalCalculator#getUTCNoon(Calendar, GeoLocation)
426421 * @see #getSolarNoonMidnightUTC(double, double, SolarEvent)
427422 *
428- * @param calendar
429- * The Calendar representing the date to calculate solar noon for
423+ * @param zonedDateTime
424+ * The <code>ZonedDateTime</code> representing the date to calculate solar noon for
430425 * @param geoLocation
431426 * The location information used for astronomical calculating sun times. This class uses only requires
432427 * the longitude for calculating noon since it is the same time anywhere along the longitude line.
433428 * @return the time in minutes from zero UTC
434429 */
435- public double getUTCMidnight (Calendar calendar , GeoLocation geoLocation ) {
436- double midnight = getSolarNoonMidnightUTC (getJulianDay (calendar ), -geoLocation .getLongitude (), SolarEvent .MIDNIGHT );
430+ public double getUTCMidnight (ZonedDateTime zonedDateTime , GeoLocation geoLocation ) {
431+ double midnight = getSolarNoonMidnightUTC (getJulianDay (zonedDateTime ), -geoLocation .getLongitude (), SolarEvent .MIDNIGHT );
437432 midnight = midnight / 60 ;
438433 return midnight > 0 ? midnight % 24 : midnight % 24 + 24 ; // ensure that the time is >= 0 and < 24
439434 }
@@ -474,8 +469,8 @@ private static double getSolarNoonMidnightUTC(double julianDay, double longitude
474469 * of sunrise or sunset in minutes for the given day at the given location on earth.
475470 * @todo Possibly increase the number of passes for improved accuracy, especially in the Arctic areas.
476471 *
477- * @param calendar
478- * The calendar
472+ * @param zonedDateTime
473+ * The <code>ZonedDateTime</code>.
479474 * @param latitude
480475 * The latitude of observer in degrees
481476 * @param longitude
@@ -486,9 +481,9 @@ private static double getSolarNoonMidnightUTC(double julianDay, double longitude
486481 * If the calculation is for {@link SolarEvent#SUNRISE SUNRISE} or {@link SolarEvent#SUNSET SUNSET}
487482 * @return the time in minutes from zero Universal Coordinated Time (UTC)
488483 */
489- private static double getSunRiseSetUTC (Calendar calendar , double latitude , double longitude , double zenith ,
484+ private static double getSunRiseSetUTC (ZonedDateTime zonedDateTime , double latitude , double longitude , double zenith ,
490485 SolarEvent solarEvent ) {
491- double julianDay = getJulianDay (calendar );
486+ double julianDay = getJulianDay (zonedDateTime );
492487
493488 // Find the time of solar noon at the location, and use that declination.
494489 // This is better than start of the Julian day
0 commit comments