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