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