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