001/*
002 * Zmanim Java API
003 * Copyright (C) 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
033 * getSunset() sunset} and twilight times. This class contains a {@link getLocalDate() LocalDate} and can therefore
034 * use the standard Calendar functionality to change dates etc. The calculation engine used to calculate the astronomical times can
035 * be changed to a different implementation by implementing the abstract {@link AstronomicalCalculator} and setting it withthe {@link
036 * setAstronomicalCalculator(AstronomicalCalculator)}. A number of different calculation engine implementations are included in the
037 * 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 twilight with a deep dip below the horizon for locations as far south of the North Pole as London,
041 * in the northern hemisphere. The sun never reaches this dip at certain times of the year. When the calculations encounter this
042 * condition a {@code null} will be returned when a {@link java.time.Instant} or {@link java.time.Duration} is expected. The reason
043 * that {@code Exception}s are not thrown in these cases is because the lack of a rise/set or twilight is not an exception, but
044 * an expected condition in many parts of the world.
045 * <p>
046 * Here is a simple example of how to use the API to calculate sunrise.
047 * First create the Calendar for the location you would like to calculate sunrise or sunset times for:
048 * 
049 * {@snippet lang='java' :
050 * String locationName = &quot;Lakewood, NJ&quot;;
051 * double latitude = 40.0828; // Lakewood, NJ
052 * double longitude = -74.2094; // Lakewood, NJ
053 * double elevation = 20; // optional elevation correction in Meters
054 * // @link region="target_zone_link" substring="getAvailableZoneIds()" target="java.time.ZoneId#getAvailableZoneIds()"
055 * ZoneId zoneId = ZoneId.of("America/New_York"); // set the zoneId to a valid ZoneId listed in getAvailableZoneIds()
056 * // @end
057 * GeoLocation location = new GeoLocation(locationName, latitude, longitude, elevation, zoneId);
058 * AstronomicalCalendar ac = new AstronomicalCalendar(location);
059 * }
060 * 
061 * To get the time of sunrise, first set the date you want (if not set, the date will default to today):
062 * 
063 * {@snippet lang='java' :
064 * LocalDate localDate = LocalDate.of(1969, Month.FEBRUARY, 8);
065 * ac.setLocalDate(localDate);
066 * Instant sunrise = ac.getSunrise();
067 * }
068 * 
069 * @author © Eliyahu Hershfeld 2004 - 2026
070 */
071public class AstronomicalCalendar implements Cloneable {
072
073        /**
074         * 90° below the vertical. Used as a basis for most calculations since the location of the sun is 90° below the horizon
075         * at sunrise and sunset.
076         * <b>Note </b>: it is important to note that for sunrise and sunset the {@link AstronomicalCalculator#adjustZenith(double,
077         * double) adjusted zenith} is required to account for the radius of the sun and refraction. The adjusted zenith should not be
078         * used for calculations above or below 90° since they are usually calculated as an offset to 90°.
079         */
080        public static final double GEOMETRIC_ZENITH = 90;
081
082        /** Sun's zenith at civil twilight (96°). */
083        public static final double CIVIL_ZENITH = 96;
084
085        /** Sun's zenith at nautical twilight (102°). */
086        public static final double NAUTICAL_ZENITH = 102;
087
088        /** Sun's zenith at astronomical twilight (108°). */
089        public static final double ASTRONOMICAL_ZENITH = 108;
090
091        /** constant for nanoseconds in a minute (60 billion / 60,000,000,000) */
092        public static final long MINUTE_NANOS = 60_000_000_000L;
093
094        /** constant for nanoseconds in an hour (3.6 trillion / 3,600,000,000,000) */
095        public static final long HOUR_NANOS = 3_600_000_000_000L;
096        
097        /**
098         * The {@code LocalDate} encapsulated by this class to track the current date used by the class
099         */
100        private LocalDate localDate;
101
102        /**
103         * the {@link GeoLocation} used for calculations.
104         */
105        private GeoLocation geoLocation;
106
107        /**
108         * the internal {@link AstronomicalCalculator} used for calculating solar based times.
109         */
110        private AstronomicalCalculator astronomicalCalculator;
111
112        /**
113         * The getSunrise method returns a {@code Instant} representing the {@link AstronomicalCalculator
114         * #getElevationAdjustment(double) elevation adjusted} sunrise time. The zenith used for the calculation uses {@link
115         * #GEOMETRIC_ZENITH geometric zenith} of 90° plus {@link AstronomicalCalculator#getElevationAdjustment(double)}. This is
116         * adjusted by the {@link AstronomicalCalculator} to add approximately 50/60 of a degree to account for 34 archminutes of
117         * refraction and 16 archminutes for the sun's radius for a total of {@link AstronomicalCalculator#adjustZenith 90.83333°}.
118         * See documentation for the specific implementation of the {@link AstronomicalCalculator} that you are using.
119         * 
120         * @return the {@code Instant} representing the exact sunrise time. If the calculation can't be computed such as in the
121         *         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
122         *         {@code null} will be returned. See detailed explanation on top of the page.
123         * @see AstronomicalCalculator#adjustZenith(double, double)
124         * @see getSeaLevelSunrise()
125         * @see getUTCSunrise(double)
126         */
127        public Instant getSunrise() {
128                double sunrise = getUTCSunrise(GEOMETRIC_ZENITH);
129                if (Double.isNaN(sunrise)) {
130                        return null;
131                } else {
132                        return getInstantFromTime(sunrise, SolarEvent.SUNRISE);
133                }
134        }
135
136        /**
137         * A method that returns the sunrise without {@link AstronomicalCalculator#getElevationAdjustment(double) elevation
138         * adjustment}. Non-sunrise and sunset calculations such as dawn and dusk, depend on the amount of visible light,
139         * something that is not affected by elevation. This method returns sunrise calculated at sea level. This forms the
140         * base for dawn calculations that are calculated as a dip below the horizon before sunrise.
141         * 
142         * @return the {@code Instant} representing the exact sea-level sunrise time. If the calculation can't be computed
143         *         such as in the Arctic Circle where there is at least one day a year where the sun does not rise, and one
144         *         where it does not set, a {@code null} will be returned. See detailed explanation on top of the page.
145         * @see getSunrise()
146         * @see getUTCSeaLevelSunrise(double)
147         * @see getSeaLevelSunset()
148         */
149        public Instant getSeaLevelSunrise() {
150                double sunrise = getUTCSeaLevelSunrise(GEOMETRIC_ZENITH);
151                if (Double.isNaN(sunrise)) {
152                        return null;
153                } else {
154                        return getInstantFromTime(sunrise, SolarEvent.SUNRISE);
155                }
156        }
157
158        /**
159         * A method that returns the beginning of <a href="https://en.wikipedia.org/wiki/Twilight#Civil_twilight">civil twilight</a>
160         * (dawn) using a zenith of {@link #CIVIL_ZENITH 96°}.
161         * 
162         * @return The {@code Instant} of the beginning of civil twilight using a zenith of 96°. If the calculation
163         *         can't be computed, {@code null} will be returned. See detailed explanation on top of the page.
164         */
165        public Instant getBeginCivilTwilight() {
166                return getSunriseOffsetByDegrees(CIVIL_ZENITH);
167        }
168
169        /**
170         * A method that returns the beginning of <a href="https://en.wikipedia.org/wiki/Twilight#Nautical_twilight">nautical twilight</a>
171         * using a zenith of {@link #NAUTICAL_ZENITH 102°}.
172         * 
173         * @return The {@code Instant} of the beginning of nautical twilight using a zenith of 102°. If the calculation
174         *         can't be computed {@code null} will be returned. See detailed explanation on top of the page.
175         */
176        public Instant getBeginNauticalTwilight() {
177                return getSunriseOffsetByDegrees(NAUTICAL_ZENITH);
178        }
179
180        /**
181         * A method that returns the beginning of <a href="https://en.wikipedia.org/wiki/Twilight#Astronomical_twilight">astronomical
182         * twilight</a> using a zenith of {@link #ASTRONOMICAL_ZENITH 108°}.
183         * 
184         * @return The {@code Instant} of the beginning of astronomical twilight using a zenith of 108°. If the calculation
185         *         can't be computed, {@code null} will be returned. See detailed explanation on top of the page.
186         */
187        public Instant getBeginAstronomicalTwilight() {
188                return getSunriseOffsetByDegrees(ASTRONOMICAL_ZENITH);
189        }
190
191        /**
192         * The getSunset method returns an {@code Instant} representing the
193         * {@link AstronomicalCalculator#getElevationAdjustment(double) elevation adjusted} sunset time. The zenith used for the
194         * calculation uses {@link #GEOMETRIC_ZENITH geometric zenith} of 90° plus {@link AstronomicalCalculator
195         * #getElevationAdjustment(double)}. This is adjusted by the {@link AstronomicalCalculator} to add approximately 50/60 of a
196         * degree to account for 34 archminutes of refraction and 16 archminutes for the sun's radius for a total of {@link
197         * AstronomicalCalculator#adjustZenith(double, double) 90.83333°}. See documentation for the specific implementation of the
198         * {@link AstronomicalCalculator} that you are using.
199         * Note: In certain cases the calculates sunset will occur before sunrise. This will typically happen when a time zone other than
200         * the local timezone is used (calculating Los Angeles sunset using a GMT time zone for example). In this case the sunset date
201         * will be incremented to the following date.
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)
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 sun 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) 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(Calendar, GeoLocation)
458         * @see com.kosherjava.zmanim.util.SunTimesCalculator#getUTCNoon(Calendar, 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 use
472         * the {@link com.kosherjava.zmanim.util.NOAACalculator} (the default) it will calculate astronomical midnight. If the calendar
473         * instance is to use the {@link com.kosherjava.zmanim.util.SunTimesCalculator USNO Calculator}, that does not have code to
474         * 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(Calendar, GeoLocation)
487         * @see com.kosherjava.zmanim.util.SunTimesCalculator#getUTCNoon(Calendar, 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 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                /**NONE solar event representing azimuth or elevation calculations that that can be any time of the day*/ NONE;
523        }
524        
525        /**
526         * Return the time at a given azimuth. This often will not occur and a null will be returned.
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         */
532        public Instant getTimeAtAzimuth(double azimuth) {
533                double rawAzimuth = getAstronomicalCalculator().getTimeAtAzimuth(getAdjustedLocalDate(), getGeoLocation(), azimuth);
534                // double rawAzimuth = getAstronomicalCalculator().getUTCAzimuthMorning(getAdjustedLocalDate(), getGeoLocation());
535                return getInstantFromTime(rawAzimuth, SolarEvent.NONE);
536        }
537        
538        /**
539         * A method that returns an {@code Instant} from the time passed in as a parameter.
540         * 
541         * @param time The time to be set as the time for the {@code Instant}. The time expected is in the format: 18.75 for
542         *         6:45:00 PM.time is sunrise and false if it is sunset
543         * @param solarEvent the type of {@link SolarEvent}
544         * @return The Instant object representation of the time double
545         */
546        protected Instant getInstantFromTime(double time, SolarEvent solarEvent) {
547                if (Double.isNaN(time)) {
548                        return null;
549                }
550                
551                LocalDate date = getAdjustedLocalDate();
552                double localTimeHours = (getGeoLocation().getLongitude() / 15) + time;
553                
554                if (solarEvent == SolarEvent.SUNRISE && localTimeHours > 18) {
555                        date = date.minusDays(1);
556                } else if (solarEvent == SolarEvent.SUNSET && localTimeHours < 6) {
557                        date = date.plusDays(1);
558                } else if (solarEvent == SolarEvent.MIDNIGHT && localTimeHours < 12) {
559                        date = date.plusDays(1);
560                } else if (solarEvent == SolarEvent.NOON) {
561                        if (localTimeHours < 0) {
562                                date = date.plusDays(1);
563                        } else if (localTimeHours > 24) {
564                                date = date.minusDays(1);
565                        }
566                }
567                
568                LocalDateTime dateTime = date.atStartOfDay().plusNanos(Math.round(time * HOUR_NANOS));
569                
570                // The computed time is in UTC fractional hours; anchor in UTC before converting.
571                return ZonedDateTime.of(dateTime, ZoneOffset.UTC).toInstant();
572        }
573
574        /**
575         * Returns the sun's elevation (number of degrees) below the horizon before sunrise that matches the offset minutes
576         * on passed in as a parameter. For example passing in 72 minutes for a calendar set to the equinox in Jerusalem
577         * returns a value close to 16.1°.
578         * 
579         * @param minutes minutes before sunrise
580         * @return the degrees below the horizon before sunrise that match the offset in minutes passed it as a parameter. If
581         *         the calculation can't be computed (no sunrise occurs on this day) a {@link Double#NaN} will be returned.
582         * @deprecated This method is slow and inefficient and should NEVER be used in a loop. This method should be replaced by calls to
583         *         {@link AstronomicalCalculator#getSolarElevation(Calendar, GeoLocation)}. That method will efficiently return the the
584         *         solar elevation (the sun's position in degrees below (or above) the horizon) at the given time even in the arctic when
585         *         there is no sunrise.
586         * @see AstronomicalCalculator#getSolarElevation(Calendar, GeoLocation)
587         * @see getSunsetSolarDipFromOffset(double)
588         */
589        @Deprecated(forRemoval=false)
590        public double getSunriseSolarDipFromOffset(double minutes) {
591                Instant seaLevelSunrise = getSeaLevelSunrise();
592                if (seaLevelSunrise == null) {
593                        return Double.NaN;
594                }
595
596                Duration offsetDuration = Duration.ofNanos((long) (-minutes * MINUTE_NANOS));
597                Instant offsetByTime = getTimeOffset(seaLevelSunrise, offsetDuration);
598                long offsetByTimeMilli = offsetByTime.toEpochMilli();
599                double degrees = 0.0;
600                double incrementor = 0.0001;
601                Instant offsetByDegrees;
602
603                do {
604                        if (minutes > 0.0) {
605                                degrees += incrementor;
606                        } else {
607                                degrees -= incrementor;
608                        }
609
610                        offsetByDegrees = getSunriseOffsetByDegrees(GEOMETRIC_ZENITH + degrees);
611
612                        if (offsetByDegrees == null || Math.abs(degrees) > 30.0) {
613                                return Double.NaN;
614                        }
615                        
616                } while ((minutes > 0.0 && offsetByDegrees.toEpochMilli() > offsetByTimeMilli) ||
617                                (minutes < 0.0 && offsetByDegrees.toEpochMilli() < offsetByTimeMilli));
618
619                return degrees;
620        }
621
622        /**
623         * Returns the sun's elevation (number of degrees) below the horizon after sunset that matches the offset minutes
624         * passed in as a parameter. For example passing in 72 minutes for a calendar set to the equinox in Jerusalem
625         * returns a value close to 16.1°.
626         * 
627         * @param minutes minutes after sunset
628         * @return the degrees below the horizon after sunset that match the offset in minutes passed it as a parameter. If the
629         *         calculation can't be computed (no sunset occurs on this day) a {@link Double#NaN} will be returned.
630         * @deprecated This method is slow and inefficient and should NEVER be used in a loop. This method should be replaced by calls to
631         *         {@link AstronomicalCalculator#getSolarElevation(Instant, GeoLocation)}. That method will efficiently return the the
632         *         solar elevation (the sun's position in degrees below (or above) the horizon) at the given time even in the arctic when
633         *         there is no sunrise.
634         * @see AstronomicalCalculator#getSolarElevation(Instant, GeoLocation)
635         * @see getSunriseSolarDipFromOffset(double)
636         */
637        @Deprecated(forRemoval=false)
638        public double getSunsetSolarDipFromOffset(double minutes) {
639                Instant seaLevelSunset = getSeaLevelSunset();
640                if (seaLevelSunset == null) {
641                        return Double.NaN;
642                }
643
644                Duration offsetDuration = Duration.ofNanos((long) (minutes * MINUTE_NANOS));
645                Instant offsetByTime = getTimeOffset(seaLevelSunset, offsetDuration);
646                long offsetByTimeMilli = offsetByTime.toEpochMilli();
647                double degrees = 0.0;
648                double incrementor = 0.0001;
649                Instant offsetByDegrees;
650
651                do {
652                        if (minutes > 0.0) {
653                                degrees += incrementor;
654                        } else {
655                                degrees -= incrementor;
656                        }
657
658                        offsetByDegrees = getSunsetOffsetByDegrees(GEOMETRIC_ZENITH + degrees);
659
660                        if (offsetByDegrees == null || Math.abs(degrees) > 30.0) {
661                                return Double.NaN;
662                        }
663
664                } while ((minutes > 0.0 && offsetByDegrees.toEpochMilli() < offsetByTimeMilli) ||
665                                (minutes < 0.0 && offsetByDegrees.toEpochMilli() > offsetByTimeMilli));
666
667                return degrees;
668        }
669
670        /**
671         * A method that returns <a href="https://en.wikipedia.org/wiki/Local_mean_time">local mean time (LMT)</a> time converted to
672         * regular clock time for the local wall-clock time passed to this method. This time is adjusted from standard time to account for
673         * the local latitude. The 360° of the globe divided by 24 calculates to 15° per hour with 4 minutes per degree, so at a
674         * longitude of 0 , 15, 30 etc... noon is at exactly 12:00pm. Lakewood, N.J., with a longitude of -74.222, is 0.7906 away from the
675         * closest multiple of 15 at -75°. This is multiplied by 4 clock minutes (per degree) to yield 3 minutes and 7 seconds for a
676         * noon time of 11:56:53am. This method is not tied to the theoretical 15° time zones, but will adjust to the actual time zone
677         * and <a href="https://en.wikipedia.org/wiki/Daylight_saving_time">Daylight saving time</a> to return LMT.
678         * 
679         * @param localTime the local wall-clock time (such as 12:00 for noon and 00:00 for midnight) to calculate as LMT.
680         * @return the {@code Instant} representing the local mean time (LMT) for the time passed in. In Lakewood, NJ, passing noon
681         *         will return 11:56:50am.
682         * @see GeoLocation#getLocalMeanTimeOffset(Instant)
683         */
684        public Instant getLocalMeanTime(LocalTime localTime) {
685                Instant localMeanTime = LocalDateTime.of(getAdjustedLocalDate(), localTime).toInstant(ZoneOffset.UTC);
686                long totalNanos = (long) (getGeoLocation().getLongitude() * 4 * MINUTE_NANOS);
687                return getTimeOffset(localMeanTime, Duration.ofNanos(-totalNanos));
688        }
689
690        /**
691         * Adjusts the {@code LocalDate} to deal with edge cases where the location crosses the antimeridian.
692         * 
693         * @see GeoLocation#getAntimeridianAdjustment(Instant)
694         * @return the adjusted Calendar
695         */
696        protected LocalDate getAdjustedLocalDate(){
697                int offset = getGeoLocation().getAntimeridianAdjustment(getMidnightLastNight().toInstant());
698                return offset == 0 ? getLocalDate() : getLocalDate().plusDays(offset);
699        }
700
701        /**
702         * Used by Molad based <em>zmanim</em> to determine if <em>zmanim</em> occur during the current day. This is also used as the
703         * anchor for current timezone-offset calculations.
704         * @return midnight at the start of the current local date in the configured {@link GeoLocation#getZoneId()}.
705         */
706        protected ZonedDateTime getMidnightLastNight() {
707                return ZonedDateTime.of(getLocalDate(),LocalTime.MIDNIGHT,getGeoLocation().getZoneId());
708        }
709
710        /**
711         * Used by Molad based <em>zmanim</em> to determine if <em>zmanim</em> occur during the current day.
712         * @return following midnight
713         */
714        protected ZonedDateTime getMidnightTonight() {
715                return ZonedDateTime.of(getLocalDate().plusDays(1),LocalTime.MIDNIGHT,getGeoLocation().getZoneId());
716        }
717
718        /**
719         * Returns an XML formatted representation of the class using the default output of the
720         *         {@link com.kosherjava.zmanim.util.ZmanimFormatter#toXML(AstronomicalCalendar) toXML} method.
721         * @return an XML formatted representation of the class. It returns the default output of the
722         *         {@link com.kosherjava.zmanim.util.ZmanimFormatter#toXML(AstronomicalCalendar) toXML} method.
723         * @see com.kosherjava.zmanim.util.ZmanimFormatter#toXML(AstronomicalCalendar)
724         * @see java.lang.Object#toString()
725         */
726        public String toString() {
727                return ZmanimFormatter.toXML(this);
728        }
729        
730        /**
731         * Returns a JSON formatted representation of the class using the default output of the
732         *         {@link com.kosherjava.zmanim.util.ZmanimFormatter#toJSON(AstronomicalCalendar) toJSON} method.
733         * @return a JSON formatted representation of the class. It returns the default output of the
734         *         {@link com.kosherjava.zmanim.util.ZmanimFormatter#toJSON(AstronomicalCalendar) toJSON} method.
735         * @see com.kosherjava.zmanim.util.ZmanimFormatter#toJSON(AstronomicalCalendar)
736         * @see java.lang.Object#toString()
737         */
738        public String toJSON() {
739                return ZmanimFormatter.toJSON(this);
740        }
741
742        /**
743         * @see java.lang.Object#equals(Object)
744         */
745        public boolean equals(Object object) {
746                if (this == object) {
747                        return true;
748                }
749                if (object == null || getClass() != object.getClass()) {
750                        return false;
751                }
752                AstronomicalCalendar aCal = (AstronomicalCalendar) object;
753                return Objects.equals(getLocalDate(), aCal.getLocalDate())
754                                && Objects.equals(getGeoLocation(), aCal.getGeoLocation())
755                                && Objects.equals(getAstronomicalCalculator(), aCal.getAstronomicalCalculator());
756        }
757
758        /**
759         * @see java.lang.Object#hashCode()
760         */
761        public int hashCode() {
762                return Objects.hash(getClass(), getLocalDate(), getGeoLocation(), getAstronomicalCalculator());
763        }
764
765        /**
766         * A method that returns the currently set {@link GeoLocation} which contains location information used for the
767         * astronomical calculations.
768         * 
769         * @return Returns the geoLocation.
770         */
771        public GeoLocation getGeoLocation() {
772                return this.geoLocation;
773        }
774
775        /**
776         * Sets the {@link GeoLocation} {@code Object} to be used for astronomical calculations.
777         * 
778         * @param geoLocation The geoLocation to set.
779         * @todo Possibly adjust for horizon elevation. It may be smart to just have the calculator check the GeoLocation
780         *       though it doesn't really belong there.
781         */
782        public void setGeoLocation(GeoLocation geoLocation) {
783                this.geoLocation = geoLocation;
784        }
785
786        /**
787         * A method that returns the currently set AstronomicalCalculator.
788         * 
789         * @return Returns the astronomicalCalculator.
790         * @see setAstronomicalCalculator(AstronomicalCalculator)
791         */
792        public AstronomicalCalculator getAstronomicalCalculator() {
793                return this.astronomicalCalculator;
794        }
795
796        /**
797         * A method to set the {@link AstronomicalCalculator} used for astronomical calculations. The Zmanim package ships
798         * with a number of different implementations of the {@code abstract} {@link AstronomicalCalculator} based on
799         * different algorithms, including the default {@link com.kosherjava.zmanim.util.NOAACalculator} based on <a href=
800         * "https://noaa.gov">NOAA's</a> implementation of Jean Meeus's algorithms as well as {@link
801         * com.kosherjava.zmanim.util.SunTimesCalculator} based on the <a href = "https://www.cnmoc.usff.navy.mil/usno/">US
802         * Naval Observatory's</a> algorithm. This allows easy runtime switching and comparison of different algorithms.
803         * 
804         * @param astronomicalCalculator The astronomicalCalculator to set.
805         */
806        public void setAstronomicalCalculator(AstronomicalCalculator astronomicalCalculator) {
807                this.astronomicalCalculator = astronomicalCalculator;
808        }
809        
810        /**
811         * returns the {@code LocalDate} object encapsulated in this class.
812         * 
813         * @return Returns the {@code LocalDate}.
814         */
815        public LocalDate getLocalDate() {
816                return this.localDate;
817        }
818        
819        /**
820         * Sets the {@code LocalDate}  object for us in this class.
821         * @param localDate The {@code LocalDate} to set.
822         */
823        public void setLocalDate(LocalDate localDate) {
824                this.localDate = localDate;
825        }
826
827        /**
828         * A method that creates a <a href="https://en.wikipedia.org/wiki/Object_copy#Deep_copy">deep copy</a> of the object.
829         * 
830         * @see java.lang.Object#clone()
831         */
832        public Object clone() {
833                AstronomicalCalendar clone = null;
834                try {
835                        clone = (AstronomicalCalendar) super.clone();
836                } catch (CloneNotSupportedException cnse) {
837                        // Required by the compiler. Should never be reached since we implement clone()
838                }
839                if (clone != null) {
840                        clone.setGeoLocation((GeoLocation) getGeoLocation().clone()); // consider converting the GeoLocation class to be immutable to avoid the deep copy
841                        clone.setAstronomicalCalculator((AstronomicalCalculator) getAstronomicalCalculator().clone()); // likely not needed
842                }
843                return clone;
844        }
845}