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