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