001/*
002 * Zmanim Java API
003 * Copyright © 2004-2026 Eliyahu Hershfeld
004 *
005 * This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General
006 * Public License as published by the Free Software Foundation; version 2.1 of the License.
007 *
008 * This library is distributed in the hope that it will be useful,but WITHOUT ANY WARRANTY; without even the implied
009 * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
010 * details.
011 * You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to
012 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA,
013 * or connect to: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
014 */
015package com.kosherjava.zmanim;
016
017import java.time.Duration;
018import java.time.Instant;
019import java.time.LocalDate;
020import java.time.LocalDateTime;
021import java.time.LocalTime;
022import java.time.ZoneOffset;
023import java.time.ZonedDateTime;
024import java.util.Objects;
025
026import com.kosherjava.zmanim.util.AstronomicalCalculator;
027import com.kosherjava.zmanim.util.GeoLocation;
028import com.kosherjava.zmanim.util.ZmanimFormatter;
029
030/**
031 * A Java calendar that calculates astronomical times such as {@link #getSunrise() sunrise}, {@link #getSunset() sunset} and twilight
032 * times. This class contains a {@link #getLocalDate() LocalDate} and can therefore use the standard calendar functionality to change
033 * dates etc. The calculation engine used to calculate the astronomical times can be changed to a different implementation by
034 * implementing the abstract {@link AstronomicalCalculator} which is configured via
035 * {@link #setAstronomicalCalculator(AstronomicalCalculator)}. A number of different calculation engine implementations are included
036 * in the util package.
037 * <b>Note:</b> There are times when the algorithms can't calculate proper values for sunrise, sunset and twilight. This is usually
038 * caused by trying to calculate times for areas either very far North or South, where sunrise / sunset never happen on that date.
039 * This is common when calculating deep twilight angles at high northern latitudes, such as London. The sun never reaches this dip at
040 * certain times of the year. When the calculations encounter this condition a {@code null} will be returned when a {@link
041 * java.time.Instant} or {@link java.time.Duration} is expected. The reason that {@code Exception}s are not thrown in these cases is
042 * because the lack of a rise/set or twilight is not an exception, but an expected condition in many parts of the world.
043 * <p>
044 * Here is a simple example of how to use the API to calculate sunrise.
045 * First create the AstronomicalCalendar for the location you would like to calculate sunrise or sunset times for:
046 * 
047 * {@snippet lang='java' :
048 * String locationName = &quot;Lakewood, NJ&quot;;
049 * double latitude = 40.0828; // Lakewood, NJ
050 * double longitude = -74.2094; // Lakewood, NJ
051 * double elevation = 20; // optional elevation correction in Meters
052 * // @link region="target_zone_link" substring="getAvailableZoneIds()" target="java.time.ZoneId#getAvailableZoneIds()"
053 * ZoneId zoneId = ZoneId.of("America/New_York"); // set the zoneId to a valid ZoneId listed in getAvailableZoneIds()
054 * // @end
055 * GeoLocation location = new GeoLocation(locationName, latitude, longitude, elevation, zoneId);
056 * AstronomicalCalendar ac = new AstronomicalCalendar(location);
057 * }
058 * 
059 * To get the time of sunrise, first set the date you want (if not set, the date will default to today):
060 * 
061 * {@snippet lang='java' :
062 * LocalDate localDate = LocalDate.of(1969, Month.FEBRUARY, 8);
063 * ac.setLocalDate(localDate);
064 * Instant sunrise = ac.getSunrise();
065 * }
066 * 
067 * @author © Eliyahu Hershfeld 2004 - 2026
068 */
069public class AstronomicalCalendar implements Cloneable {
070
071        /**
072         * 90° below the vertical. Used as a basis for most calculations since the location of the sun is 90° below the horizon
073         * at sunrise and sunset.
074         * <b>Note </b>: it is important to note that for sunrise and sunset the {@link AstronomicalCalculator#adjustZenith(double,
075         * double, LocalDate) adjusted zenith} is required to account for the radius of the sun and refraction. The adjusted zenith should
076         * not be used for calculations above or below 90° since they are usually calculated as an offset to 90°.
077         */
078        public static final double GEOMETRIC_ZENITH = 90;
079
080        /** Sun's zenith at civil twilight (96°). */
081        public static final double CIVIL_ZENITH = 96;
082
083        /** Sun's zenith at nautical twilight (102°). */
084        public static final double NAUTICAL_ZENITH = 102;
085
086        /** Sun's zenith at astronomical twilight (108°). */
087        public static final double ASTRONOMICAL_ZENITH = 108;
088
089        /** constant for nanoseconds in a minute (60 billion / 60,000,000,000) */
090        public static final long MINUTE_NANOS = 60_000_000_000L;
091
092        /** constant for nanoseconds in an hour (3.6 trillion / 3,600,000,000,000) */
093        public static final long HOUR_NANOS = 3_600_000_000_000L;
094        
095        /**
096         * The {@code LocalDate} encapsulated by this class to track the current date used by the class
097         */
098        private LocalDate localDate;
099
100        /**
101         * the {@link GeoLocation} used for calculations.
102         */
103        private GeoLocation geoLocation;
104
105        /**
106         * the internal {@link AstronomicalCalculator} used for calculating solar based times.
107         */
108        private AstronomicalCalculator astronomicalCalculator;
109
110        /**
111         * The getSunrise method returns a {@code Instant} representing the {@link AstronomicalCalculator
112         * #getElevationAdjustment(double) elevation adjusted} sunrise time. The zenith used for the calculation uses {@link
113         * #GEOMETRIC_ZENITH geometric zenith} of 90° plus {@link AstronomicalCalculator#getElevationAdjustment(double)}. This is
114         * adjusted by the {@link AstronomicalCalculator} to add approximately 50/60 of a degree to account for 34 arcminutes of
115         * refraction and 16 arcminutes for the sun's radius for a total of {@link AstronomicalCalculator#adjustZenith 90.83333°}.
116         * See documentation for the specific implementation of the {@link AstronomicalCalculator} that you are using.
117         * 
118         * @return the {@code Instant} representing the exact sunrise time. If the calculation can't be computed such as in the
119         *         Arctic Circle where there is at least one day a year where the sun does not rise, and one where it does not set, a
120         *         {@code null} will be returned. See detailed explanation on top of the page.
121         * @see AstronomicalCalculator#adjustZenith(double, double, LocalDate)
122         * @see #getSeaLevelSunrise()
123         * @see #getUTCSunrise(double)
124         */
125        public Instant getSunrise() {
126                double sunrise = getUTCSunrise(GEOMETRIC_ZENITH);
127                if (Double.isNaN(sunrise)) {
128                        return null;
129                } else {
130                        return getInstantFromTime(sunrise, SolarEvent.SUNRISE);
131                }
132        }
133
134        /**
135         * A method that returns the sunrise without {@link AstronomicalCalculator#getElevationAdjustment(double) elevation
136         * adjustment}. Non-sunrise and sunset calculations such as dawn and dusk, depend on the amount of visible light,
137         * something that is not affected by elevation. This method returns sunrise calculated at sea level. This forms the
138         * base for dawn calculations that are calculated as a dip below the horizon before sunrise.
139         * 
140         * @return the {@code Instant} representing the exact sea-level sunrise time. If the calculation can't be computed
141         *         such as in the Arctic Circle where there is at least one day a year where the sun does not rise, and one
142         *         where it does not set, a {@code null} will be returned. See detailed explanation on top of the page.
143         * @see #getSunrise()
144         * @see #getUTCSeaLevelSunrise(double)
145         * @see #getSeaLevelSunset()
146         */
147        public Instant getSeaLevelSunrise() {
148                double sunrise = getUTCSeaLevelSunrise(GEOMETRIC_ZENITH);
149                if (Double.isNaN(sunrise)) {
150                        return null;
151                } else {
152                        return getInstantFromTime(sunrise, SolarEvent.SUNRISE);
153                }
154        }
155
156        /**
157         * A method that returns the beginning of <a href="https://en.wikipedia.org/wiki/Twilight#Civil_twilight">civil twilight</a>
158         * (dawn) using a zenith of {@link #CIVIL_ZENITH 96°}.
159         * 
160         * @return The {@code Instant} of the beginning of civil twilight using a zenith of 96°. If the calculation
161         *         can't be computed, {@code null} will be returned. See detailed explanation on top of the page.
162         */
163        public Instant getBeginCivilTwilight() {
164                return getSunriseOffsetByDegrees(CIVIL_ZENITH);
165        }
166
167        /**
168         * A method that returns the beginning of <a href="https://en.wikipedia.org/wiki/Twilight#Nautical_twilight">nautical twilight</a>
169         * using a zenith of {@link #NAUTICAL_ZENITH 102°}.
170         * 
171         * @return The {@code Instant} of the beginning of nautical twilight using a zenith of 102°. If the calculation
172         *         can't be computed {@code null} will be returned. See detailed explanation on top of the page.
173         */
174        public Instant getBeginNauticalTwilight() {
175                return getSunriseOffsetByDegrees(NAUTICAL_ZENITH);
176        }
177
178        /**
179         * A method that returns the beginning of <a href="https://en.wikipedia.org/wiki/Twilight#Astronomical_twilight">astronomical
180         * twilight</a> using a zenith of {@link #ASTRONOMICAL_ZENITH 108°}.
181         * 
182         * @return The {@code Instant} of the beginning of astronomical twilight using a zenith of 108°. If the calculation
183         *         can't be computed, {@code null} will be returned. See detailed explanation on top of the page.
184         */
185        public Instant getBeginAstronomicalTwilight() {
186                return getSunriseOffsetByDegrees(ASTRONOMICAL_ZENITH);
187        }
188
189        /**
190         * The getSunset method returns an {@code Instant} representing the
191         * {@link AstronomicalCalculator#getElevationAdjustment(double) elevation adjusted} sunset time. The zenith used for the
192         * calculation uses {@link #GEOMETRIC_ZENITH geometric zenith} of 90° plus {@link AstronomicalCalculator
193         * #getElevationAdjustment(double)}. This is adjusted by the {@link AstronomicalCalculator} to add approximately 50/60 of a
194         * degree to account for 34 arcminutes of refraction and 16 arcminutes for the sun's radius for a total of {@link
195         * AstronomicalCalculator#adjustZenith(double, double, LocalDate) 90.83333°}. See documentation for the specific implementation of the
196         * {@link AstronomicalCalculator} that you are using.
197         * Note: In certain cases the calculates sunset will occur before sunrise. This will typically happen when a time zone other than
198         * the local timezone is used (calculating Los Angeles sunset using a GMT time zone for example). In this case the sunset date
199         * will be incremented to the following date.
200         * @todo update documentation for solar radius changes.
201         *
202         * @return the {@code Instant} representing the exact sunset time. If the calculation can't be computed such as in the Arctic
203         *         Circle where there is at least one day a year where the sun does not rise, and one where it does not set, a
204         *         {@code null} will be returned. See detailed explanation on top of the page.
205         * @see AstronomicalCalculator#adjustZenith(double, double, LocalDate)
206         * @see #getSeaLevelSunset()
207         * @see #getUTCSunset(double)
208         */
209        public Instant getSunset() {
210                double sunset = getUTCSunset(GEOMETRIC_ZENITH);
211                if (Double.isNaN(sunset)) {
212                        return null;
213                } else {
214                        return getInstantFromTime(sunset, SolarEvent.SUNSET);
215                }
216        }
217        
218        /**
219         * A method that returns the sunset without {@link AstronomicalCalculator#getElevationAdjustment(double) elevation adjustment}.
220         * Non-sunrise and sunset calculations such as dawn and dusk, depend on the amount of visible light, something that is not
221         * affected by elevation. This method returns sunset calculated at sea level. This forms the base for dusk calculations that are
222         * calculated as a dip below the horizon after sunset.
223         * 
224         * @return the {@code Instant} representing the exact sea-level sunset time. If the calculation can't be computed
225         *         such as in the Arctic Circle where there is at least one day a year where the sun does not rise, and one
226         *         where it does not set, a {@code null} will be returned. See detailed explanation on top of the page.
227         * @see #getSunset()
228         * @see #getUTCSeaLevelSunset(double)
229         */
230        public Instant getSeaLevelSunset() {
231                double sunset = getUTCSeaLevelSunset(GEOMETRIC_ZENITH);
232                if (Double.isNaN(sunset)) {
233                        return null;
234                } else {
235                        return getInstantFromTime(sunset, SolarEvent.SUNSET);
236                }
237        }
238
239        /**
240         * A method that returns the end of <a href="https://en.wikipedia.org/wiki/Twilight#Civil_twilight">civil twilight</a>
241         * using a zenith of {@link #CIVIL_ZENITH 96°}.
242         * 
243         * @return The {@code Instant} of the end of civil twilight using a zenith of {@link #CIVIL_ZENITH 96°}. If the
244         *         calculation can't be computed, {@code null} will be returned. See detailed explanation on top of the page.
245         */
246        public Instant getEndCivilTwilight() {
247                return getSunsetOffsetByDegrees(CIVIL_ZENITH);
248        }
249
250        /**
251         * A method that returns the end of nautical twilight using a zenith of {@link #NAUTICAL_ZENITH 102°}.
252         * 
253         * @return The {@code Instant} of the end of nautical twilight using a zenith of {@link #NAUTICAL_ZENITH 102°}. If the
254         *         calculation can't be computed, {@code null} will be returned. See detailed explanation on top of the page.
255         */
256        public Instant getEndNauticalTwilight() {
257                return getSunsetOffsetByDegrees(NAUTICAL_ZENITH);
258        }
259
260        /**
261         * A method that returns the end of astronomical twilight using a zenith of {@link #ASTRONOMICAL_ZENITH 108°}.
262         * 
263         * @return the {@code Instant} of the end of astronomical twilight using a zenith of {@link #ASTRONOMICAL_ZENITH 108°}. If
264         *         the calculation can't be computed, {@code null} will be returned. See detailed explanation on top of the page.
265         */
266        public Instant getEndAstronomicalTwilight() {
267                return getSunsetOffsetByDegrees(ASTRONOMICAL_ZENITH);
268        }
269        
270        /**
271         * A utility method that returns an {@code Instant} offset by the offset time passed in. Please note that the level of light
272         * during twilight is not affected by elevation, so if this is being used to calculate an offset before sunrise or after sunset
273         * with the intent of getting a rough "level of light" calculation, the sunrise or sunset time passed to this method should be
274         * sea level sunrise and sunset.
275         * 
276         * @param time the start time
277         * @param offset the {@code Duration} of the offset to add to the time.
278         * @return the {@link java.time.Instant} with the offset of the {@code Duration} added to it
279         */
280        public static Instant getTimeOffset(Instant time, Duration offset) {
281                if (time == null || offset == null) {
282                        return null;
283                }
284                return time.plus(offset);
285        }
286        
287        /**
288         * A utility method that returns the time of an offset by degrees below or above the horizon of {@link #getSunrise() sunrise}.
289         * Note that the degree offset is from the vertical, so for a calculation of 14° before sunrise, an offset of 14
290         * + {@link #GEOMETRIC_ZENITH} = 104 would have to be passed as a parameter.
291         * 
292         * @param offsetZenith the degrees before {@link #getSunrise()} to use in the calculation. For time after sunrise use negative
293         *         numbers. Note that the degree offset is from the vertical, so for a calculation of 14° before sunrise, an offset
294         *         of 14 + {@link #GEOMETRIC_ZENITH} = 104 would have to be passed as a parameter.
295         * @return The {@link java.time.Instant} of the offset after (or before) {@link #getSunrise()}. If the calculation
296         *         can't be computed such as in the Arctic Circle where there is at least one day a year where the sun does
297         *         not rise, and one where it does not set, a {@code null} will be returned. See detailed explanation
298         *         on top of the page.
299         */
300        public Instant getSunriseOffsetByDegrees(double offsetZenith) {
301                double dawn = getUTCSunrise(offsetZenith);
302                return Double.isNaN(dawn) ? null : getInstantFromTime(dawn, SolarEvent.SUNRISE);
303        }
304
305        /**
306         * A utility method that returns the time of an offset by degrees below or above the horizon of {@link #getSunset()
307         * sunset}. Note that the degree offset is from the vertical, so for a calculation of 14° after sunset, an offset of 14 +
308         * {@link #GEOMETRIC_ZENITH} = 104 would have to be passed as a parameter.
309         * 
310         * @param offsetZenith the degrees after {@link #getSunset()} to use in the calculation. For time before sunset use negative
311         *         numbers. Note that the degree offset is from the vertical, so for a calculation of 14° after sunset, an offset
312         *         of 14 + {@link #GEOMETRIC_ZENITH} = 104 would have to be passed as a parameter.
313         * @return The {@link java.time.Instant} of the offset after (or before) {@link #getSunset()}. If the calculation
314         *         can't be computed such as in the Arctic Circle where there is at least one day a year where the sun does not rise, and
315         *         and one where it does not set, a {@code null} will be returned. See detailed explanation on top of the page.
316         */
317        public Instant getSunsetOffsetByDegrees(double offsetZenith) {
318                double sunset = getUTCSunset(offsetZenith);
319                return Double.isNaN(sunset) ? null : getInstantFromTime(sunset, SolarEvent.SUNSET);
320        }
321
322        /**
323         * Default constructor will set a default {@link GeoLocation#GeoLocation()}, a default {@link AstronomicalCalculator#getDefault()
324         * AstronomicalCalculator} and default the {@code LocalDate} to the current date.
325         */
326        public AstronomicalCalendar() {
327                this(new GeoLocation());
328        }
329
330        /**
331         * A constructor that takes in <a href="https://en.wikipedia.org/wiki/Geolocation">geolocation</a> information as a parameter.
332         * The default {@link AstronomicalCalculator#getDefault() AstronomicalCalculator} used for solar calculations is the more
333         * accurate {@link com.kosherjava.zmanim.util.NOAACalculator}.
334         *
335         * @param geoLocation The location information used for calculating astronomical solar times.
336         * @see setAstronomicalCalculator(AstronomicalCalculator) for changing the calculator class.
337         */
338        public AstronomicalCalendar(GeoLocation geoLocation) {
339                setLocalDate(LocalDate.now(geoLocation.getZoneId()));
340                setGeoLocation(geoLocation);
341                setAstronomicalCalculator(AstronomicalCalculator.getDefault());
342        }
343
344        /**
345         * A method that returns the sunrise in UTC time without correction for time zone offset from GMT and without using daylight
346         * savings time.
347         * 
348         * @param zenith the degrees below the horizon. For time after sunrise use negative numbers.
349         * @return The time in the format: 18.75 for 18:45:00 UTC/GMT. If the calculation can't be computed such as in the
350         *         Arctic Circle where there is at least one day a year where the sun does not rise, and one where it does
351         *         not set, {@link Double#NaN} will be returned. See detailed explanation on top of the page.
352         */
353        public double getUTCSunrise(double zenith) {
354                return getAstronomicalCalculator().getCachedUTCSunrise(getAdjustedLocalDate(), getGeoLocation(), zenith, true);
355        }
356
357        /**
358         * A method that returns the sunrise in UTC time without correction for time zone offset from GMT and without using
359         * daylight savings time. Non-sunrise and sunset calculations such as dawn and dusk, depend on the amount of visible
360         * light, something that is not affected by elevation. This method returns UTC sunrise calculated at sea level. This
361         * forms the base for dawn calculations that are calculated as a dip below the horizon before sunrise.
362         * 
363         * @param zenith the degrees below the horizon. For time after sunrise use negative numbers.
364         * @return The time in the format: 18.75 for 18:45:00 UTC/GMT. If the calculation can't be computed such as in the
365         *         Arctic Circle where there is at least one day a year where the sun does not rise, and one where it does
366         *         not set, {@link Double#NaN} will be returned. See detailed explanation on top of the page.
367         * @see #getUTCSunrise(double)
368         * @see #getUTCSeaLevelSunset(double)
369         */
370        public double getUTCSeaLevelSunrise(double zenith) {
371                return getAstronomicalCalculator().getCachedUTCSunrise(getAdjustedLocalDate(), getGeoLocation(), zenith, false);
372        }
373
374        /**
375         * A method that returns the sunset in UTC time without correction for time zone offset from GMT and without using
376         * daylight savings time.
377         * 
378         * @param zenith the degrees below the horizon. For time after sunset use negative numbers.
379         * @return The time in the format: 18.75 for 18:45:00 UTC/GMT. If the calculation can't be computed such as in the
380         *         Arctic Circle where there is at least one day a year where the sun does not rise, and one where it does
381         *         not set, {@link Double#NaN} will be returned. See detailed explanation on top of the page.
382         * @see #getUTCSeaLevelSunset(double)
383         */
384        public double getUTCSunset(double zenith) {
385                return getAstronomicalCalculator().getCachedUTCSunset(getAdjustedLocalDate(), getGeoLocation(), zenith, true);
386        }
387
388        /**
389         * A method that returns the sunset in UTC time without correction for elevation, time zone offset from GMT and without using
390         * daylight savings time. Non-sunrise and sunset calculations such as dawn and dusk, depend on the amount of visible light,
391         * something that is not affected by elevation. This method returns UTC sunset calculated at sea level. This forms the base for
392         * dusk calculations that are calculated as a dip below the horizon after sunset.
393         * 
394         * @param zenith the degrees below the horizon. For time before sunset use negative numbers.
395         * @return The time in the format: 18.75 for 18:45:00 UTC/GMT. If the calculation can't be computed such as in the
396         *         Arctic Circle where there is at least one day a year where the sun does not rise, and one where it does
397         *         not set, {@link Double#NaN} will be returned. See detailed explanation on top of the page.
398         * @see #getUTCSunset(double)
399         * @see #getUTCSeaLevelSunrise(double)
400         */
401        public double getUTCSeaLevelSunset(double zenith) {
402                return getAstronomicalCalculator().getCachedUTCSunset(getAdjustedLocalDate(), getGeoLocation(), zenith, false);
403        }
404
405        /**
406         * A method that returns a sea-level based temporal (solar) hour. The day from {@link #getSeaLevelSunrise() sea-level sunrise} to
407         * {@link #getSeaLevelSunset() sea-level sunset} is split into 12 equal parts with each one being a temporal hour.
408         * 
409         * @see #getSeaLevelSunrise()
410         * @see #getSeaLevelSunset()
411         * @see #getTemporalHour(Instant, Instant)
412         * @return the {@code Duration} of the temporal hour. If the calculation can't be computed a {@code null} will be
413         *         returned. See detailed explanation on top of the page.
414         */
415        public Duration getTemporalHour() {
416                return getTemporalHour(getSeaLevelSunrise(), getSeaLevelSunset());
417        }
418        
419        /**
420         * A utility method that will allow the calculation of a temporal (solar) hour based on the sunrise and sunset passed as
421         * parameters to this method. An example of the use of this method would be the calculation of a elevation adjusted temporal
422         * hour by passing in {@link #getSunrise() sunrise} and {@link #getSunset() sunset} as parameters.
423         * 
424         * @param startOfDay The start of the day.
425         * @param endOfDay The end of the day.
426         * @return the {@code Duration} of the temporal hour. If the calculation can't be computed a {@code null} will be
427         *         returned. See detailed explanation on top of the page.
428         * @see #getTemporalHour()
429         */
430        public Duration getTemporalHour(Instant startOfDay, Instant endOfDay) {
431                if (startOfDay == null || endOfDay == null) {
432                        return null;
433                }
434                
435                return Duration.between(startOfDay, endOfDay).dividedBy(12);
436        }
437
438        /**
439         * A method that returns sundial or solar noon. It occurs when the Sun is <a href=
440         * "https://en.wikipedia.org/wiki/Transit_%28astronomy%29">transiting</a> the <a
441         * href="https://en.wikipedia.org/wiki/Meridian_%28astronomy%29">celestial meridian</a>. The calculations used by this class
442         * depend on the {@link AstronomicalCalculator} used. If this calendar instance is {@link #setAstronomicalCalculator(
443         * AstronomicalCalculator)} is set to use the {@link com.kosherjava.zmanim.util.NOAACalculator} (the default) it will calculate
444         * astronomical noon. If the calendar instance is  to use the {@link com.kosherjava.zmanim.util.SunTimesCalculator}, that does
445         * not have code to calculate astronomical noon, the sun transit is calculated as halfway between sea level sunrise and sea level
446         * sunset, which can be slightly off the real transit time due to changes in declination (the lengthening or shortening day). See
447         * <a href="https://kosherjava.com/2020/07/02/definition-of-chatzos/">The Definition of Chatzos</a> for details on the proper
448         * definition of solar noon / midday.
449         * 
450         * @return the {@code Instant} representing Sun's transit. If the calculation can't be computed such as when using the {@link
451         *         com.kosherjava.zmanim.util.SunTimesCalculator USNO calculator} that does not support getting solar noon for the Arctic
452         *         Circle (where there is at least one day a year where the sun does not rise, and one where it does not set), a
453         *         {@code null} will be returned. See detailed explanation on top of the page.
454         * @see #getSunTransit(Instant, Instant)
455         * @see #getTemporalHour()
456         * @see com.kosherjava.zmanim.util.NOAACalculator#getUTCNoon(LocalDate, GeoLocation)
457         * @see com.kosherjava.zmanim.util.SunTimesCalculator#getUTCNoon(LocalDate, GeoLocation)
458         */
459        public Instant getSunTransit() {
460                double noon = getAstronomicalCalculator().getCachedUTCNoon(getAdjustedLocalDate(), getGeoLocation());
461                return getInstantFromTime(noon, SolarEvent.NOON);
462        }
463        
464        /**
465         * A method that returns solar midnight as the <b>end of the day</b> (that may actually be after midnight of the  day it is
466         * being calculated for). For example calculating solar midnight for February 8, will calculate it for midnight between February
467         * 8 and February 9. It occurs when the Sun is <a href="https://en.wikipedia.org/wiki/Transit_%28astronomy%29">transiting</a> the
468         * lower <a href="https://en.wikipedia.org/wiki/Meridian_%28astronomy%29">celestial meridian</a>, or when the sun is at it's
469         * <a href="https://en.wikipedia.org/wiki/Nadir">nadir</a>. The calculations used by this class depend on the {@link
470         * AstronomicalCalculator} used. If this calendar instance is {@link #setAstronomicalCalculator(AstronomicalCalculator) set} to
471         * use the {@link com.kosherjava.zmanim.util.NOAACalculator} (the default) it will calculate astronomical midnight. If the
472         * calendar instance is to use the {@link com.kosherjava.zmanim.util.SunTimesCalculator USNO Calculator}, that does not have code
473         * to calculate astronomical noon, midnight is calculated as 12 hours after halfway between sea level sunrise and sea level sunset
474         * of that day. This can be slightly off the real transit time due to changes in declination (the lengthening or shortening day).
475         * See <a href="https://kosherjava.com/2020/07/02/definition-of-chatzos/">The Definition of Chatzos</a> for details on the proper
476         * definition of solar noon / midday.
477         * 
478         * @return the {@code Instant} representing Sun's lower transit at the <b>end of the current day</b>. If the calculation
479         *         can't be computed such as when using the {@link com.kosherjava.zmanim.util.SunTimesCalculator USNO calculator} that does
480         *         not support getting solar noon or midnight for the Arctic Circle (where there is at least one day a year where the sun
481         *         does not rise, and one where it does not set), a {@code null} will be returned. This is not relevant when using the
482         *         {@link com.kosherjava.zmanim.util.NOAACalculator NOAA Calculator} that is never expected to return {@code null}.
483         *         See the detailed explanation on top of the page.
484         * @see #getSunTransit()
485         * @see com.kosherjava.zmanim.util.NOAACalculator#getUTCNoon(LocalDate, GeoLocation)
486         * @see com.kosherjava.zmanim.util.SunTimesCalculator#getUTCNoon(LocalDate, GeoLocation)
487         */
488        public Instant getSolarMidnight() {
489                double noon = getAstronomicalCalculator().getCachedUTCMidnight(getAdjustedLocalDate(), getGeoLocation());
490                return getInstantFromTime(noon, SolarEvent.MIDNIGHT);
491        }
492
493        /**
494         * A method that returns sundial or solar noon (or midnight) calculated as halfway between the times passed in. It is close to,
495         * but not exactly  occurs when the Sun is <a href="https://en.wikipedia.org/wiki/Transit_%28astronomy%29">transiting</a> the
496         * <a href="https://en.wikipedia.org/wiki/Meridian_%28astronomy%29">celestial meridian</a>. It will not exactly match the
497         * astronomical transit, due to changes in declination (the lengthening or shortening day).
498         * 
499         * @param startOfDay the start of day for calculating the sun's transit. This can be sea level sunrise, visual sunrise (or any
500         *         arbitrary start of day) passed to this method.
501         * @param endOfDay the end of day for calculating the sun's transit. This can be sea level sunset, visual sunset (or any arbitrary
502         *         end of day) passed to this method.
503         * @return the {@code Instant} representing Sun's transit. If the calculation can't be computed such as in the
504         *         Arctic Circle where there is at least one day a year where the sun does not rise, and one where it does
505         *         not set, {@code null} will be returned. See detailed explanation on top of the page.
506         */
507        public Instant getSunTransit(Instant startOfDay, Instant endOfDay) {
508                Duration temporalHour = getTemporalHour(startOfDay, endOfDay);
509                if (temporalHour == null) {
510                        return null;
511                }
512                return getTimeOffset(startOfDay, temporalHour.multipliedBy(6));
513        }
514
515        /**
516         * An {@code enum} to indicate what type of solar event is being calculated.
517         */
518        protected enum SolarEvent {
519                /**SUNRISE A solar event related to sunrise*/SUNRISE, /**SUNSET A solar event related to sunset*/SUNSET,
520                /**NOON A solar event related to noon*/NOON, /**MIDNIGHT A solar event related to midnight*/MIDNIGHT;
521        }
522        
523        /**
524         * Return the time at either an azimuth 90° (directly east) or 270° (directly west).
525         * 
526         * @param azimuth the azimuth that you want to get the time of day for.
527         * @return the time that the azimuth will be reached. There are cases where this azimuth will never be reached for the date and
528         *         location, and a null will be returned in that case.
529         * @see com.kosherjava.zmanim.util.AstronomicalCalculator#getTimeAtAzimuth(LocalDate, GeoLocation, double)
530         * @see com.kosherjava.zmanim.ComprehensiveZmanimCalendar#getPolarSunriseBenIshChai()
531         * @see com.kosherjava.zmanim.ComprehensiveZmanimCalendar#getPolarSunsetBenIshChai()
532         * @throws IllegalArgumentException if the azimuth is not 90° or 270°.
533         * @todo Once a reliable implementation to get any azimuth at any date for any latitude is implemented, make this method more
534         *         generic.
535         */
536        public Instant getTimeAtAzimuth90Or270(double azimuth) {
537                double rawAzimuth = getAstronomicalCalculator().getTimeAtAzimuth(getAdjustedLocalDate(), getGeoLocation(), azimuth);
538                return getInstantFromTime(rawAzimuth, (azimuth == 90) ? SolarEvent.SUNRISE : SolarEvent.SUNSET); 
539        }
540        
541        /**
542         * A method that returns an {@code Instant} from the {@code double} passed in as a parameter.
543         * 
544         * @param time The time to be set as the time for the {@code Instant}. The time expected is in the format: 18.75 for 6:45:00 PM. 
545         * @param solarEvent the type of {@link SolarEvent}
546         * @return The Instant object representation of the time double
547         */
548        protected Instant getInstantFromTime(double time, SolarEvent solarEvent) {
549                if (Double.isNaN(time)) {
550                        return null;
551                }
552                
553                LocalDate date = getAdjustedLocalDate();
554                double localTimeHours = (getGeoLocation().getLongitude() / 15) + time;
555                
556                // The calculator gives only the UTC time of day, a number from 0 to 24. It doesn't say which UTC day the event
557                // is on, so start from the calculation date and correct it.
558                //
559                // localTimeHours is that time in local mean time, which is UTC plus the longitude's hours. It can come out below 0 or
560                // above 24, which means the event is really on the day before or the day after the date. Each event has a
561                // window of local mean time that belongs to the date, and these checks move the date by a day when the event
562                // falls outside it.
563                //
564                // - Sunrise belongs to the date from 18:00 the evening before through 18:00 of the date. After 18:00 it is
565                //   really the evening before (an Arctic sunrise just after midnight, say), so go back a day. At -6:00 or
566                //   earlier the UTC time wrapped around midnight, so go forward a day. For example, just west of the date line
567                //   local mean time is about 12 hours behind UTC. A sunrise at noon local mean time is then 24:00 UTC, which
568                //   arrives as 0:00 and reads as -12:00 local mean time.
569                // - Sunset belongs from 06:00 of the date through 06:00 the next morning. Before 06:00 go forward a day. At
570                //   30:00, which is 06:00 the next morning, or later the UTC time wrapped the other way, so go back a day.
571                // - Solar midnight is the one after the date's noon, so one before 12:00 moves forward a day.
572                // - Solar noon belongs between 0:00 and 24:00. Before 0:00 go forward a day, and after 24:00 go back a day.
573                if (solarEvent == SolarEvent.SUNRISE && localTimeHours > 18) {
574                        date = date.minusDays(1);
575                } else if (solarEvent == SolarEvent.SUNRISE && localTimeHours <= -6) {
576                        date = date.plusDays(1);
577                } else if (solarEvent == SolarEvent.SUNSET && localTimeHours < 6) {
578                        date = date.plusDays(1);
579                } else if (solarEvent == SolarEvent.SUNSET && localTimeHours >= 30) {
580                        date = date.minusDays(1);
581                } else if (solarEvent == SolarEvent.MIDNIGHT && localTimeHours < 12) {
582                        date = date.plusDays(1);
583                } else if (solarEvent == SolarEvent.NOON) {
584                        if (localTimeHours < 0) {
585                                date = date.plusDays(1);
586                        } else if (localTimeHours > 24) {
587                                date = date.minusDays(1);
588                        }
589                }
590                
591                LocalDateTime dateTime = date.atStartOfDay().plusNanos(Math.round(time * HOUR_NANOS));
592                
593                // The computed time is in UTC fractional hours; anchor in UTC before converting.
594                return ZonedDateTime.of(dateTime, ZoneOffset.UTC).toInstant();
595        }
596
597        /**
598         * Returns the sun's elevation (number of degrees) below the horizon before sunrise that matches the offset minutes
599         * on passed in as a parameter. For example passing in 72 minutes for a calendar set to the equinox in Jerusalem
600         * returns a value close to 16.1°.
601         * 
602         * @param minutes minutes before sunrise
603         * @return the degrees below the horizon before {@link #getSeaLevelSunrise()} that match the offset in minutes passed in as a
604         *         parameter. If the calculation can't be computed (no sunrise occurs on this day) a {@link Double#NaN} will be returned.
605         * @deprecated This method is slow and inefficient and should NEVER be used in a loop. This method should be replaced by calls to
606         *         {@link AstronomicalCalculator#getSolarElevation(Instant, GeoLocation)}. That method will efficiently return the solar
607         *         elevation (the sun's position in degrees below (or above) the horizon) at the given time even in the arctic when there
608         *         is no sunrise.
609         * @see AstronomicalCalculator#getSolarElevation(Instant, GeoLocation)
610         * @see #getSunsetSolarDipFromOffset(double)
611         */
612        @Deprecated(forRemoval=false)
613        public double getSunriseSolarDipFromOffset(double minutes) {
614                if (minutes == 0.0) {
615                        return 0.0;
616                }
617                if (Double.isNaN(minutes)) {
618                        return Double.NaN;
619                }
620                
621                Instant seaLevelSunrise = getSeaLevelSunrise();
622                if (seaLevelSunrise == null) {
623                        return Double.NaN;
624                }
625
626                Duration offsetDuration = Duration.ofNanos((long) (-minutes * MINUTE_NANOS));
627                Instant offsetByTime = getTimeOffset(seaLevelSunrise, offsetDuration);
628                long offsetByTimeMilli = offsetByTime.toEpochMilli();
629                double degrees = 0.0;
630                double incrementor = 0.0001;
631                Instant offsetByDegrees;
632
633                do {
634                        if (minutes > 0.0) {
635                                degrees += incrementor;
636                        } else {
637                                degrees -= incrementor;
638                        }
639
640                        offsetByDegrees = getSunriseOffsetByDegrees(GEOMETRIC_ZENITH + degrees);
641
642                        if (offsetByDegrees == null || Math.abs(degrees) > 30.0) {
643                                return Double.NaN;
644                        }
645                        
646                } while ((minutes > 0.0 && offsetByDegrees.toEpochMilli() > offsetByTimeMilli) ||
647                                (minutes < 0.0 && offsetByDegrees.toEpochMilli() < offsetByTimeMilli));
648
649                return degrees;
650        }
651
652        /**
653         * Returns the sun's elevation (number of degrees) below the horizon after sunset that matches the offset minutes
654         * passed in as a parameter. For example passing in 72 minutes for a date set to the equinox in Jerusalem
655         * returns a value close to 16.1°.
656         * 
657         * @param minutes minutes after sunset
658         * @return the degrees below the horizon after sunset that match the offset in minutes passed it as a parameter. If the
659         *         calculation can't be computed (no sunset occurs on this day) a {@link Double#NaN} will be returned.
660         * @deprecated This method is slow and inefficient and should NEVER be used in a loop. This method should be replaced by calls to
661         *         {@link AstronomicalCalculator#getSolarElevation(Instant, GeoLocation)}. That method will efficiently return the solar
662         *         elevation (the sun's position in degrees below (or above) the horizon) at the given time even in the arctic when there
663         *         is no sunrise.
664         * @see AstronomicalCalculator#getSolarElevation(Instant, GeoLocation)
665         * @see #getSunriseSolarDipFromOffset(double)
666         */
667        @Deprecated(forRemoval=false)
668        public double getSunsetSolarDipFromOffset(double minutes) {
669                if (minutes == 0.0) {
670                        return 0.0;
671                }
672                if (Double.isNaN(minutes)) {
673                        return Double.NaN;
674                }
675                
676                Instant seaLevelSunset = getSeaLevelSunset();
677                if (seaLevelSunset == null) {
678                        return Double.NaN;
679                }
680
681                Duration offsetDuration = Duration.ofNanos((long) (minutes * MINUTE_NANOS));
682                Instant offsetByTime = getTimeOffset(seaLevelSunset, offsetDuration);
683                long offsetByTimeMilli = offsetByTime.toEpochMilli();
684                double degrees = 0.0;
685                double incrementor = 0.0001;
686                Instant offsetByDegrees;
687
688                do {
689                        if (minutes > 0.0) {
690                                degrees += incrementor;
691                        } else {
692                                degrees -= incrementor;
693                        }
694
695                        offsetByDegrees = getSunsetOffsetByDegrees(GEOMETRIC_ZENITH + degrees);
696
697                        if (offsetByDegrees == null || Math.abs(degrees) > 30.0) {
698                                return Double.NaN;
699                        }
700
701                } while ((minutes > 0.0 && offsetByDegrees.toEpochMilli() < offsetByTimeMilli) ||
702                                (minutes < 0.0 && offsetByDegrees.toEpochMilli() > offsetByTimeMilli));
703
704                return degrees;
705        }
706
707        /**
708         * A method that returns <a href="https://en.wikipedia.org/wiki/Local_mean_time">local mean time (LMT)</a> for a {@code LocalTime}
709         * passed to this method. This time is adjusted from standard time to account for the local latitude. The 360° of the globe
710         * divided by 24 calculates to 15° per hour with 4 minutes per degree, so at a longitude of 0 , 15, 30 etc... noon is at exactly
711         * 12:00 PM. Lakewood, NJ, with a longitude of -74.222, is 0.7906 away from the closest multiple of 15 at -75°. This is
712         * multiplied by 4 clock minutes (per degree) to yield 3 minutes and 7 seconds for a noon time of 11:56:53 AM. This method is not
713         * tied to the theoretical 15° time zones, but will adjust to the actual time zone and <a href=
714         * "https://en.wikipedia.org/wiki/Daylight_saving_time">Daylight saving time</a> to return LMT.
715         * 
716         * @param localTime the local wall-clock time (such as 12:00 for noon and 00:00 for midnight) to calculate as LMT.
717         * @return the {@code Instant} representing the local mean time (LMT) for the time passed in. In Lakewood, NJ, passing noon
718         *         will return 11:56:50 AM.
719         * @see GeoLocation#getLocalMeanTimeOffset(Instant)
720         */
721        public Instant getLocalMeanTime(LocalTime localTime) {
722                Instant localMeanTime = LocalDateTime.of(getAdjustedLocalDate(), localTime).toInstant(ZoneOffset.UTC);
723                long totalNanos = (long) (getGeoLocation().getLongitude() * 4 * MINUTE_NANOS);
724                return getTimeOffset(localMeanTime, Duration.ofNanos(-totalNanos));
725        }
726
727        /**
728         * Adjusts the {@code LocalDate} to deal with edge cases where the location crosses the antimeridian.
729         * 
730         * @see GeoLocation#getAntimeridianAdjustment(Instant)
731         * @return the adjusted {@code LocalDate}
732         */
733        protected LocalDate getAdjustedLocalDate(){
734                int offset = getGeoLocation().getAntimeridianAdjustment(getMidnightLastNight().toInstant());
735                return offset == 0 ? getLocalDate() : getLocalDate().plusDays(offset);
736        }
737
738        /**
739         * Used by Molad based <em>zmanim</em> to determine if <em>zmanim</em> occur during the current day. This is also used as the
740         * anchor for current timezone-offset calculations.
741         * @return midnight at the start of the current local date in the configured {@link GeoLocation#getZoneId()}.
742         */
743        protected ZonedDateTime getMidnightLastNight() {
744                return ZonedDateTime.of(getLocalDate(),LocalTime.MIDNIGHT,getGeoLocation().getZoneId());
745        }
746
747        /**
748         * Used by Molad based <em>zmanim</em> to determine if <em>zmanim</em> occur during the current day.
749         * @return following midnight
750         */
751        protected ZonedDateTime getMidnightTonight() {
752                return ZonedDateTime.of(getLocalDate().plusDays(1),LocalTime.MIDNIGHT,getGeoLocation().getZoneId());
753        }
754
755        /**
756         * Returns an XML formatted representation of the class using the default output of the
757         *         {@link com.kosherjava.zmanim.util.ZmanimFormatter#toXML(AstronomicalCalendar) toXML} method.
758         * @return an XML formatted representation of the class. It returns the default output of the
759         *         {@link com.kosherjava.zmanim.util.ZmanimFormatter#toXML(AstronomicalCalendar) toXML} method.
760         * @see com.kosherjava.zmanim.util.ZmanimFormatter#toXML(AstronomicalCalendar)
761         * @see java.lang.Object#toString()
762         */
763        public String toString() {
764                return ZmanimFormatter.toXML(this);
765        }
766        
767        /**
768         * Returns a JSON formatted representation of the class using the default output of the
769         *         {@link com.kosherjava.zmanim.util.ZmanimFormatter#toJSON(AstronomicalCalendar) toJSON} method.
770         * @return a JSON formatted representation of the class. It returns the default output of the
771         *         {@link com.kosherjava.zmanim.util.ZmanimFormatter#toJSON(AstronomicalCalendar) toJSON} method.
772         * @see com.kosherjava.zmanim.util.ZmanimFormatter#toJSON(AstronomicalCalendar)
773         * @see java.lang.Object#toString()
774         */
775        public String toJSON() {
776                return ZmanimFormatter.toJSON(this);
777        }
778
779        /**
780         * {@inheritDoc}
781         * <p>
782         * Two {@code AstronomicalCalendar} instances are considered equal if their {@link #getLocalDate()}, {@link #getGeoLocation()}
783         * and {@link #getAstronomicalCalculator()} values are identical.
784         * 
785         * @param object the reference object with which to compare
786         * @return {@inheritDoc}
787         */
788        @Override
789        public boolean equals(Object object) {
790                if (this == object) {
791                        return true;
792                }
793                if (object == null || getClass() != object.getClass()) {
794                        return false;
795                }
796                AstronomicalCalendar aCal = (AstronomicalCalendar) object;
797                return Objects.equals(getLocalDate(), aCal.getLocalDate())
798                                && Objects.equals(getGeoLocation(), aCal.getGeoLocation())
799                                && Objects.equals(getAstronomicalCalculator(), aCal.getAstronomicalCalculator());
800        }
801
802        /**
803         * {@inheritDoc}
804         * <p>
805         * This implementation hashes the {@code Class}, {@linkplain #getLocalDate()}, {@link #getGeoLocation()} and
806         * {@link #getAstronomicalCalculator()} properties to maintain the contract with {@link #equals(Object)}.
807         * 
808         * @return {@inheritDoc}
809         */
810        @Override
811        public int hashCode() {
812                return Objects.hash(getClass(), getLocalDate(), getGeoLocation(), getAstronomicalCalculator());
813        }
814
815        /**
816         * A method that returns the currently set {@link GeoLocation} which contains location information used for the
817         * astronomical calculations.
818         * 
819         * @return Returns the geoLocation.
820         */
821        public GeoLocation getGeoLocation() {
822                return this.geoLocation;
823        }
824
825        /**
826         * Sets the {@link GeoLocation} {@code Object} to be used for astronomical calculations.
827         * 
828         * @param geoLocation The geoLocation to set.
829         * @todo Possibly adjust for horizon elevation. It may be smart to just have the calculator check the GeoLocation
830         *       though it doesn't really belong there.
831         */
832        public void setGeoLocation(GeoLocation geoLocation) {
833                this.geoLocation = geoLocation;
834        }
835
836        /**
837         * A method that returns the currently set AstronomicalCalculator.
838         * 
839         * @return Returns the astronomicalCalculator.
840         * @see #setAstronomicalCalculator(AstronomicalCalculator)
841         */
842        public AstronomicalCalculator getAstronomicalCalculator() {
843                return this.astronomicalCalculator;
844        }
845
846        /**
847         * A method to set the {@link AstronomicalCalculator} used for astronomical calculations. The Zmanim package ships
848         * with a number of different implementations of the {@code abstract} {@link AstronomicalCalculator} based on
849         * different algorithms, including the default {@link com.kosherjava.zmanim.util.NOAACalculator} based on <a href=
850         * "https://noaa.gov">NOAA's</a> implementation of Jean Meeus's algorithms as well as {@link
851         * com.kosherjava.zmanim.util.SunTimesCalculator} based on the <a href = "https://www.cnmoc.usff.navy.mil/usno/">US
852         * Naval Observatory's</a> algorithm. This allows easy runtime switching and comparison of different algorithms.
853         * 
854         * @param astronomicalCalculator The {@code AstronomicalCalculator} to set.
855         */
856        public void setAstronomicalCalculator(AstronomicalCalculator astronomicalCalculator) {
857                this.astronomicalCalculator = astronomicalCalculator;
858        }
859        
860        /**
861         * returns the {@code LocalDate} object encapsulated in this class.
862         * 
863         * @return Returns the {@code LocalDate}.
864         */
865        public LocalDate getLocalDate() {
866                return this.localDate;
867        }
868        
869        /**
870         * Sets the {@code LocalDate}  object for use in this class.
871         * @param localDate The {@code LocalDate} to set.
872         */
873        public void setLocalDate(LocalDate localDate) {
874                this.localDate = localDate;
875        }
876
877        /**
878         * {@inheritDoc}
879         * 
880         * @return {@inheritDoc}
881         */
882        @Override
883        public Object clone() {
884                AstronomicalCalendar clone = null;
885                try {
886                        clone = (AstronomicalCalendar) super.clone();
887                } catch (CloneNotSupportedException cnse) {
888                        throw new AssertionError("Clone not supported on a Cloneable object", cnse);
889                }
890
891                clone.setGeoLocation((GeoLocation) getGeoLocation().clone()); // consider converting the GeoLocation class to be immutable to avoid the deep copy
892                clone.setAstronomicalCalculator((AstronomicalCalculator) getAstronomicalCalculator().clone()); // likely not needed
893
894                return clone;
895        }
896}