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