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