Skip to main content

calendrical_calculations/
astronomy.rs

1// This file is part of ICU4X.
2//
3// The contents of this file implement algorithms from Calendrical Calculations
4// by Reingold & Dershowitz, Cambridge University Press, 4th edition (2018),
5// which have been released as Lisp code at <https://github.com/EdReingold/calendar-code2/>
6// under the Apache-2.0 license. Accordingly, this file is released under
7// the Apache License, Version 2.0 which can be found at the calendrical_calculations
8// package root or at http://www.apache.org/licenses/LICENSE-2.0.
9
10//! This file contains important structs and functions relating to location,
11//! time, and astronomy; these are intended for calender calculations and based off
12//! _Calendrical Calculations_ by Reingold & Dershowitz.
13
14// TODO(#3709): Address inconcistencies with existing ICU code for extreme dates.
15
16use crate::error::LocationOutOfBoundsError;
17use crate::helpers::{binary_search, i64_to_i32, invert_angular, next_moment, poly};
18use crate::rata_die::{Moment, RataDie};
19use core::f64::consts::PI;
20#[allow(unused_imports)]
21use core_maths::*;
22
23// TODO: this isn't f64::div_euclid as defined in std. Figure out what the call sites
24// mean to do.
25fn div_euclid_f64(n: f64, d: f64) -> f64 {
26    debug_assert!(d > 0.0);
27    let (a, b) = (n / d, n % d);
28    if n >= 0.0 || b == 0.0 {
29        a
30    } else {
31        a - 1.0
32    }
33}
34
35#[derive(Debug, Copy, Clone, PartialEq)]
36/// A Location on the Earth given as a latitude, longitude, elevation, and standard time zone.
37/// Latitude is given in degrees from -90 to 90, longitude in degrees from -180 to 180,
38/// elevation in meters, and zone as a UTC offset in fractional days (ex. UTC+1 would have zone = 1.0 / 24.0)
39#[allow(clippy::exhaustive_structs)] // This is all that is needed by the book algorithms
40pub struct Location {
41    /// latitude from -90 to 90
42    pub(crate) latitude: f64,
43    /// longitude from -180 to 180
44    pub(crate) longitude: f64,
45    /// elevation in meters
46    pub(crate) elevation: f64,
47    /// UTC timezone offset in fractional days (1 hr = 1.0 / 24.0 day),
48    /// within the range (-12.0 / 24.0) to (14.0 / 24.0)
49    pub(crate) utc_offset: f64,
50}
51
52/// The mean synodic month in days of 86400 atomic seconds
53/// (86400 seconds = 24 hours * 60 minutes/hour * 60 seconds/minute)
54///
55/// This is defined in _Calendrical Calculations_ by Reingold & Dershowitz.
56/// Reference lisp code: <https://github.com/EdReingold/calendar-code2/blob/9afc1f3/calendar.l#L3880-L3882>
57pub const MEAN_SYNODIC_MONTH: f64 = 29.530588861;
58
59/// The Moment of noon on January 1, 2000
60pub const J2000: Moment = Moment::new(730120.5);
61
62/// The mean tropical year in days
63///
64/// This is defined in _Calendrical Calculations_ by Reingold & Dershowitz.
65/// Reference lisp code: <https://github.com/EdReingold/calendar-code2/blob/9afc1f3/calendar.l#L3872-L3874>
66pub const MEAN_TROPICAL_YEAR: f64 = 365.242189;
67
68/// The minimum allowable UTC offset (-12 hours) in fractional days (-0.5 days)
69pub const MIN_UTC_OFFSET: f64 = -0.5;
70
71/// The maximum allowable UTC offset (+14 hours) in fractional days (14.0 / 24.0 days)
72pub const MAX_UTC_OFFSET: f64 = 14.0 / 24.0;
73
74/// The angle of winter for the purposes of solar calculations
75pub const WINTER: f64 = 270.0;
76
77/// The moment of the first new moon of the CE, which occurred on January 11, 1 CE.
78pub const NEW_MOON_ZERO: Moment = Moment::new(11.458922815770109);
79
80impl Location {
81    /// Create a location; latitude is from -90 to 90, longitude is from -180 to 180,
82    /// and `utc_offset` is from (-12.0 / 24.0) to (14.0 / 24.0);
83    /// attempting to create a location outside of these bounds will result in a [`LocationOutOfBoundsError`].
84    pub fn try_new(
85        latitude: f64,
86        longitude: f64,
87        elevation: f64,
88        utc_offset: f64,
89    ) -> Result<Location, LocationOutOfBoundsError> {
90        if !(-90.0..=90.0).contains(&latitude) {
91            return Err(LocationOutOfBoundsError::Latitude(latitude));
92        }
93        if !(-180.0..=180.0).contains(&longitude) {
94            return Err(LocationOutOfBoundsError::Longitude(longitude));
95        }
96        if !(MIN_UTC_OFFSET..=MAX_UTC_OFFSET).contains(&utc_offset) {
97            return Err(LocationOutOfBoundsError::Offset(
98                utc_offset,
99                MIN_UTC_OFFSET,
100                MAX_UTC_OFFSET,
101            ));
102        }
103        Ok(Location {
104            latitude,
105            longitude,
106            elevation,
107            utc_offset,
108        })
109    }
110
111    /// Get the longitude of a Location
112    #[allow(dead_code)]
113    pub(crate) fn longitude(&self) -> f64 {
114        self.longitude
115    }
116
117    /// Get the latitude of a Location
118    #[allow(dead_code)]
119    pub(crate) fn latitude(&self) -> f64 {
120        self.latitude
121    }
122
123    /// Get the elevation of a Location
124    #[allow(dead_code)]
125    pub(crate) fn elevation(&self) -> f64 {
126        self.elevation
127    }
128
129    /// Get the utc-offset of a Location
130    #[allow(dead_code)]
131    pub(crate) fn zone(&self) -> f64 {
132        self.utc_offset
133    }
134
135    /// Convert a longitude into a mean time zone;
136    /// this yields the difference in Moment given a longitude
137    /// e.g. a longitude of 90 degrees is 0.25 (90 / 360) days ahead
138    /// of a location with a longitude of 0 degrees.
139    pub(crate) fn zone_from_longitude(longitude: f64) -> f64 {
140        longitude / (360.0)
141    }
142
143    /// Convert standard time to local mean time given a location and a time zone with given offset
144    ///
145    /// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz.
146    /// Reference lisp code: <https://github.com/EdReingold/calendar-code2/blob/9afc1f3/calendar.l#L3501-L3506>
147    #[allow(dead_code)]
148    pub(crate) fn standard_from_local(standard_time: Moment, location: Location) -> Moment {
149        Self::standard_from_universal(
150            Self::universal_from_local(standard_time, location),
151            location,
152        )
153    }
154
155    /// Convert from local mean time to universal time given a location
156    ///
157    /// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz.
158    /// Reference lisp code: <https://github.com/EdReingold/calendar-code2/blob/9afc1f3/calendar.l#L3496-L3499>
159    pub(crate) fn universal_from_local(local_time: Moment, location: Location) -> Moment {
160        local_time - Self::zone_from_longitude(location.longitude)
161    }
162
163    /// Convert from universal time to local time given a location
164    ///
165    /// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz.
166    /// Reference lisp code: <https://github.com/EdReingold/calendar-code2/blob/9afc1f3/calendar.l#L3491-L3494>
167    #[allow(dead_code)] // TODO: Remove dead_code tag after use
168    pub(crate) fn local_from_universal(universal_time: Moment, location: Location) -> Moment {
169        universal_time + Self::zone_from_longitude(location.longitude)
170    }
171
172    /// Given a UTC-offset in hours and a Moment in standard time,
173    /// return the Moment in universal time from the time zone with the given offset.
174    /// The field `utc_offset` should be within the range of possible offsets given by
175    /// the constand fields `MIN_UTC_OFFSET` and `MAX_UTC_OFFSET`.
176    ///
177    /// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz.
178    /// Reference lisp code: <https://github.com/EdReingold/calendar-code2/blob/9afc1f3/calendar.l#L3479-L3483>
179    pub(crate) fn universal_from_standard(standard_moment: Moment, location: Location) -> Moment {
180        debug_assert!(location.utc_offset > MIN_UTC_OFFSET && location.utc_offset < MAX_UTC_OFFSET, "UTC offset {0} was not within the possible range of offsets (see astronomy::MIN_UTC_OFFSET and astronomy::MAX_UTC_OFFSET)", location.utc_offset);
181        standard_moment - location.utc_offset
182    }
183    /// Given a Moment in standard time and UTC-offset in hours,
184    /// return the Moment in standard time from the time zone with the given offset.
185    /// The field `utc_offset` should be within the range of possible offsets given by
186    /// the constand fields `MIN_UTC_OFFSET` and `MAX_UTC_OFFSET`.
187    ///
188    /// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz.
189    /// Reference lisp code: <https://github.com/EdReingold/calendar-code2/blob/9afc1f3/calendar.l#L3473-L3477>
190    #[allow(dead_code)]
191    pub(crate) fn standard_from_universal(standard_time: Moment, location: Location) -> Moment {
192        debug_assert!(location.utc_offset > MIN_UTC_OFFSET && location.utc_offset < MAX_UTC_OFFSET, "UTC offset {0} was not within the possible range of offsets (see astronomy::MIN_UTC_OFFSET and astronomy::MAX_UTC_OFFSET)", location.utc_offset);
193        standard_time + location.utc_offset
194    }
195}
196
197#[derive(Debug)]
198/// The Astronomical struct provides functions which support astronomical
199/// calculations used by many observational calendars.
200#[allow(clippy::exhaustive_structs)] // only exists to collect methods
201pub struct Astronomical;
202
203impl Astronomical {
204    /// Function for the ephemeris correction, which corrects the
205    /// somewhat-unpredictable discrepancy between dynamical time
206    /// and universal time
207    ///
208    /// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz,
209    /// originally from _Astronomical Algorithms_ by Jean Meeus (1991) with data from NASA.
210    /// Reference lisp code: <https://github.com/EdReingold/calendar-code2/blob/9afc1f3/calendar.l#L3884-L3952>
211    pub fn ephemeris_correction(moment: Moment) -> f64 {
212        // TODO: Change this to directly convert from moment to Gregorian year through a separate fn
213        let year = moment.inner() / 365.2425;
214        // Note: Converting to int handles negative number Euclidean division skew.
215        let year_int = (if year > 0.0 { year + 1.0 } else { year }) as i32;
216        let fixed_mid_year = crate::gregorian::fixed_from_gregorian(year_int, 7, 1);
217        let c = ((fixed_mid_year.to_i64_date() as f64) - 693596.0) / 36525.0;
218        let y2000 = (year_int - 2000) as f64;
219        let y1700 = (year_int - 1700) as f64;
220        let y1600 = (year_int - 1600) as f64;
221        let y1000 = ((year_int - 1000) as f64) / 100.0;
222        let y0 = year_int as f64 / 100.0;
223        let y1820 = ((year_int - 1820) as f64) / 100.0;
224
225        if (2051..=2150).contains(&year_int) {
226            (-20.0
227                + 32.0 * (((year_int - 1820) * (year_int - 1820)) as f64 / 10000.0)
228                + 0.5628 * (2150 - year_int) as f64)
229                / 86400.0
230        } else if (2006..=2050).contains(&year_int) {
231            (62.92 + 0.32217 * y2000 + 0.005589 * y2000 * y2000) / 86400.0
232        } else if (1987..=2005).contains(&year_int) {
233            // This polynomial is written out manually instead of using pow for optimization, see #3743
234            (63.86 + 0.3345 * y2000 - 0.060374 * y2000 * y2000
235                + 0.0017275 * y2000 * y2000 * y2000
236                + 0.000651814 * y2000 * y2000 * y2000 * y2000
237                + 0.00002373599 * y2000 * y2000 * y2000 * y2000 * y2000)
238                / 86400.0
239        } else if (1900..=1986).contains(&year_int) {
240            // This polynomial is written out manually instead of using a fn like pow for optimization, see #3743
241            -0.00002 + 0.000297 * c + 0.025184 * c * c - 0.181133 * c * c * c
242                + 0.553040 * c * c * c * c
243                - 0.861938 * c * c * c * c * c
244                + 0.677066 * c * c * c * c * c * c
245                - 0.212591 * c * c * c * c * c * c * c
246        } else if (1800..=1899).contains(&year_int) {
247            // This polynomial is written out manually instead of using a fn like pow for optimization, see #3743
248            -0.000009
249                + 0.003844 * c
250                + 0.083563 * c * c
251                + 0.865736 * c * c * c
252                + 4.867575 * c * c * c * c
253                + 15.845535 * c * c * c * c * c
254                + 31.332267 * c * c * c * c * c * c
255                + 38.291999 * c * c * c * c * c * c * c
256                + 28.316289 * c * c * c * c * c * c * c * c
257                + 11.636204 * c * c * c * c * c * c * c * c * c
258                + 2.043794 * c * c * c * c * c * c * c * c * c * c
259        } else if (1700..=1799).contains(&year_int) {
260            // This polynomial is written out manually instead of using a fn like pow for optimization, see #3743
261            (8.118780842 - 0.005092142 * y1700 + 0.003336121 * y1700 * y1700
262                - 0.0000266484 * y1700 * y1700 * y1700)
263                / 86400.0
264        } else if (1600..=1699).contains(&year_int) {
265            // This polynomial is written out manually instead of using a fn like pow for optimization, see #3743
266            (120.0 - 0.9808 * y1600 - 0.01532 * y1600 * y1600
267                + 0.000140272128 * y1600 * y1600 * y1600)
268                / 86400.0
269        } else if (500..=1599).contains(&year_int) {
270            // This polynomial is written out manually instead of using a fn like pow for optimization, see #3743
271            (1574.2 - 556.01 * y1000 + 71.23472 * y1000 * y1000 + 0.319781 * y1000 * y1000 * y1000
272                - 0.8503463 * y1000 * y1000 * y1000 * y1000
273                - 0.005050998 * y1000 * y1000 * y1000 * y1000 * y1000
274                + 0.0083572073 * y1000 * y1000 * y1000 * y1000 * y1000 * y1000)
275                / 86400.0
276        } else if (-499..=499).contains(&year_int) {
277            // This polynomial is written out manually instead of using a fn like pow for optimization, see #3743
278            (10583.6 - 1014.41 * y0 + 33.78311 * y0 * y0
279                - 5.952053 * y0 * y0 * y0
280                - 0.1798452 * y0 * y0 * y0 * y0
281                + 0.022174192 * y0 * y0 * y0 * y0 * y0
282                + 0.0090316521 * y0 * y0 * y0 * y0 * y0 * y0)
283                / 86400.0
284        } else {
285            (-20.0 + 32.0 * y1820 * y1820) / 86400.0
286        }
287    }
288
289    /// Include the ephemeris correction to universal time, yielding dynamical time
290    ///
291    /// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz.
292    /// Reference lisp code: <https://github.com/EdReingold/calendar-code2/blob/9afc1f3/calendar.l#L3850-L3853>
293    pub fn dynamical_from_universal(universal: Moment) -> Moment {
294        // TODO: Determine correct naming scheme for "dynamical"
295        universal + Self::ephemeris_correction(universal)
296    }
297
298    /// Remove the ephemeris correction from dynamical time, yielding universal time
299    ///
300    /// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz.
301    /// Reference lisp code: <https://github.com/EdReingold/calendar-code2/blob/9afc1f3/calendar.l#L3845-L3848>
302    pub fn universal_from_dynamical(dynamical: Moment) -> Moment {
303        // TODO: Determine correct naming scheme for "dynamical"
304        dynamical - Self::ephemeris_correction(dynamical)
305    }
306
307    /// The number of uniform length centuries (36525 days measured in dynamical time)
308    /// before or after noon on January 1, 2000
309    ///
310    /// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz.
311    /// Reference lisp code: <https://github.com/EdReingold/calendar-code2/blob/9afc1f3/calendar.l#L3551-L3555>
312    pub fn julian_centuries(moment: Moment) -> f64 {
313        let intermediate = Self::dynamical_from_universal(moment);
314        (intermediate - J2000) / 36525.0
315    }
316
317    /// The equation of time, which approximates the difference between apparent solar time and
318    /// mean time; for example, the difference between when the sun is highest in the sky (solar noon)
319    /// and noon as measured by a clock adjusted to the local longitude. This varies throughout the
320    /// year and the difference is given by the equation of time.
321    ///
322    /// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz,
323    /// originally from _Astronomical Algorithms_ by Jean Meeus, 2nd edn., 1998, p. 185.
324    /// Reference lisp code: <https://github.com/EdReingold/calendar-code2/blob/9afc1f3/calendar.l#L3954-L3983>
325    pub fn equation_of_time(moment: Moment) -> f64 {
326        let c = Self::julian_centuries(moment);
327        let lambda = poly(c, &[280.46645, 36000.76983, 0.0003032]);
328        let anomaly = poly(c, &[357.52910, 35999.05030, -0.0001559, -0.00000048]);
329        let eccentricity = poly(c, &[0.016708617, -0.000042037, -0.0000001236]);
330        let varepsilon = Self::obliquity(moment);
331        let y = (varepsilon / 2.0).to_radians().tan();
332        let y = y * y;
333        let equation = (y * (2.0 * lambda).to_radians().sin()
334            - 2.0 * eccentricity * anomaly.to_radians().sin()
335            + 4.0
336                * eccentricity
337                * y
338                * anomaly.to_radians().sin()
339                * (2.0 * lambda).to_radians().cos()
340            - 0.5 * y * y * (4.0 * lambda).to_radians().sin()
341            - 1.25 * eccentricity * eccentricity * (2.0 * anomaly).to_radians().sin())
342            / (2.0 * PI);
343
344        equation.signum() * equation.abs().min(12.0 / 24.0)
345    }
346
347    /// The standard time of dusk at a given location on a given date, or `None` if there is no
348    /// dusk on that date.
349    ///
350    /// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz.
351    /// Reference lisp code: <https://github.com/EdReingold/calendar-code2/blob/9afc1f3/calendar.l#L3670-L3679>
352    pub fn dusk(date: f64, location: Location, alpha: f64) -> Option<Moment> {
353        let evening = false;
354        let moment_of_depression = Self::moment_of_depression(
355            Moment::new(date + (18.0 / 24.0)),
356            location,
357            alpha,
358            evening,
359        )?;
360        Some(Location::standard_from_local(
361            moment_of_depression,
362            location,
363        ))
364    }
365
366    /// Calculates the obliquity of the ecliptic at a given moment, meaning the angle of the Earth's
367    /// axial tilt with respect to the plane of its orbit around the sun  (currently ~23.4 deg)
368    ///
369    /// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz.
370    /// Reference lisp code: <https://github.com/EdReingold/calendar-code2/blob/9afc1f3/calendar.l#L3557-L3565>
371    pub fn obliquity(moment: Moment) -> f64 {
372        let c = Self::julian_centuries(moment);
373        let angle = 23.0 + 26.0 / 60.0 + 21.448 / 3600.0;
374        let coefs = &[0.0, -46.8150 / 3600.0, -0.00059 / 3600.0, 0.001813 / 3600.0];
375        angle + poly(c, coefs)
376    }
377
378    /// Calculates the declination at a given [`Moment`] of UTC time of an object at ecliptic latitude `beta` and ecliptic longitude `lambda`; all angles are in degrees.
379    /// the declination is the angular distance north or south of an object in the sky with respect to the plane
380    /// of the Earth's equator; analogous to latitude.
381    ///
382    /// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz.
383    /// Reference lisp code: <https://github.com/EdReingold/calendar-code2/blob/9afc1f3/calendar.l#L3567-L3576>
384    pub fn declination(moment: Moment, beta: f64, lambda: f64) -> f64 {
385        let varepsilon = Self::obliquity(moment);
386        (beta.to_radians().sin() * varepsilon.to_radians().cos()
387            + beta.to_radians().cos() * varepsilon.to_radians().sin() * lambda.to_radians().sin())
388        .asin()
389        .to_degrees()
390        .rem_euclid(360.0)
391    }
392
393    /// Calculates the right ascension at a given [`Moment`] of UTC time of an object at ecliptic latitude `beta` and ecliptic longitude `lambda`; all angles are in degrees.
394    /// the right ascension is the angular distance east or west of an object in the sky with respect to the plane
395    /// of the vernal equinox, which is the celestial coordinate point at which the ecliptic intersects the celestial
396    /// equator marking spring in the northern hemisphere; analogous to longitude.
397    ///
398    /// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz.
399    /// Reference lisp code: <https://github.com/EdReingold/calendar-code2/blob/9afc1f3/calendar.l#L3578-L3588>
400    pub fn right_ascension(moment: Moment, beta: f64, lambda: f64) -> f64 {
401        let varepsilon = Self::obliquity(moment);
402
403        let y = lambda.to_radians().sin() * varepsilon.to_radians().cos()
404            - beta.to_radians().tan() * varepsilon.to_radians().sin();
405        let x = lambda.to_radians().cos();
406
407        // Arctangent of y/x in degrees, handling zero cases
408        y.atan2(x).to_degrees().rem_euclid(360.0)
409    }
410
411    /// Local time from apparent solar time at a given location
412    ///
413    /// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz.
414    /// Reference lisp code: <https://github.com/EdReingold/calendar-code2/blob/9afc1f3/calendar.l#L3521-L3524>
415    pub fn local_from_apparent(moment: Moment, location: Location) -> Moment {
416        moment - Self::equation_of_time(Location::universal_from_local(moment, location))
417    }
418
419    /// Approx moment in local time near `moment` at which the depression angle of the sun is `alpha` (negative if
420    /// the sun is above the horizon) at the given location; since the same angle of depression of the sun
421    /// can exist twice in a day, early is set to true to specify the morning moment, and false for the
422    /// evening. Returns `None` if the specified angle is not reached.
423    ///
424    /// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz.
425    /// Reference lisp code: <https://github.com/EdReingold/calendar-code2/blob/9afc1f3/calendar.l#L3607-L3631>
426    pub fn approx_moment_of_depression(
427        moment: Moment,
428        location: Location,
429        alpha: f64,
430        early: bool, /* TODO: Replace this bool with an enum with Morning and Evening, or Early and Late */
431    ) -> Option<Moment> {
432        let date = moment.as_rata_die().to_f64_date().floor();
433        let alt = if alpha >= 0.0 {
434            if early {
435                date
436            } else {
437                date + 1.0
438            }
439        } else {
440            date + 12.0 / 24.0
441        };
442
443        let value = if Self::sine_offset(moment, location, alpha).abs() > 1.0 {
444            Self::sine_offset(Moment::new(alt), location, alpha)
445        } else {
446            Self::sine_offset(moment, location, alpha)
447        };
448
449        if value.abs() <= 1.0 {
450            let offset =
451                (value.asin().to_degrees().rem_euclid(360.0) / 360.0 + 0.5).rem_euclid(1.0) - 0.5;
452
453            let moment = Moment::new(
454                date + if early {
455                    (6.0 / 24.0) - offset
456                } else {
457                    (18.0 / 24.0) + offset
458                },
459            );
460            Some(Self::local_from_apparent(moment, location))
461        } else {
462            None
463        }
464    }
465
466    /// Moment in local time near `approx` at which the depression angle of the sun is `alpha` (negative if
467    /// the sun is above the horizon) at the given location; since the same angle of depression of the sun
468    /// can exist twice in a day, early is set to true to specify the morning moment, and false for the
469    /// evening. Returns `None` if the specified angle is not reached.
470    ///
471    /// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz.
472    /// Reference lisp code: <https://github.com/EdReingold/calendar-code2/blob/9afc1f3/calendar.l#L3633-L3647>
473    pub fn moment_of_depression(
474        approx: Moment,
475        location: Location,
476        alpha: f64,
477        early: bool, /* TODO: Replace this bool with an enum with Morning and Evening, or Early and Late */
478    ) -> Option<Moment> {
479        let moment = Self::approx_moment_of_depression(approx, location, alpha, early)?;
480        if (approx - moment).abs() < 30.0 {
481            Some(moment)
482        } else {
483            Self::moment_of_depression(moment, location, alpha, early)
484        }
485    }
486
487    /// The angle of refraction caused by Earth's atmosphere at a given location.
488    ///
489    /// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz.
490    /// Reference lisp code: <https://github.com/EdReingold/calendar-code2/blob/9afc1f3/calendar.l#L3681-L3690>
491    pub fn refraction(location: Location) -> f64 {
492        // The moment is not used.
493        let h = location.elevation.max(0.0);
494        let earth_r = 6.372e6; // Radius of Earth.
495        let dip = (earth_r / (earth_r + h)).acos().to_degrees();
496
497        (34.0 / 60.0) + dip + ((19.0 / 3600.0) * h.sqrt())
498    }
499
500    /// The moment (in universal time) of the nth new moon after
501    /// (or before if n is negative) the new moon of January 11, 1 CE,
502    /// which is the first new moon after R.D. 0.
503    ///
504    /// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz,
505    /// originally from _Astronomical Algorithms_ by Jean Meeus, corrected 2nd edn., 2005.
506    /// Reference code: <https://github.com/EdReingold/calendar-code2/blob/9afc1f3/calendar.l#L4288-L4377>
507    pub fn nth_new_moon(n: i32) -> Moment {
508        // The following polynomials are written out instead of using pow for optimization, see #3743
509        let n0 = 24724.0;
510        let k = (n as f64) - n0;
511        let c = k / 1236.85;
512        let approx = J2000
513            + (5.09766 + (MEAN_SYNODIC_MONTH * 1236.85 * c) + (0.00015437 * c * c)
514                - (0.00000015 * c * c * c)
515                + (0.00000000073 * c * c * c * c));
516        let e = 1.0 - (0.002516 * c) - (0.0000074 * c * c);
517        let solar_anomaly =
518            2.5534 + (1236.85 * 29.10535670 * c) - (0.0000014 * c * c) - (0.00000011 * c * c * c);
519        let lunar_anomaly = 201.5643
520            + (385.81693528 * 1236.85 * c)
521            + (0.0107582 * c * c)
522            + (0.00001238 * c * c * c)
523            - (0.000000058 * c * c * c * c);
524        let moon_argument = 160.7108 + (390.67050284 * 1236.85 * c)
525            - (0.0016118 * c * c)
526            - (0.00000227 * c * c * c)
527            + (0.000000011 * c * c * c * c);
528        let omega =
529            124.7746 + (-1.56375588 * 1236.85 * c) + (0.0020672 * c * c) + (0.00000215 * c * c * c);
530
531        let mut st = (
532            0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
533            0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
534        );
535        let [v0, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23] = [
536            -0.40720, 0.17241, 0.01608, 0.01039, 0.00739, -0.00514, 0.00208, -0.00111, -0.00057,
537            0.00056, -0.00042, 0.00042, 0.00038, -0.00024, -0.00007, 0.00004, 0.00004, 0.00003,
538            0.00003, -0.00003, 0.00003, -0.00002, -0.00002, 0.00002,
539        ];
540        let [x0, x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11, x12, x13, x14, x15, x16, x17, x18, x19, x20, x21, x22, x23] = [
541            0.0, 1.0, 0.0, 0.0, -1.0, 1.0, 2.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, -1.0, 2.0, 0.0, 3.0,
542            1.0, 0.0, 1.0, -1.0, -1.0, 1.0, 0.0,
543        ];
544        let [y0, y1, y2, y3, y4, y5, y6, y7, y8, y9, y10, y11, y12, y13, y14, y15, y16, y17, y18, y19, y20, y21, y22, y23] = [
545            1.0, 0.0, 2.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 2.0, 3.0, 0.0, 0.0, 2.0, 1.0, 2.0, 0.0,
546            1.0, 2.0, 1.0, 1.0, 1.0, 3.0, 4.0,
547        ];
548        let [z0, z1, z2, z3, z4, z5, z6, z7, z8, z9, z10, z11, z12, z13, z14, z15, z16, z17, z18, z19, z20, z21, z22, z23] = [
549            0.0, 0.0, 0.0, 2.0, 0.0, 0.0, 0.0, -2.0, 2.0, 0.0, 0.0, 2.0, -2.0, 0.0, 0.0, -2.0, 0.0,
550            -2.0, 2.0, 2.0, 2.0, -2.0, 0.0, 0.0,
551        ];
552
553        let mut at = (
554            0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
555        );
556        let [i0, i1, i2, i3, i4, i5, i6, i7, i8, i9, i10, i11, i12] = [
557            251.88, 251.83, 349.42, 84.66, 141.74, 207.14, 154.84, 34.52, 207.19, 291.34, 161.72,
558            239.56, 331.55,
559        ];
560        let [j0, j1, j2, j3, j4, j5, j6, j7, j8, j9, j10, j11, j12] = [
561            0.016321, 26.651886, 36.412478, 18.206239, 53.303771, 2.453732, 7.306860, 27.261239,
562            0.121824, 1.844379, 24.198154, 25.513099, 3.592518,
563        ];
564        let [l0, l1, l2, l3, l4, l5, l6, l7, l8, l9, l10, l11, l12] = [
565            0.000165, 0.000164, 0.000126, 0.000110, 0.000062, 0.000060, 0.000056, 0.000047,
566            0.000042, 0.000040, 0.000037, 0.000035, 0.000023,
567        ];
568
569        let mut correction = -0.00017 * omega.to_radians().sin();
570
571        // This summation is unrolled for optimization, see #3743
572        st.0 = v0
573            * (x0 * solar_anomaly + y0 * lunar_anomaly + z0 * moon_argument)
574                .to_radians()
575                .sin();
576        st.1 = v1
577            * e
578            * (x1 * solar_anomaly + y1 * lunar_anomaly + z1 * moon_argument)
579                .to_radians()
580                .sin();
581        st.2 = v2
582            * (x2 * solar_anomaly + y2 * lunar_anomaly + z2 * moon_argument)
583                .to_radians()
584                .sin();
585        st.3 = v3
586            * (x3 * solar_anomaly + y3 * lunar_anomaly + z3 * moon_argument)
587                .to_radians()
588                .sin();
589        st.4 = v4
590            * e
591            * (x4 * solar_anomaly + y4 * lunar_anomaly + z4 * moon_argument)
592                .to_radians()
593                .sin();
594        st.5 = v5
595            * e
596            * (x5 * solar_anomaly + y5 * lunar_anomaly + z5 * moon_argument)
597                .to_radians()
598                .sin();
599        st.6 = v6
600            * e
601            * e
602            * (x6 * solar_anomaly + y6 * lunar_anomaly + z6 * moon_argument)
603                .to_radians()
604                .sin();
605        st.7 = v7
606            * (x7 * solar_anomaly + y7 * lunar_anomaly + z7 * moon_argument)
607                .to_radians()
608                .sin();
609        st.8 = v8
610            * (x8 * solar_anomaly + y8 * lunar_anomaly + z8 * moon_argument)
611                .to_radians()
612                .sin();
613        st.9 = v9
614            * e
615            * (x9 * solar_anomaly + y9 * lunar_anomaly + z9 * moon_argument)
616                .to_radians()
617                .sin();
618        st.10 = v10
619            * (x10 * solar_anomaly + y10 * lunar_anomaly + z10 * moon_argument)
620                .to_radians()
621                .sin();
622        st.11 = v11
623            * e
624            * (x11 * solar_anomaly + y11 * lunar_anomaly + z11 * moon_argument)
625                .to_radians()
626                .sin();
627        st.12 = v12
628            * e
629            * (x12 * solar_anomaly + y12 * lunar_anomaly + z12 * moon_argument)
630                .to_radians()
631                .sin();
632        st.13 = v13
633            * e
634            * (x13 * solar_anomaly + y13 * lunar_anomaly + z13 * moon_argument)
635                .to_radians()
636                .sin();
637        st.14 = v14
638            * (x14 * solar_anomaly + y14 * lunar_anomaly + z14 * moon_argument)
639                .to_radians()
640                .sin();
641        st.15 = v15
642            * (x15 * solar_anomaly + y15 * lunar_anomaly + z15 * moon_argument)
643                .to_radians()
644                .sin();
645        st.16 = v16
646            * (x16 * solar_anomaly + y16 * lunar_anomaly + z16 * moon_argument)
647                .to_radians()
648                .sin();
649        st.17 = v17
650            * (x17 * solar_anomaly + y17 * lunar_anomaly + z17 * moon_argument)
651                .to_radians()
652                .sin();
653        st.18 = v18
654            * (x18 * solar_anomaly + y18 * lunar_anomaly + z18 * moon_argument)
655                .to_radians()
656                .sin();
657        st.19 = v19
658            * (x19 * solar_anomaly + y19 * lunar_anomaly + z19 * moon_argument)
659                .to_radians()
660                .sin();
661        st.20 = v20
662            * (x20 * solar_anomaly + y20 * lunar_anomaly + z20 * moon_argument)
663                .to_radians()
664                .sin();
665        st.21 = v21
666            * (x21 * solar_anomaly + y21 * lunar_anomaly + z21 * moon_argument)
667                .to_radians()
668                .sin();
669        st.22 = v22
670            * (x22 * solar_anomaly + y22 * lunar_anomaly + z22 * moon_argument)
671                .to_radians()
672                .sin();
673        st.23 = v23
674            * (x23 * solar_anomaly + y23 * lunar_anomaly + z23 * moon_argument)
675                .to_radians()
676                .sin();
677
678        let sum = st.0
679            + st.1
680            + st.2
681            + st.3
682            + st.4
683            + st.5
684            + st.6
685            + st.7
686            + st.8
687            + st.9
688            + st.10
689            + st.11
690            + st.12
691            + st.13
692            + st.14
693            + st.15
694            + st.16
695            + st.17
696            + st.18
697            + st.19
698            + st.20
699            + st.21
700            + st.22
701            + st.23;
702
703        correction += sum;
704        let extra = 0.000325
705            * (299.77 + (132.8475848 * c) - (0.009173 * c * c))
706                .to_radians()
707                .sin();
708
709        at.0 = l0 * (i0 + j0 * k).to_radians().sin();
710        at.1 = l1 * (i1 + j1 * k).to_radians().sin();
711        at.2 = l2 * (i2 + j2 * k).to_radians().sin();
712        at.3 = l3 * (i3 + j3 * k).to_radians().sin();
713        at.4 = l4 * (i4 + j4 * k).to_radians().sin();
714        at.5 = l5 * (i5 + j5 * k).to_radians().sin();
715        at.6 = l6 * (i6 + j6 * k).to_radians().sin();
716        at.7 = l7 * (i7 + j7 * k).to_radians().sin();
717        at.8 = l8 * (i8 + j8 * k).to_radians().sin();
718        at.9 = l9 * (i9 + j9 * k).to_radians().sin();
719        at.10 = l10 * (i10 + j10 * k).to_radians().sin();
720        at.11 = l11 * (i11 + j11 * k).to_radians().sin();
721        at.12 = l12 * (i12 + j12 * k).to_radians().sin();
722
723        let additional = at.0
724            + at.1
725            + at.2
726            + at.3
727            + at.4
728            + at.5
729            + at.6
730            + at.7
731            + at.8
732            + at.9
733            + at.10
734            + at.11
735            + at.12;
736        Self::universal_from_dynamical(approx + correction + extra + additional)
737    }
738
739    /// Sidereal time, as the hour angle between the meridian and the vernal equinox,
740    /// from a given moment.
741    ///
742    /// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz,
743    /// originally from _Astronomical Algorithms_ by Meeus, 2nd edition (1988), p. 88.
744    /// Reference lisp code: <https://github.com/EdReingold/calendar-code2/blob/9afc1f3/calendar.l#L3860-L3870>
745    #[allow(dead_code)] // TODO: Remove dead code tag after use
746    pub fn sidereal_from_moment(moment: Moment) -> f64 {
747        let c = (moment - J2000) / 36525.0;
748        let coefficients = &[
749            (280.46061837),
750            (36525.0 * 360.98564736629),
751            (0.000387933),
752            (-1.0 / 38710000.0),
753        ];
754
755        let angle = poly(c, coefficients);
756
757        angle.rem_euclid(360.0)
758    }
759
760    /// Ecliptic (aka celestial) latitude of the moon (in degrees)
761    ///
762    /// This is not a geocentric or geodetic latitude, it does not take into account the
763    /// rotation of the Earth and is instead measured from the ecliptic.
764    ///
765    /// `julian_centuries` is the result of calling `Self::julian_centuries(moment)`.
766    /// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz,
767    /// originally from _Astronomical Algorithms_ by Jean Meeus, 2nd edn., 1998, pp. 338-342.
768    /// Reference code: <https://github.com/EdReingold/calendar-code2/blob/9afc1f3/calendar.l#L4466>
769    pub fn lunar_latitude(julian_centuries: f64) -> f64 {
770        let c = julian_centuries;
771        let l = Self::mean_lunar_longitude(c);
772        let d = Self::lunar_elongation(c);
773        let ms = Self::solar_anomaly(c);
774        let ml = Self::lunar_anomaly(c);
775        let f = Self::moon_node(c);
776        let e = 1.0 - (0.002516 * c) - (0.0000074 * c * c);
777
778        let mut ct = (
779            0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
780            0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
781            0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
782            0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
783        );
784
785        let [w0, w1, w2, w3, w4, w5, w6, w7, w8, w9, w10, w11, w12, w13, w14, w15, w16, w17, w18, w19, w20, w21, w22, w23, w24, w25, w26, w27, w28, w29, w30, w31, w32, w33, w34, w35, w36, w37, w38, w39, w40, w41, w42, w43, w44, w45, w46, w47, w48, w49, w50, w51, w52, w53, w54, w55, w56, w57, w58, w59] = [
786            0.0, 0.0, 0.0, 2.0, 2.0, 2.0, 2.0, 0.0, 2.0, 0.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0,
787            0.0, 4.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 4.0, 4.0, 0.0, 4.0, 2.0, 2.0,
788            2.0, 2.0, 0.0, 2.0, 2.0, 2.0, 2.0, 4.0, 2.0, 2.0, 0.0, 2.0, 1.0, 1.0, 0.0, 2.0, 1.0,
789            2.0, 0.0, 4.0, 4.0, 1.0, 4.0, 1.0, 4.0, 2.0,
790        ];
791
792        let [x0, x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11, x12, x13, x14, x15, x16, x17, x18, x19, x20, x21, x22, x23, x24, x25, x26, x27, x28, x29, x30, x31, x32, x33, x34, x35, x36, x37, x38, x39, x40, x41, x42, x43, x44, x45, x46, x47, x48, x49, x50, x51, x52, x53, x54, x55, x56, x57, x58, x59] = [
793            0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -1.0, 0.0, 0.0, 1.0, -1.0, -1.0,
794            -1.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
795            0.0, -1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, -1.0, -2.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0,
796            0.0, -1.0, 1.0, 0.0, -1.0, 0.0, 0.0, 0.0, -1.0, -2.0,
797        ];
798
799        let [y0, y1, y2, y3, y4, y5, y6, y7, y8, y9, y10, y11, y12, y13, y14, y15, y16, y17, y18, y19, y20, y21, y22, y23, y24, y25, y26, y27, y28, y29, y30, y31, y32, y33, y34, y35, y36, y37, y38, y39, y40, y41, y42, y43, y44, y45, y46, y47, y48, y49, y50, y51, y52, y53, y54, y55, y56, y57, y58, y59] = [
800            0.0, 1.0, 1.0, 0.0, -1.0, -1.0, 0.0, 2.0, 1.0, 2.0, 0.0, -2.0, 1.0, 0.0, -1.0, 0.0,
801            -1.0, -1.0, -1.0, 0.0, 0.0, -1.0, 0.0, 1.0, 1.0, 0.0, 0.0, 3.0, 0.0, -1.0, 1.0, -2.0,
802            0.0, 2.0, 1.0, -2.0, 3.0, 2.0, -3.0, -1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 0.0,
803            -2.0, -1.0, 1.0, -2.0, 2.0, -2.0, -1.0, 1.0, 1.0, -1.0, 0.0, 0.0,
804        ];
805
806        let [z0, z1, z2, z3, z4, z5, z6, z7, z8, z9, z10, z11, z12, z13, z14, z15, z16, z17, z18, z19, z20, z21, z22, z23, z24, z25, z26, z27, z28, z29, z30, z31, z32, z33, z34, z35, z36, z37, z38, z39, z40, z41, z42, z43, z44, z45, z46, z47, z48, z49, z50, z51, z52, z53, z54, z55, z56, z57, z58, z59] = [
807            1.0, 1.0, -1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0, -1.0, -1.0, -1.0, 1.0, -1.0, 1.0, 1.0,
808            -1.0, -1.0, -1.0, 1.0, 3.0, 1.0, 1.0, 1.0, -1.0, -1.0, -1.0, 1.0, -1.0, 1.0, -3.0, 1.0,
809            -3.0, -1.0, -1.0, 1.0, -1.0, 1.0, -1.0, 1.0, 1.0, 1.0, 1.0, -1.0, 3.0, -1.0, -1.0, 1.0,
810            -1.0, -1.0, 1.0, -1.0, 1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, 1.0,
811        ];
812
813        let [v0, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59] = [
814            5128122.0, 280602.0, 277693.0, 173237.0, 55413.0, 46271.0, 32573.0, 17198.0, 9266.0,
815            8822.0, 8216.0, 4324.0, 4200.0, -3359.0, 2463.0, 2211.0, 2065.0, -1870.0, 1828.0,
816            -1794.0, -1749.0, -1565.0, -1491.0, -1475.0, -1410.0, -1344.0, -1335.0, 1107.0, 1021.0,
817            833.0, 777.0, 671.0, 607.0, 596.0, 491.0, -451.0, 439.0, 422.0, 421.0, -366.0, -351.0,
818            331.0, 315.0, 302.0, -283.0, -229.0, 223.0, 223.0, -220.0, -220.0, -185.0, 181.0,
819            -177.0, 176.0, 166.0, -164.0, 132.0, -119.0, 115.0, 107.0,
820        ];
821
822        // This summation is unrolled for optimization, see #3743
823        ct.0 = v0 * (w0 * d + x0 * ms + y0 * ml + z0 * f).to_radians().sin();
824        ct.1 = v1 * (w1 * d + x1 * ms + y1 * ml + z1 * f).to_radians().sin();
825        ct.2 = v2 * (w2 * d + x2 * ms + y2 * ml + z2 * f).to_radians().sin();
826        ct.3 = v3 * (w3 * d + x3 * ms + y3 * ml + z3 * f).to_radians().sin();
827        ct.4 = v4 * (w4 * d + x4 * ms + y4 * ml + z4 * f).to_radians().sin();
828        ct.5 = v5 * (w5 * d + x5 * ms + y5 * ml + z5 * f).to_radians().sin();
829        ct.6 = v6 * (w6 * d + x6 * ms + y6 * ml + z6 * f).to_radians().sin();
830        ct.7 = v7 * (w7 * d + x7 * ms + y7 * ml + z7 * f).to_radians().sin();
831        ct.8 = v8 * (w8 * d + x8 * ms + y8 * ml + z8 * f).to_radians().sin();
832        ct.9 = v9 * (w9 * d + x9 * ms + y9 * ml + z9 * f).to_radians().sin();
833        ct.10 = v10 * e * (w10 * d + x10 * ms + y10 * ml + z10 * f).to_radians().sin();
834        ct.11 = v11 * (w11 * d + x11 * ms + y11 * ml + z11 * f).to_radians().sin();
835        ct.12 = v12 * (w12 * d + x12 * ms + y12 * ml + z12 * f).to_radians().sin();
836        ct.13 = v13 * e * (w13 * d + x13 * ms + y13 * ml + z13 * f).to_radians().sin();
837        ct.14 = v14 * e * (w14 * d + x14 * ms + y14 * ml + z14 * f).to_radians().sin();
838        ct.15 = v15 * e * (w15 * d + x15 * ms + y15 * ml + z15 * f).to_radians().sin();
839        ct.16 = v16 * e * (w16 * d + x16 * ms + y16 * ml + z16 * f).to_radians().sin();
840        ct.17 = v17 * e * (w17 * d + x17 * ms + y17 * ml + z17 * f).to_radians().sin();
841        ct.18 = v18 * (w18 * d + x18 * ms + y18 * ml + z18 * f).to_radians().sin();
842        ct.19 = v19 * e * (w19 * d + x19 * ms + y19 * ml + z19 * f).to_radians().sin();
843        ct.20 = v20 * (w20 * d + x20 * ms + y20 * ml + z20 * f).to_radians().sin();
844        ct.21 = v21 * e * (w21 * d + x21 * ms + y21 * ml + z21 * f).to_radians().sin();
845        ct.22 = v22 * (w22 * d + x22 * ms + y22 * ml + z22 * f).to_radians().sin();
846        ct.23 = v23 * e * (w23 * d + x23 * ms + y23 * ml + z23 * f).to_radians().sin();
847        ct.24 = v24 * e * (w24 * d + x24 * ms + y24 * ml + z24 * f).to_radians().sin();
848        ct.25 = v25 * e * (w25 * d + x25 * ms + y25 * ml + z25 * f).to_radians().sin();
849        ct.26 = v26 * (w26 * d + x26 * ms + y26 * ml + z26 * f).to_radians().sin();
850        ct.27 = v27 * (w27 * d + x27 * ms + y27 * ml + z27 * f).to_radians().sin();
851        ct.28 = v28 * (w28 * d + x28 * ms + y28 * ml + z28 * f).to_radians().sin();
852        ct.29 = v29 * (w29 * d + x29 * ms + y29 * ml + z29 * f).to_radians().sin();
853        ct.30 = v30 * (w30 * d + x30 * ms + y30 * ml + z30 * f).to_radians().sin();
854        ct.31 = v31 * (w31 * d + x31 * ms + y31 * ml + z31 * f).to_radians().sin();
855        ct.32 = v32 * (w32 * d + x32 * ms + y32 * ml + z32 * f).to_radians().sin();
856        ct.33 = v33 * (w33 * d + x33 * ms + y33 * ml + z33 * f).to_radians().sin();
857        ct.34 = v34 * e * (w34 * d + x34 * ms + y34 * ml + z34 * f).to_radians().sin();
858        ct.35 = v35 * (w35 * d + x35 * ms + y35 * ml + z35 * f).to_radians().sin();
859        ct.36 = v36 * (w36 * d + x36 * ms + y36 * ml + z36 * f).to_radians().sin();
860        ct.37 = v37 * (w37 * d + x37 * ms + y37 * ml + z37 * f).to_radians().sin();
861        ct.38 = v38 * (w38 * d + x38 * ms + y38 * ml + z38 * f).to_radians().sin();
862        ct.39 = v39 * e * (w39 * d + x39 * ms + y39 * ml + z39 * f).to_radians().sin();
863        ct.40 = v40 * e * (w40 * d + x40 * ms + y40 * ml + z40 * f).to_radians().sin();
864        ct.41 = v41 * (w41 * d + x41 * ms + y41 * ml + z41 * f).to_radians().sin();
865        ct.42 = v42 * e * (w42 * d + x42 * ms + y42 * ml + z42 * f).to_radians().sin();
866        ct.43 = v43 * e * e * (w43 * d + x43 * ms + y43 * ml + z43 * f).to_radians().sin();
867        ct.44 = v44 * (w44 * d + x44 * ms + y44 * ml + z44 * f).to_radians().sin();
868        ct.45 = v45 * e * (w45 * d + x45 * ms + y45 * ml + z45 * f).to_radians().sin();
869        ct.46 = v46 * e * (w46 * d + x46 * ms + y46 * ml + z46 * f).to_radians().sin();
870        ct.47 = v47 * e * (w47 * d + x47 * ms + y47 * ml + z47 * f).to_radians().sin();
871        ct.48 = v48 * e * (w48 * d + x48 * ms + y48 * ml + z48 * f).to_radians().sin();
872        ct.49 = v49 * e * (w49 * d + x49 * ms + y49 * ml + z49 * f).to_radians().sin();
873        ct.50 = v50 * (w50 * d + x50 * ms + y50 * ml + z50 * f).to_radians().sin();
874        ct.51 = v51 * e * (w51 * d + x51 * ms + y51 * ml + z51 * f).to_radians().sin();
875        ct.52 = v52 * e * (w52 * d + x52 * ms + y52 * ml + z52 * f).to_radians().sin();
876        ct.53 = v53 * (w53 * d + x53 * ms + y53 * ml + z53 * f).to_radians().sin();
877        ct.54 = v54 * e * (w54 * d + x54 * ms + y54 * ml + z54 * f).to_radians().sin();
878        ct.55 = v55 * (w55 * d + x55 * ms + y55 * ml + z55 * f).to_radians().sin();
879        ct.56 = v56 * (w56 * d + x56 * ms + y56 * ml + z56 * f).to_radians().sin();
880        ct.57 = v57 * (w57 * d + x57 * ms + y57 * ml + z57 * f).to_radians().sin();
881        ct.58 = v58 * e * (w58 * d + x58 * ms + y58 * ml + z58 * f).to_radians().sin();
882        ct.59 = v59 * e * e * (w59 * d + x59 * ms + y59 * ml + z59 * f).to_radians().sin();
883
884        let mut correction = ct.0
885            + ct.1
886            + ct.2
887            + ct.3
888            + ct.4
889            + ct.5
890            + ct.6
891            + ct.7
892            + ct.8
893            + ct.9
894            + ct.10
895            + ct.11
896            + ct.12
897            + ct.13
898            + ct.14
899            + ct.15
900            + ct.16
901            + ct.17
902            + ct.18
903            + ct.19
904            + ct.20
905            + ct.21
906            + ct.22
907            + ct.23
908            + ct.24
909            + ct.25
910            + ct.26
911            + ct.27
912            + ct.28
913            + ct.29
914            + ct.30
915            + ct.31
916            + ct.32
917            + ct.33
918            + ct.34
919            + ct.35
920            + ct.36
921            + ct.37
922            + ct.38
923            + ct.39
924            + ct.40
925            + ct.41
926            + ct.42
927            + ct.43
928            + ct.44
929            + ct.45
930            + ct.46
931            + ct.47
932            + ct.48
933            + ct.49
934            + ct.50
935            + ct.51
936            + ct.52
937            + ct.53
938            + ct.54
939            + ct.55
940            + ct.56
941            + ct.57
942            + ct.58
943            + ct.59;
944
945        correction /= 1_000_000.0;
946
947        let venus = (175.0
948            * ((119.75 + c * 131.849 + f).to_radians().sin()
949                + (119.75 + c * 131.849 - f).to_radians().sin()))
950            / 1_000_000.0;
951
952        let flat_earth = (-2235.0 * l.to_radians().sin()
953            + 127.0 * (l - ml).to_radians().sin()
954            + -115.0 * (l + ml).to_radians().sin())
955            / 1_000_000.0;
956
957        let extra = (382.0 * (313.45 + (c * 481266.484)).to_radians().sin()) / 1_000_000.0;
958
959        correction + venus + flat_earth + extra
960    }
961
962    /// Ecliptic (aka celestial) longitude of the moon (in degrees)
963    ///
964    /// This is not a geocentric or geodetic longitude, it does not take into account the
965    /// rotation of the Earth and is instead measured from the ecliptic and the vernal equinox.
966    ///
967    /// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz,
968    /// originally from _Astronomical Algorithms_ by Jean Meeus, 2nd edn., 1998, pp. 338-342.
969    /// Reference code: <https://github.com/EdReingold/calendar-code2/blob/9afc1f3/calendar.l#L4215-L4278>
970    pub fn lunar_longitude(julian_centuries: f64) -> f64 {
971        let c = julian_centuries;
972        let l = Self::mean_lunar_longitude(c);
973        let d = Self::lunar_elongation(c);
974        let ms = Self::solar_anomaly(c);
975        let ml = Self::lunar_anomaly(c);
976        let f = Self::moon_node(c);
977        let e = 1.0 - (0.002516 * c) - (0.0000074 * c * c);
978
979        let mut ct = (
980            0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
981            0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
982            0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
983            0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
984        );
985
986        let [v0, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58] = [
987            6288774.0, 1274027.0, 658314.0, 213618.0, -185116.0, -114332.0, 58793.0, 57066.0,
988            53322.0, 45758.0, -40923.0, -34720.0, -30383.0, 15327.0, -12528.0, 10980.0, 10675.0,
989            10034.0, 8548.0, -7888.0, -6766.0, -5163.0, 4987.0, 4036.0, 3994.0, 3861.0, 3665.0,
990            -2689.0, -2602.0, 2390.0, -2348.0, 2236.0, -2120.0, -2069.0, 2048.0, -1773.0, -1595.0,
991            1215.0, -1110.0, -892.0, -810.0, 759.0, -713.0, -700.0, 691.0, 596.0, 549.0, 537.0,
992            520.0, -487.0, -399.0, -381.0, 351.0, -340.0, 330.0, 327.0, -323.0, 299.0, 294.0,
993        ];
994        let [w0, w1, w2, w3, w4, w5, w6, w7, w8, w9, w10, w11, w12, w13, w14, w15, w16, w17, w18, w19, w20, w21, w22, w23, w24, w25, w26, w27, w28, w29, w30, w31, w32, w33, w34, w35, w36, w37, w38, w39, w40, w41, w42, w43, w44, w45, w46, w47, w48, w49, w50, w51, w52, w53, w54, w55, w56, w57, w58] = [
995            0.0, 2.0, 2.0, 0.0, 0.0, 0.0, 2.0, 2.0, 2.0, 2.0, 0.0, 1.0, 0.0, 2.0, 0.0, 0.0, 4.0,
996            0.0, 4.0, 2.0, 2.0, 1.0, 1.0, 2.0, 2.0, 4.0, 2.0, 0.0, 2.0, 2.0, 1.0, 2.0, 0.0, 0.0,
997            2.0, 2.0, 2.0, 4.0, 0.0, 3.0, 2.0, 4.0, 0.0, 2.0, 2.0, 2.0, 4.0, 0.0, 4.0, 1.0, 2.0,
998            0.0, 1.0, 3.0, 4.0, 2.0, 0.0, 1.0, 2.0,
999        ];
1000        let [x0, x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11, x12, x13, x14, x15, x16, x17, x18, x19, x20, x21, x22, x23, x24, x25, x26, x27, x28, x29, x30, x31, x32, x33, x34, x35, x36, x37, x38, x39, x40, x41, x42, x43, x44, x45, x46, x47, x48, x49, x50, x51, x52, x53, x54, x55, x56, x57, x58] = [
1001            0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, -1.0, 0.0, -1.0, 1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0,
1002            0.0, 0.0, 1.0, 1.0, 0.0, 1.0, -1.0, 0.0, 0.0, 0.0, 1.0, 0.0, -1.0, 0.0, -2.0, 1.0, 2.0,
1003            -2.0, 0.0, 0.0, -1.0, 0.0, 0.0, 1.0, -1.0, 2.0, 2.0, 1.0, -1.0, 0.0, 0.0, -1.0, 0.0,
1004            1.0, 0.0, 1.0, 0.0, 0.0, -1.0, 2.0, 1.0, 0.0,
1005        ];
1006        let [y0, y1, y2, y3, y4, y5, y6, y7, y8, y9, y10, y11, y12, y13, y14, y15, y16, y17, y18, y19, y20, y21, y22, y23, y24, y25, y26, y27, y28, y29, y30, y31, y32, y33, y34, y35, y36, y37, y38, y39, y40, y41, y42, y43, y44, y45, y46, y47, y48, y49, y50, y51, y52, y53, y54, y55, y56, y57, y58] = [
1007            1.0, -1.0, 0.0, 2.0, 0.0, 0.0, -2.0, -1.0, 1.0, 0.0, -1.0, 0.0, 1.0, 0.0, 1.0, 1.0,
1008            -1.0, 3.0, -2.0, -1.0, 0.0, -1.0, 0.0, 1.0, 2.0, 0.0, -3.0, -2.0, -1.0, -2.0, 1.0, 0.0,
1009            2.0, 0.0, -1.0, 1.0, 0.0, -1.0, 2.0, -1.0, 1.0, -2.0, -1.0, -1.0, -2.0, 0.0, 1.0, 4.0,
1010            0.0, -2.0, 0.0, 2.0, 1.0, -2.0, -3.0, 2.0, 1.0, -1.0, 3.0,
1011        ];
1012        let [z0, z1, z2, z3, z4, z5, z6, z7, z8, z9, z10, z11, z12, z13, z14, z15, z16, z17, z18, z19, z20, z21, z22, z23, z24, z25, z26, z27, z28, z29, z30, z31, z32, z33, z34, z35, z36, z37, z38, z39, z40, z41, z42, z43, z44, z45, z46, z47, z48, z49, z50, z51, z52, z53, z54, z55, z56, z57, z58] = [
1013            0.0, 0.0, 0.0, 0.0, 0.0, 2.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -2.0, 2.0, -2.0, 0.0,
1014            0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 2.0, 0.0, 0.0, 0.0, 0.0, 0.0,
1015            0.0, -2.0, 2.0, 0.0, 2.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -2.0, 0.0, 0.0, 0.0, 0.0, -2.0,
1016            -2.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
1017        ];
1018
1019        // This summation is unrolled for optimization, see #3743
1020        ct.0 = v0 * (w0 * d + x0 * ms + y0 * ml + z0 * f).to_radians().sin();
1021        ct.1 = v1 * (w1 * d + x1 * ms + y1 * ml + z1 * f).to_radians().sin();
1022        ct.2 = v2 * (w2 * d + x2 * ms + y2 * ml + z2 * f).to_radians().sin();
1023        ct.3 = v3 * (w3 * d + x3 * ms + y3 * ml + z3 * f).to_radians().sin();
1024        ct.4 = v4 * e * (w4 * d + x4 * ms + y4 * ml + z4 * f).to_radians().sin();
1025        ct.5 = v5 * (w5 * d + x5 * ms + y5 * ml + z5 * f).to_radians().sin();
1026        ct.6 = v6 * (w6 * d + x6 * ms + y6 * ml + z6 * f).to_radians().sin();
1027        ct.7 = v7 * e * (w7 * d + x7 * ms + y7 * ml + z7 * f).to_radians().sin();
1028        ct.8 = v8 * (w8 * d + x8 * ms + y8 * ml + z8 * f).to_radians().sin();
1029        ct.9 = v9 * e * (w9 * d + x9 * ms + y9 * ml + z9 * f).to_radians().sin();
1030        ct.10 = v10 * e * (w10 * d + x10 * ms + y10 * ml + z10 * f).to_radians().sin();
1031        ct.11 = v11 * (w11 * d + x11 * ms + y11 * ml + z11 * f).to_radians().sin();
1032        ct.12 = v12 * e * (w12 * d + x12 * ms + y12 * ml + z12 * f).to_radians().sin();
1033        ct.13 = v13 * (w13 * d + x13 * ms + y13 * ml + z13 * f).to_radians().sin();
1034        ct.14 = v14 * (w14 * d + x14 * ms + y14 * ml + z14 * f).to_radians().sin();
1035        ct.15 = v15 * (w15 * d + x15 * ms + y15 * ml + z15 * f).to_radians().sin();
1036        ct.16 = v16 * (w16 * d + x16 * ms + y16 * ml + z16 * f).to_radians().sin();
1037        ct.17 = v17 * (w17 * d + x17 * ms + y17 * ml + z17 * f).to_radians().sin();
1038        ct.18 = v18 * (w18 * d + x18 * ms + y18 * ml + z18 * f).to_radians().sin();
1039        ct.19 = v19 * e * (w19 * d + x19 * ms + y19 * ml + z19 * f).to_radians().sin();
1040        ct.20 = v20 * e * (w20 * d + x20 * ms + y20 * ml + z20 * f).to_radians().sin();
1041        ct.21 = v21 * (w21 * d + x21 * ms + y21 * ml + z21 * f).to_radians().sin();
1042        ct.22 = v22 * e * (w22 * d + x22 * ms + y22 * ml + z22 * f).to_radians().sin();
1043        ct.23 = v23 * e * (w23 * d + x23 * ms + y23 * ml + z23 * f).to_radians().sin();
1044        ct.24 = v24 * (w24 * d + x24 * ms + y24 * ml + z24 * f).to_radians().sin();
1045        ct.25 = v25 * (w25 * d + x25 * ms + y25 * ml + z25 * f).to_radians().sin();
1046        ct.26 = v26 * (w26 * d + x26 * ms + y26 * ml + z26 * f).to_radians().sin();
1047        ct.27 = v27 * e * (w27 * d + x27 * ms + y27 * ml + z27 * f).to_radians().sin();
1048        ct.28 = v28 * (w28 * d + x28 * ms + y28 * ml + z28 * f).to_radians().sin();
1049        ct.29 = v29 * e * (w29 * d + x29 * ms + y29 * ml + z29 * f).to_radians().sin();
1050        ct.30 = v30 * (w30 * d + x30 * ms + y30 * ml + z30 * f).to_radians().sin();
1051        ct.31 = v31 * e * e * (w31 * d + x31 * ms + y31 * ml + z31 * f).to_radians().sin();
1052        ct.32 = v32 * e * (w32 * d + x32 * ms + y32 * ml + z32 * f).to_radians().sin();
1053        ct.33 = v33 * e * e * (w33 * d + x33 * ms + y33 * ml + z33 * f).to_radians().sin();
1054        ct.34 = v34 * e * e * (w34 * d + x34 * ms + y34 * ml + z34 * f).to_radians().sin();
1055        ct.35 = v35 * (w35 * d + x35 * ms + y35 * ml + z35 * f).to_radians().sin();
1056        ct.36 = v36 * (w36 * d + x36 * ms + y36 * ml + z36 * f).to_radians().sin();
1057        ct.37 = v37 * e * (w37 * d + x37 * ms + y37 * ml + z37 * f).to_radians().sin();
1058        ct.38 = v38 * (w38 * d + x38 * ms + y38 * ml + z38 * f).to_radians().sin();
1059        ct.39 = v39 * (w39 * d + x39 * ms + y39 * ml + z39 * f).to_radians().sin();
1060        ct.40 = v40 * e * (w40 * d + x40 * ms + y40 * ml + z40 * f).to_radians().sin();
1061        ct.41 = v41 * e * (w41 * d + x41 * ms + y41 * ml + z41 * f).to_radians().sin();
1062        ct.42 = v42 * e * e * (w42 * d + x42 * ms + y42 * ml + z42 * f).to_radians().sin();
1063        ct.43 = v43 * e * e * (w43 * d + x43 * ms + y43 * ml + z43 * f).to_radians().sin();
1064        ct.44 = v44 * e * (w44 * d + x44 * ms + y44 * ml + z44 * f).to_radians().sin();
1065        ct.45 = v45 * e * (w45 * d + x45 * ms + y45 * ml + z45 * f).to_radians().sin();
1066        ct.46 = v46 * (w46 * d + x46 * ms + y46 * ml + z46 * f).to_radians().sin();
1067        ct.47 = v47 * (w47 * d + x47 * ms + y47 * ml + z47 * f).to_radians().sin();
1068        ct.48 = v48 * e * (w48 * d + x48 * ms + y48 * ml + z48 * f).to_radians().sin();
1069        ct.49 = v49 * (w49 * d + x49 * ms + y49 * ml + z49 * f).to_radians().sin();
1070        ct.50 = v50 * e * (w50 * d + x50 * ms + y50 * ml + z50 * f).to_radians().sin();
1071        ct.51 = v51 * (w51 * d + x51 * ms + y51 * ml + z51 * f).to_radians().sin();
1072        ct.52 = v52 * e * (w52 * d + x52 * ms + y52 * ml + z52 * f).to_radians().sin();
1073        ct.53 = v53 * (w53 * d + x53 * ms + y53 * ml + z53 * f).to_radians().sin();
1074        ct.54 = v54 * (w54 * d + x54 * ms + y54 * ml + z54 * f).to_radians().sin();
1075        ct.55 = v55 * e * (w55 * d + x55 * ms + y55 * ml + z55 * f).to_radians().sin();
1076        ct.56 = v56 * e * e * (w56 * d + x56 * ms + y56 * ml + z56 * f).to_radians().sin();
1077        ct.57 = v57 * e * (w57 * d + x57 * ms + y57 * ml + z57 * f).to_radians().sin();
1078        ct.58 = v58 * (w58 * d + x58 * ms + y58 * ml + z58 * f).to_radians().sin();
1079
1080        let mut correction = ct.0
1081            + ct.1
1082            + ct.2
1083            + ct.3
1084            + ct.4
1085            + ct.5
1086            + ct.6
1087            + ct.7
1088            + ct.8
1089            + ct.9
1090            + ct.10
1091            + ct.11
1092            + ct.12
1093            + ct.13
1094            + ct.14
1095            + ct.15
1096            + ct.16
1097            + ct.17
1098            + ct.18
1099            + ct.19
1100            + ct.20
1101            + ct.21
1102            + ct.22
1103            + ct.23
1104            + ct.24
1105            + ct.25
1106            + ct.26
1107            + ct.27
1108            + ct.28
1109            + ct.29
1110            + ct.30
1111            + ct.31
1112            + ct.32
1113            + ct.33
1114            + ct.34
1115            + ct.35
1116            + ct.36
1117            + ct.37
1118            + ct.38
1119            + ct.39
1120            + ct.40
1121            + ct.41
1122            + ct.42
1123            + ct.43
1124            + ct.44
1125            + ct.45
1126            + ct.46
1127            + ct.47
1128            + ct.48
1129            + ct.49
1130            + ct.50
1131            + ct.51
1132            + ct.52
1133            + ct.53
1134            + ct.54
1135            + ct.55
1136            + ct.56
1137            + ct.57
1138            + ct.58;
1139
1140        correction /= 1000000.0;
1141        let venus = 3958.0 / 1000000.0 * (119.75 + c * 131.849).to_radians().sin();
1142        let jupiter = 318.0 / 1000000.0 * (53.09 + c * 479264.29).to_radians().sin();
1143        let flat_earth = 1962.0 / 1000000.0 * (l - f).to_radians().sin();
1144        (l + correction + venus + jupiter + flat_earth + Self::nutation(julian_centuries))
1145            .rem_euclid(360.0)
1146    }
1147
1148    /// Mean longitude of the moon (in degrees) at a given Moment in Julian centuries.
1149    ///
1150    /// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz,
1151    /// originally from _Astronomical Algorithms_ by Jean Meeus, 2nd edn., 1998, pp. 336-340.
1152    /// Reference code: <https://github.com/EdReingold/calendar-code2/blob/9afc1f3/calendar.l#L4148-L4158>
1153    fn mean_lunar_longitude(c: f64) -> f64 {
1154        // This polynomial is written out manually instead of using a fn like pow for optimization, see #3743
1155        let n = 218.3164477
1156            + c * (481267.88123421 - 0.0015786 * c + c * c / 538841.0 - c * c * c / 65194000.0);
1157
1158        n.rem_euclid(360.0)
1159    }
1160
1161    /// Closest fixed date on or after `date` on the eve of which crescent moon first became visible at `location`.
1162    ///
1163    /// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz.
1164    /// Reference lisp code: <https://github.com/EdReingold/calendar-code2/blob/9afc1f3/calendar.l#L6883-L6896>
1165    pub fn phasis_on_or_after(
1166        date: RataDie,
1167        location: Location,
1168        lunar_phase: Option<f64>,
1169    ) -> RataDie {
1170        let lunar_phase =
1171            lunar_phase.unwrap_or_else(|| Self::calculate_new_moon_at_or_before(date));
1172        let age = date.to_f64_date() - lunar_phase;
1173        let tau = if age <= 4.0 || Self::visible_crescent((date - 1).as_moment(), location) {
1174            lunar_phase + 29.0 // Next new moon
1175        } else {
1176            date.to_f64_date()
1177        };
1178        next_moment(Moment::new(tau), location, Self::visible_crescent)
1179    }
1180
1181    /// Closest fixed date on or before `date` when crescent moon first became visible at `location`.
1182    /// Lunar phase is the result of calling `lunar_phase(moment, julian_centuries)` in an earlier function.
1183    ///
1184    /// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz.
1185    /// Reference lisp code: <https://github.com/EdReingold/calendar-code2/blob/9afc1f3/calendar.l#L6868-L6881>
1186    pub fn phasis_on_or_before(
1187        date: RataDie,
1188        location: Location,
1189        lunar_phase: Option<f64>,
1190    ) -> RataDie {
1191        let lunar_phase =
1192            lunar_phase.unwrap_or_else(|| Self::calculate_new_moon_at_or_before(date));
1193        let age = date.to_f64_date() - lunar_phase;
1194        let tau = if age <= 3.0 && !Self::visible_crescent((date).as_moment(), location) {
1195            lunar_phase - 30.0 // Previous new moon
1196        } else {
1197            lunar_phase
1198        };
1199        next_moment(Moment::new(tau), location, Self::visible_crescent)
1200    }
1201
1202    /// Calculate the day that the new moon occurred on or before the given date.
1203    pub fn calculate_new_moon_at_or_before(date: RataDie) -> f64 {
1204        Self::lunar_phase_at_or_before(0.0, date.as_moment())
1205            .inner()
1206            .floor()
1207    }
1208
1209    /// Length of the lunar month containing `date` in days, based on observability at `location`.
1210    /// Calculates the month length for the Islamic Observational Calendar
1211    /// Can return 31 days due to the imprecise nature of trying to approximate an observational calendar. (See page 294 of the Calendrical Calculations book)
1212    ///
1213    /// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz.
1214    /// Reference lisp code: <https://github.com/EdReingold/calendar-code2/blob/9afc1f3/calendar.l#L7068-L7074>
1215    pub fn month_length(date: RataDie, location: Location) -> u8 {
1216        let moon = Self::phasis_on_or_after(date + 1, location, None);
1217        let prev = Self::phasis_on_or_before(date, location, None);
1218
1219        debug_assert!(moon > prev);
1220        debug_assert!(moon - prev < u8::MAX.into());
1221        (moon - prev) as u8
1222    }
1223
1224    /// Lunar elongation (the moon's angular distance east of the Sun) at a given Moment in Julian centuries
1225    ///
1226    /// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz,
1227    /// originally from _Astronomical Algorithms_ by Jean Meeus, 2nd edn., 1998, p. 338.
1228    /// Reference code: <https://github.com/EdReingold/calendar-code2/blob/9afc1f3/calendar.l#L4160-L4170>
1229    fn lunar_elongation(c: f64) -> f64 {
1230        // This polynomial is written out manually instead of using a fn like pow for optimization, see #3743
1231        (297.85019021 + 445267.1114034 * c - 0.0018819 * c * c + c * c * c / 545868.0
1232            - c * c * c * c / 113065000.0)
1233            .rem_euclid(360.0)
1234    }
1235
1236    /// Altitude of the moon (in degrees) at a given moment
1237    ///
1238    /// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz,
1239    /// originally from _Astronomical Algorithms_ by Jean Meeus, 2nd edn., 1998.
1240    /// Lisp code reference: <https://github.com/EdReingold/calendar-code2/blob/9afc1f3/calendar.l#L4537>
1241    pub fn lunar_altitude(moment: Moment, location: Location) -> f64 {
1242        let phi = location.latitude;
1243        let psi = location.longitude;
1244        let c = Self::julian_centuries(moment);
1245        let lambda = Self::lunar_longitude(c);
1246        let beta = Self::lunar_latitude(c);
1247        let alpha = Self::right_ascension(moment, beta, lambda);
1248        let delta = Self::declination(moment, beta, lambda);
1249        let theta0 = Self::sidereal_from_moment(moment);
1250        let cap_h = (theta0 + psi - alpha).rem_euclid(360.0);
1251
1252        let altitude = (phi.to_radians().sin() * delta.to_radians().sin()
1253            + phi.to_radians().cos() * delta.to_radians().cos() * cap_h.to_radians().cos())
1254        .asin()
1255        .to_degrees();
1256
1257        (altitude + 180.0).rem_euclid(360.0) - 180.0
1258    }
1259
1260    /// Distance to the moon in meters at the given moment.
1261    ///
1262    /// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz,
1263    /// originally from _Astronomical Algorithms_ by Jean Meeus, 2nd edn., 1998, pp. 338-342.
1264    /// Lisp code reference: <https://github.com/EdReingold/calendar-code2/blob/9afc1f3/calendar.l#L4568-L4617>
1265    #[allow(dead_code)]
1266    pub fn lunar_distance(moment: Moment) -> f64 {
1267        let c = Self::julian_centuries(moment);
1268        let cap_d = Self::lunar_elongation(c);
1269        let cap_m = Self::solar_anomaly(c);
1270        let cap_m_prime = Self::lunar_anomaly(c);
1271        let cap_f = Self::moon_node(c);
1272        let cap_e = 1.0 - (0.002516 * c) - (0.0000074 * c * c);
1273
1274        let args_lunar_elongation = [
1275            0.0, 2.0, 2.0, 0.0, 0.0, 0.0, 2.0, 2.0, 2.0, 2.0, 0.0, 1.0, 0.0, 2.0, 0.0, 0.0, 4.0,
1276            0.0, 4.0, 2.0, 2.0, 1.0, 1.0, 2.0, 2.0, 4.0, 2.0, 0.0, 2.0, 2.0, 1.0, 2.0, 0.0, 0.0,
1277            2.0, 2.0, 2.0, 4.0, 0.0, 3.0, 2.0, 4.0, 0.0, 2.0, 2.0, 2.0, 4.0, 0.0, 4.0, 1.0, 2.0,
1278            0.0, 1.0, 3.0, 4.0, 2.0, 0.0, 1.0, 2.0, 2.0,
1279        ];
1280
1281        let args_solar_anomaly = [
1282            0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, -1.0, 0.0, -1.0, 1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0,
1283            0.0, 0.0, 1.0, 1.0, 0.0, 1.0, -1.0, 0.0, 0.0, 0.0, 1.0, 0.0, -1.0, 0.0, -2.0, 1.0, 2.0,
1284            -2.0, 0.0, 0.0, -1.0, 0.0, 0.0, 1.0, -1.0, 2.0, 2.0, 1.0, -1.0, 0.0, 0.0, -1.0, 0.0,
1285            1.0, 0.0, 1.0, 0.0, 0.0, -1.0, 2.0, 1.0, 0.0, 0.0,
1286        ];
1287
1288        let args_lunar_anomaly = [
1289            1.0, -1.0, 0.0, 2.0, 0.0, 0.0, -2.0, -1.0, 1.0, 0.0, -1.0, 0.0, 1.0, 0.0, 1.0, 1.0,
1290            -1.0, 3.0, -2.0, -1.0, 0.0, -1.0, 0.0, 1.0, 2.0, 0.0, -3.0, -2.0, -1.0, -2.0, 1.0, 0.0,
1291            2.0, 0.0, -1.0, 1.0, 0.0, -1.0, 2.0, -1.0, 1.0, -2.0, -1.0, -1.0, -2.0, 0.0, 1.0, 4.0,
1292            0.0, -2.0, 0.0, 2.0, 1.0, -2.0, -3.0, 2.0, 1.0, -1.0, 3.0, -1.0,
1293        ];
1294
1295        let args_moon_node = [
1296            0.0, 0.0, 0.0, 0.0, 0.0, 2.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -2.0, 2.0, -2.0, 0.0,
1297            0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 2.0, 0.0, 0.0, 0.0, 0.0, 0.0,
1298            0.0, -2.0, 2.0, 0.0, 2.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -2.0, 0.0, 0.0, 0.0, 0.0, -2.0,
1299            -2.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -2.0,
1300        ];
1301
1302        let cosine_coeff = [
1303            -20905355.0,
1304            -3699111.0,
1305            -2955968.0,
1306            -569925.0,
1307            48888.0,
1308            -3149.0,
1309            246158.0,
1310            -152138.0,
1311            -170733.0,
1312            -204586.0,
1313            -129620.0,
1314            108743.0,
1315            104755.0,
1316            10321.0,
1317            0.0,
1318            79661.0,
1319            -34782.0,
1320            -23210.0,
1321            -21636.0,
1322            24208.0,
1323            30824.0,
1324            -8379.0,
1325            -16675.0,
1326            -12831.0,
1327            -10445.0,
1328            -11650.0,
1329            14403.0,
1330            -7003.0,
1331            0.0,
1332            10056.0,
1333            6322.0,
1334            -9884.0,
1335            5751.0,
1336            0.0,
1337            -4950.0,
1338            4130.0,
1339            0.0,
1340            -3958.0,
1341            0.0,
1342            3258.0,
1343            2616.0,
1344            -1897.0,
1345            -2117.0,
1346            2354.0,
1347            0.0,
1348            0.0,
1349            -1423.0,
1350            -1117.0,
1351            -1571.0,
1352            -1739.0,
1353            0.0,
1354            -4421.0,
1355            0.0,
1356            0.0,
1357            0.0,
1358            0.0,
1359            1165.0,
1360            0.0,
1361            0.0,
1362            8752.0,
1363        ];
1364
1365        let correction: f64 = cosine_coeff
1366            .iter()
1367            .zip(args_lunar_elongation.iter())
1368            .zip(args_solar_anomaly.iter())
1369            .zip(args_lunar_anomaly.iter())
1370            .zip(args_moon_node.iter())
1371            .map(|((((&v, &w), &x), &y), &z)| {
1372                v * cap_e.powf(x.abs())
1373                    * (w * cap_d + x * cap_m + y * cap_m_prime + z * cap_f)
1374                        .to_radians()
1375                        .cos()
1376            })
1377            .sum();
1378
1379        385000560.0 + correction
1380    }
1381
1382    /// The parallax of the moon, meaning the difference in angle of the direction of the moon
1383    /// as measured from a given location and from the center of the Earth, in degrees.
1384    /// Note: the location is encoded as the `lunar_altitude_val` which is the result of `lunar_altitude(moment,location)`.
1385    /// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz,
1386    /// originally from _Astronomical Algorithms_ by Jean Meeus, 2nd edn., 1998.
1387    /// Lisp code reference: <https://github.com/EdReingold/calendar-code2/blob/9afc1f3/calendar.l#L4619-L4628>
1388    pub fn lunar_parallax(lunar_altitude_val: f64, moment: Moment) -> f64 {
1389        let cap_delta = Self::lunar_distance(moment);
1390        let alt = 6378140.0 / cap_delta;
1391        let arg = alt * lunar_altitude_val.to_radians().cos();
1392        arg.asin().to_degrees().rem_euclid(360.0)
1393    }
1394
1395    /// Topocentric altitude of the moon.
1396    ///
1397    /// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz.
1398    /// Lisp code reference: <https://github.com/EdReingold/calendar-code2/blob/9afc1f3/calendar.l#L4630-L4636>
1399    fn topocentric_lunar_altitude(moment: Moment, location: Location) -> f64 {
1400        let lunar_altitude = Self::lunar_altitude(moment, location);
1401        lunar_altitude - Self::lunar_parallax(lunar_altitude, moment)
1402    }
1403
1404    /// Observed altitude of upper limb of moon at moment at location.
1405    /// /// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz.
1406    /// Lisp code reference: <https://github.com/EdReingold/calendar-code2/blob/9afc1f3/calendar.l#L4646-L4653>
1407    fn observed_lunar_altitude(moment: Moment, location: Location) -> f64 {
1408        let r = Self::topocentric_lunar_altitude(moment, location);
1409        let y = Self::refraction(location);
1410        let z = 16.0 / 60.0;
1411
1412        r + y + z
1413    }
1414
1415    /// Average anomaly of the sun (in degrees) at a given Moment in Julian centuries.
1416    /// See: <https://en.wikipedia.org/wiki/Mean_anomaly>
1417    ///
1418    /// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz,
1419    /// originally from _Astronomical Algorithms_ by Jean Meeus, 2nd edn., 1998, p. 338.
1420    /// Lisp code reference: <https://github.com/EdReingold/calendar-code2/blob/9afc1f3/calendar.l#L4172-L4182>
1421    fn solar_anomaly(c: f64) -> f64 {
1422        // This polynomial is written out manually instead of using a fn like pow for optimization, see #3743
1423        (357.5291092 + 35999.0502909 * c - 0.0001536 * c * c + c * c * c / 24490000.0)
1424            .rem_euclid(360.0)
1425    }
1426
1427    /// Average anomaly of the moon (in degrees) at a given Moment in Julian centuries
1428    /// See: <https://en.wikipedia.org/wiki/Mean_anomaly>
1429    ///
1430    /// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz,
1431    /// originally from _Astronomical Algorithms_ by Jean Meeus, 2nd edn., 1998, p. 338.
1432    /// Lisp code reference: <https://github.com/EdReingold/calendar-code2/blob/9afc1f3/calendar.l#L4184-L4194>
1433    fn lunar_anomaly(c: f64) -> f64 {
1434        // This polynomial is written out manually instead of using a fn like pow for optimization, see #3743
1435        (134.9633964 + 477198.8675055 * c + 0.0087414 * c * c + c * c * c / 69699.0
1436            - c * c * c * c / 14712000.0)
1437            .rem_euclid(360.0)
1438    }
1439
1440    /// The moon's argument of latitude, in degrees, at the moment given by `c` in Julian centuries.
1441    /// The argument of latitude is used to define the position of a body moving in a Kepler orbit.
1442    ///
1443    /// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz,
1444    /// originally from _Astronomical Algorithms_ by Jean Meeus, 2nd edn., 1998, p. 338.
1445    /// Lisp code reference: <https://github.com/EdReingold/calendar-code2/blob/9afc1f3/calendar.l#L4196-L4206>
1446    fn moon_node(c: f64) -> f64 {
1447        // This polynomial is written out manually instead of using a fn like pow for optimization, see #3743
1448        (93.2720950 + 483202.0175233 * c - 0.0036539 * c * c - c * c * c / 3526000.0
1449            + c * c * c * c / 863310000.0)
1450            .rem_euclid(360.0)
1451    }
1452
1453    /// Standard time of moonset on the date of the given moment and at the given location.
1454    /// Returns `None` if there is no such moonset.
1455    ///
1456    /// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz.
1457    /// Lisp code reference: <https://github.com/EdReingold/calendar-code2/blob/9afc1f3/calendar.l#L4655-L4681>
1458    #[allow(dead_code)] // TODO: Remove dead code tag after use
1459    fn moonset(date: Moment, location: Location) -> Option<Moment> {
1460        let moment = Location::universal_from_standard(date, location);
1461        let waxing = Self::lunar_phase(date, Self::julian_centuries(date)) < 180.0;
1462        let alt = Self::observed_lunar_altitude(moment, location);
1463        let lat = location.latitude;
1464        let offset = alt / (4.0 * (90.0 - lat.abs()));
1465
1466        let approx = if waxing {
1467            if offset > 0.0 {
1468                moment + offset
1469            } else {
1470                moment + 1.0 + offset
1471            }
1472        } else {
1473            moment - offset + 0.5
1474        };
1475
1476        let set = Moment::new(binary_search(
1477            approx.inner() - (6.0 / 24.0),
1478            approx.inner() + (6.0 / 24.0),
1479            |x| Self::observed_lunar_altitude(Moment::new(x), location) < 0.0,
1480            1.0 / 24.0 / 60.0,
1481        ));
1482
1483        if set < moment + 1.0 {
1484            let std = Moment::new(
1485                Location::standard_from_universal(set, location)
1486                    .inner()
1487                    .max(date.inner()),
1488            );
1489            debug_assert!(std >= date, "std should not be less than date");
1490            if std < date {
1491                return None;
1492            }
1493            Some(std)
1494        } else {
1495            None
1496        }
1497    }
1498
1499    /// Standard time of sunset on the date of the given moment and at the given location.
1500    /// Returns `None` if there is no such sunset.
1501    ///
1502    /// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz.
1503    /// Lisp code reference: <https://github.com/EdReingold/calendar-code2/blob/9afc1f3/calendar.l#L3700-L3706>
1504    #[allow(dead_code)]
1505    pub fn sunset(date: Moment, location: Location) -> Option<Moment> {
1506        let alpha = Self::refraction(location) + (16.0 / 60.0);
1507        Self::dusk(date.inner(), location, alpha)
1508    }
1509
1510    /// Time between sunset and moonset on the date of the given moment at the given location.
1511    /// Returns `None` if there is no such sunset.
1512    ///
1513    /// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz.
1514    /// Lisp code reference: <https://github.com/EdReingold/calendar-code2/blob/9afc1f3/calendar.l#L6770-L6778>
1515    pub fn moonlag(date: Moment, location: Location) -> Option<f64> {
1516        if let Some(sun) = Self::sunset(date, location) {
1517            if let Some(moon) = Self::moonset(date, location) {
1518                Some(moon - sun)
1519            } else {
1520                Some(1.0)
1521            }
1522        } else {
1523            None
1524        }
1525    }
1526
1527    /// Longitudinal nutation (periodic variation in the inclination of the Earth's axis) at a given Moment.
1528    /// Argument comes from the result of calling `Self::julian_centuries(moment)` in an earlier function.
1529    ///
1530    /// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz.
1531    /// Reference code: <https://github.com/EdReingold/calendar-code2/blob/9afc1f3/calendar.l#L4037-L4047>
1532    fn nutation(julian_centuries: f64) -> f64 {
1533        // This polynomial is written out manually instead of using a fn like pow for optimization, see #3743
1534        let c = julian_centuries;
1535        let a = 124.90 - 1934.134 * c + 0.002063 * c * c;
1536        let b = 201.11 + 72001.5377 * c + 0.00057 * c * c;
1537        -0.004778 * a.to_radians().sin() - 0.0003667 * b.to_radians().sin()
1538    }
1539
1540    /// The phase of the moon at a given Moment, defined as the difference in longitudes
1541    /// of the sun and the moon.
1542    ///
1543    /// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz.
1544    /// Reference code: <https://github.com/EdReingold/calendar-code2/blob/9afc1f3/calendar.l#L4397-L4414>
1545    pub fn lunar_phase(moment: Moment, julian_centuries: f64) -> f64 {
1546        let t0 = NEW_MOON_ZERO;
1547        let maybe_n = i64_to_i32(div_euclid_f64(moment - t0, MEAN_SYNODIC_MONTH).round() as i64);
1548        debug_assert!(
1549            maybe_n.is_ok(),
1550            "Lunar phase moment should be in range of i32"
1551        );
1552        let n = maybe_n.unwrap_or_else(|e| e.saturate());
1553        let a = (Self::lunar_longitude(julian_centuries) - Self::solar_longitude(julian_centuries))
1554            .rem_euclid(360.0);
1555        let b = 360.0 * ((moment - Self::nth_new_moon(n)) / MEAN_SYNODIC_MONTH).rem_euclid(1.0);
1556        if (a - b).abs() > 180.0 {
1557            b
1558        } else {
1559            a
1560        }
1561    }
1562
1563    /// Moment in universal time of the last time at or before the given moment when the lunar phase
1564    /// was equal to the `phase` given.
1565    ///
1566    /// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz.
1567    /// Reference code: <https://github.com/EdReingold/calendar-code2/blob/9afc1f3/calendar.l#L4416-L4427>
1568    pub fn lunar_phase_at_or_before(phase: f64, moment: Moment) -> Moment {
1569        let julian_centuries = Self::julian_centuries(moment);
1570        let tau = moment.inner()
1571            - (MEAN_SYNODIC_MONTH / 360.0)
1572                * ((Self::lunar_phase(moment, julian_centuries) - phase) % 360.0);
1573        let a = tau - 2.0;
1574        let b = moment.inner().min(tau + 2.0);
1575
1576        let lunar_phase_f64 = |x: f64| -> f64 {
1577            Self::lunar_phase(Moment::new(x), Self::julian_centuries(Moment::new(x)))
1578        };
1579
1580        Moment::new(invert_angular(lunar_phase_f64, phase, (a, b)))
1581    }
1582
1583    /// The longitude of the Sun at a given Moment in degrees.
1584    /// Moment is not directly used but is enconded from the argument `julian_centuries` which is the result of calling `Self::julian_centuries(moment) in an earlier function`.
1585    ///
1586    /// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz,
1587    /// originally from "Planetary Programs and Tables from -4000 to +2800" by Bretagnon & Simon, 1986.
1588    /// Reference code: <https://github.com/EdReingold/calendar-code2/blob/9afc1f3/calendar.l#L3985-L4035>
1589    pub fn solar_longitude(julian_centuries: f64) -> f64 {
1590        let c: f64 = julian_centuries;
1591        let mut lt = (
1592            0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
1593            0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
1594            0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
1595        );
1596        let [x0, x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11, x12, x13, x14, x15, x16, x17, x18, x19, x20, x21, x22, x23, x24, x25, x26, x27, x28, x29, x30, x31, x32, x33, x34, x35, x36, x37, x38, x39, x40, x41, x42, x43, x44, x45, x46, x47, x48] = [
1597            403406.0, 195207.0, 119433.0, 112392.0, 3891.0, 2819.0, 1721.0, 660.0, 350.0, 334.0,
1598            314.0, 268.0, 242.0, 234.0, 158.0, 132.0, 129.0, 114.0, 99.0, 93.0, 86.0, 78.0, 72.0,
1599            68.0, 64.0, 46.0, 38.0, 37.0, 32.0, 29.0, 28.0, 27.0, 27.0, 25.0, 24.0, 21.0, 21.0,
1600            20.0, 18.0, 17.0, 14.0, 13.0, 13.0, 13.0, 12.0, 10.0, 10.0, 10.0, 10.0,
1601        ];
1602        let [y0, y1, y2, y3, y4, y5, y6, y7, y8, y9, y10, y11, y12, y13, y14, y15, y16, y17, y18, y19, y20, y21, y22, y23, y24, y25, y26, y27, y28, y29, y30, y31, y32, y33, y34, y35, y36, y37, y38, y39, y40, y41, y42, y43, y44, y45, y46, y47, y48] = [
1603            270.54861, 340.19128, 63.91854, 331.26220, 317.843, 86.631, 240.052, 310.26, 247.23,
1604            260.87, 297.82, 343.14, 166.79, 81.53, 3.50, 132.75, 182.95, 162.03, 29.8, 266.4,
1605            249.2, 157.6, 257.8, 185.1, 69.9, 8.0, 197.1, 250.4, 65.3, 162.7, 341.5, 291.6, 98.5,
1606            146.7, 110.0, 5.2, 342.6, 230.9, 256.1, 45.3, 242.9, 115.2, 151.8, 285.3, 53.3, 126.6,
1607            205.7, 85.9, 146.1,
1608        ];
1609        let [z0, z1, z2, z3, z4, z5, z6, z7, z8, z9, z10, z11, z12, z13, z14, z15, z16, z17, z18, z19, z20, z21, z22, z23, z24, z25, z26, z27, z28, z29, z30, z31, z32, z33, z34, z35, z36, z37, z38, z39, z40, z41, z42, z43, z44, z45, z46, z47, z48] = [
1610            0.9287892,
1611            35999.1376958,
1612            35999.4089666,
1613            35998.7287385,
1614            71998.20261,
1615            71998.4403,
1616            36000.35726,
1617            71997.4812,
1618            32964.4678,
1619            -19.4410,
1620            445267.1117,
1621            45036.8840,
1622            3.1008,
1623            22518.4434,
1624            -19.9739,
1625            65928.9345,
1626            9038.0293,
1627            3034.7684,
1628            33718.148,
1629            3034.448,
1630            -2280.773,
1631            29929.992,
1632            31556.493,
1633            149.588,
1634            9037.750,
1635            107997.405,
1636            -4444.176,
1637            151.771,
1638            67555.316,
1639            31556.080,
1640            -4561.540,
1641            107996.706,
1642            1221.655,
1643            62894.167,
1644            31437.369,
1645            14578.298,
1646            -31931.757,
1647            34777.243,
1648            1221.999,
1649            62894.511,
1650            -4442.039,
1651            107997.909,
1652            119.066,
1653            16859.071,
1654            -4.578,
1655            26895.292,
1656            -39.127,
1657            12297.536,
1658            90073.778,
1659        ];
1660
1661        // This summation is unrolled for optimization, see #3743
1662        lt.0 = x0 * (y0 + z0 * c).to_radians().sin();
1663        lt.1 = x1 * (y1 + z1 * c).to_radians().sin();
1664        lt.2 = x2 * (y2 + z2 * c).to_radians().sin();
1665        lt.3 = x3 * (y3 + z3 * c).to_radians().sin();
1666        lt.4 = x4 * (y4 + z4 * c).to_radians().sin();
1667        lt.5 = x5 * (y5 + z5 * c).to_radians().sin();
1668        lt.6 = x6 * (y6 + z6 * c).to_radians().sin();
1669        lt.7 = x7 * (y7 + z7 * c).to_radians().sin();
1670        lt.8 = x8 * (y8 + z8 * c).to_radians().sin();
1671        lt.9 = x9 * (y9 + z9 * c).to_radians().sin();
1672        lt.10 = x10 * (y10 + z10 * c).to_radians().sin();
1673        lt.11 = x11 * (y11 + z11 * c).to_radians().sin();
1674        lt.12 = x12 * (y12 + z12 * c).to_radians().sin();
1675        lt.13 = x13 * (y13 + z13 * c).to_radians().sin();
1676        lt.14 = x14 * (y14 + z14 * c).to_radians().sin();
1677        lt.15 = x15 * (y15 + z15 * c).to_radians().sin();
1678        lt.16 = x16 * (y16 + z16 * c).to_radians().sin();
1679        lt.17 = x17 * (y17 + z17 * c).to_radians().sin();
1680        lt.18 = x18 * (y18 + z18 * c).to_radians().sin();
1681        lt.19 = x19 * (y19 + z19 * c).to_radians().sin();
1682        lt.20 = x20 * (y20 + z20 * c).to_radians().sin();
1683        lt.21 = x21 * (y21 + z21 * c).to_radians().sin();
1684        lt.22 = x22 * (y22 + z22 * c).to_radians().sin();
1685        lt.23 = x23 * (y23 + z23 * c).to_radians().sin();
1686        lt.24 = x24 * (y24 + z24 * c).to_radians().sin();
1687        lt.25 = x25 * (y25 + z25 * c).to_radians().sin();
1688        lt.26 = x26 * (y26 + z26 * c).to_radians().sin();
1689        lt.27 = x27 * (y27 + z27 * c).to_radians().sin();
1690        lt.28 = x28 * (y28 + z28 * c).to_radians().sin();
1691        lt.29 = x29 * (y29 + z29 * c).to_radians().sin();
1692        lt.30 = x30 * (y30 + z30 * c).to_radians().sin();
1693        lt.31 = x31 * (y31 + z31 * c).to_radians().sin();
1694        lt.32 = x32 * (y32 + z32 * c).to_radians().sin();
1695        lt.33 = x33 * (y33 + z33 * c).to_radians().sin();
1696        lt.34 = x34 * (y34 + z34 * c).to_radians().sin();
1697        lt.35 = x35 * (y35 + z35 * c).to_radians().sin();
1698        lt.36 = x36 * (y36 + z36 * c).to_radians().sin();
1699        lt.37 = x37 * (y37 + z37 * c).to_radians().sin();
1700        lt.38 = x38 * (y38 + z38 * c).to_radians().sin();
1701        lt.39 = x39 * (y39 + z39 * c).to_radians().sin();
1702        lt.40 = x40 * (y40 + z40 * c).to_radians().sin();
1703        lt.41 = x41 * (y41 + z41 * c).to_radians().sin();
1704        lt.42 = x42 * (y42 + z42 * c).to_radians().sin();
1705        lt.43 = x43 * (y43 + z43 * c).to_radians().sin();
1706        lt.44 = x44 * (y44 + z44 * c).to_radians().sin();
1707        lt.45 = x45 * (y45 + z45 * c).to_radians().sin();
1708        lt.46 = x46 * (y46 + z46 * c).to_radians().sin();
1709        lt.47 = x47 * (y47 + z47 * c).to_radians().sin();
1710        lt.48 = x48 * (y48 + z48 * c).to_radians().sin();
1711
1712        let mut lambda = lt.0
1713            + lt.1
1714            + lt.2
1715            + lt.3
1716            + lt.4
1717            + lt.5
1718            + lt.6
1719            + lt.7
1720            + lt.8
1721            + lt.9
1722            + lt.10
1723            + lt.11
1724            + lt.12
1725            + lt.13
1726            + lt.14
1727            + lt.15
1728            + lt.16
1729            + lt.17
1730            + lt.18
1731            + lt.19
1732            + lt.20
1733            + lt.21
1734            + lt.22
1735            + lt.23
1736            + lt.24
1737            + lt.25
1738            + lt.26
1739            + lt.27
1740            + lt.28
1741            + lt.29
1742            + lt.30
1743            + lt.31
1744            + lt.32
1745            + lt.33
1746            + lt.34
1747            + lt.35
1748            + lt.36
1749            + lt.37
1750            + lt.38
1751            + lt.39
1752            + lt.40
1753            + lt.41
1754            + lt.42
1755            + lt.43
1756            + lt.44
1757            + lt.45
1758            + lt.46
1759            + lt.47
1760            + lt.48;
1761        lambda *= 0.000005729577951308232;
1762        lambda += 282.7771834 + 36000.76953744 * c;
1763        (lambda + Self::aberration(c) + Self::nutation(julian_centuries)).rem_euclid(360.0)
1764    }
1765
1766    /// The best viewing time (UT) in the evening for viewing the young moon from `location` on `date`. This is defined as
1767    /// the time when the sun is 4.5 degrees below the horizon, or `date + 1` if there is no such time.
1768    ///
1769    /// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz.
1770    /// Reference lisp code: <https://github.com/EdReingold/calendar-code2/blob/9afc1f3/calendar.l#L7337-L7346>
1771    fn simple_best_view(date: RataDie, location: Location) -> Moment {
1772        let dark = Self::dusk(date.to_f64_date(), location, 4.5);
1773        let best = dark.unwrap_or((date + 1).as_moment());
1774
1775        Location::universal_from_standard(best, location)
1776    }
1777
1778    /// Angular separation of the sun and moon at `moment`, for the purposes of determining the likely
1779    /// visibility of the crescent moon.
1780    ///
1781    /// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz.
1782    /// Reference lisp code: <https://github.com/EdReingold/calendar-code2/blob/9afc1f3/calendar.l#L7284-L7290>
1783    fn arc_of_light(moment: Moment) -> f64 {
1784        let julian_centuries = Self::julian_centuries(moment);
1785        (Self::lunar_latitude(julian_centuries).to_radians().cos()
1786            * Self::lunar_phase(moment, julian_centuries)
1787                .to_radians()
1788                .cos())
1789        .acos()
1790        .to_degrees()
1791    }
1792
1793    /// Criterion for likely visibility of the crescent moon on the eve of `date` at `location`,
1794    /// not intended for high altitudes or polar regions, as defined by S.K. Shaukat.
1795    ///
1796    /// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz.
1797    /// Reference lisp code: <https://github.com/EdReingold/calendar-code2/blob/9afc1f3/calendar.l#L7306-L7317>
1798    pub fn shaukat_criterion(date: Moment, location: Location) -> bool {
1799        let tee = Self::simple_best_view((date - 1.0).as_rata_die(), location);
1800        let phase = Self::lunar_phase(tee, Self::julian_centuries(tee));
1801        let h = Self::lunar_altitude(tee, location);
1802        let cap_arcl = Self::arc_of_light(tee);
1803
1804        let new = 0.0;
1805        let first_quarter = 90.0;
1806        let deg_10_6 = 10.6;
1807        let deg_90 = 90.0;
1808        let deg_4_1 = 4.1;
1809
1810        if phase > new
1811            && phase < first_quarter
1812            && cap_arcl >= deg_10_6
1813            && cap_arcl <= deg_90
1814            && h > deg_4_1
1815        {
1816            return true;
1817        }
1818
1819        false
1820    }
1821
1822    /// Criterion for possible visibility of crescent moon on the eve of `date` at `location`;
1823    /// currently, this calls `shaukat_criterion`, but this can be replaced with another implementation.
1824    pub fn visible_crescent(date: Moment, location: Location) -> bool {
1825        Self::shaukat_criterion(date, location)
1826    }
1827
1828    /// Given an `angle` and a [`Moment`] `moment`, approximate the `Moment` at or before moment
1829    /// at which solar longitude exceeded the given angle.
1830    ///
1831    /// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz.
1832    /// Lisp code reference: <https://github.com/EdReingold/calendar-code2/blob/9afc1f3/calendar.l#L4132-L4146>
1833    pub fn estimate_prior_solar_longitude(angle: f64, moment: Moment) -> Moment {
1834        let rate = MEAN_TROPICAL_YEAR / 360.0;
1835        let julian_centuries = Self::julian_centuries(moment);
1836        let tau =
1837            moment - rate * (Self::solar_longitude(julian_centuries) - angle).rem_euclid(360.0);
1838        let delta = (Self::solar_longitude(Self::julian_centuries(tau)) - angle + 180.0)
1839            .rem_euclid(360.0)
1840            - 180.0;
1841        let result_rhs = tau - rate * delta;
1842        if moment < result_rhs {
1843            moment
1844        } else {
1845            result_rhs
1846        }
1847    }
1848
1849    /// Aberration at the time given in Julian centuries.
1850    /// See: <https://sceweb.sce.uhcl.edu/helm/WEB-Positional%20Astronomy/Tutorial/Aberration/Aberration.html>
1851    ///
1852    /// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz.
1853    /// Lisp code reference: <https://github.com/EdReingold/calendar-code2/blob/9afc1f3/calendar.l#L4049-L4057>
1854    fn aberration(c: f64) -> f64 {
1855        // This code differs from the lisp/book code by taking in a julian centuries value instead of
1856        // a Moment; this is because aberration is only ever called in the fn solar_longitude, which
1857        // already converts moment to julian centuries. Thus this function takes the julian centuries
1858        // to avoid unnecessarily calculating the same value twice.
1859        0.0000974 * (177.63 + 35999.01848 * c).to_radians().cos() - 0.005575
1860    }
1861
1862    /// Find the time of the new moon preceding a given Moment (the last new moon before the moment)
1863    ///
1864    /// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz.
1865    /// Most of the math performed in the equivalent book/lisp function is done in [`Self::num_of_new_moon_at_or_after`].
1866    /// Lisp code reference: <https://github.com/EdReingold/calendar-code2/blob/9afc1f3/calendar.l#L4379-L4386>
1867    pub fn new_moon_before(moment: Moment) -> Moment {
1868        Self::nth_new_moon(Self::num_of_new_moon_at_or_after(moment) - 1)
1869    }
1870
1871    /// Find the time of the new moon following a given Moment (the first new moon before the moment)
1872    ///
1873    /// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz.
1874    /// Most of the math performed in the equivalent book/lisp function is done in [`Self::num_of_new_moon_at_or_after`].
1875    /// Lisp code reference: <https://github.com/EdReingold/calendar-code2/blob/9afc1f3/calendar.l#L4388-L4395>
1876    pub fn new_moon_at_or_after(moment: Moment) -> Moment {
1877        Self::nth_new_moon(Self::num_of_new_moon_at_or_after(moment))
1878    }
1879
1880    /// Function to find the number of the new moon at or after a given moment;
1881    /// helper function for `new_moon_before` and `new_moon_at_or_after`.
1882    ///
1883    /// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz.
1884    /// This function incorporates code from the book/lisp equivalent functions
1885    /// of [`Self::new_moon_before`] and [`Self::new_moon_at_or_after`].
1886    /// Lisp code reference: <https://github.com/EdReingold/calendar-code2/blob/9afc1f3/calendar.l#L4379-L4395>
1887    pub fn num_of_new_moon_at_or_after(moment: Moment) -> i32 {
1888        let t0: Moment = NEW_MOON_ZERO;
1889        let phi = Self::lunar_phase(moment, Self::julian_centuries(moment));
1890        let maybe_n = i64_to_i32(
1891            (div_euclid_f64(moment - t0, MEAN_SYNODIC_MONTH) - phi / 360.0).round() as i64,
1892        );
1893        debug_assert!(maybe_n.is_ok(), "Num of new moon should be in range of i32");
1894        let n = maybe_n.unwrap_or_else(|e| e.saturate());
1895        let mut result = n;
1896        let mut iters = 0;
1897        let max_iters = 31;
1898        while iters < max_iters && Self::nth_new_moon(result) < moment {
1899            iters += 1;
1900            result += 1;
1901        }
1902        result
1903    }
1904
1905    /// Sine of angle between the position of the sun at the given moment in local time and the moment
1906    /// at which the angle of depression of the sun from the given location is equal to `alpha`.
1907    ///
1908    /// Based on functions from _Calendrical Calculations_ by Reingold & Dershowitz.
1909    /// Lisp code reference: <https://github.com/EdReingold/calendar-code2/blob/9afc1f3/calendar.l#L3590-L3605>
1910    pub fn sine_offset(moment: Moment, location: Location, alpha: f64) -> f64 {
1911        let phi = location.latitude;
1912        let tee_prime = Location::universal_from_local(moment, location);
1913        let delta = Self::declination(
1914            tee_prime,
1915            0.0,
1916            Self::solar_longitude(Self::julian_centuries(tee_prime)),
1917        );
1918
1919        phi.to_radians().tan() * delta.to_radians().tan()
1920            + alpha.to_radians().sin() / (delta.to_radians().cos() * phi.to_radians().cos())
1921    }
1922}
1923
1924#[cfg(test)]
1925mod tests {
1926
1927    use super::*;
1928
1929    // Constants applied to provide a margin of error when comparing floating-point values in tests.
1930    const TEST_LOWER_BOUND_FACTOR: f64 = 0.9999999;
1931    const TEST_UPPER_BOUND_FACTOR: f64 = 1.0000001;
1932
1933    macro_rules! assert_eq_f64 {
1934        ($expected_value:expr, $value:expr, $moment:expr) => {
1935            if $expected_value > 0.0 {
1936                assert!($value > $expected_value * TEST_LOWER_BOUND_FACTOR,
1937                         "calculation failed for the test case:\n\n\tMoment: {:?} with expected: {} and calculated: {}\n\n",
1938                         $moment, $expected_value, $value);
1939                assert!($value < $expected_value * TEST_UPPER_BOUND_FACTOR,
1940                         "calculation failed for the test case:\n\n\tMoment: {:?} with expected: {} and calculated: {}\n\n",
1941                         $moment, $expected_value, $value);
1942            } else {
1943                assert!($value > $expected_value * TEST_UPPER_BOUND_FACTOR,
1944                         "calculation failed for the test case:\n\n\tMoment: {:?} with expected: {} and calculated: {}\n\n",
1945                         $moment, $expected_value, $value);
1946                assert!($value < $expected_value * TEST_LOWER_BOUND_FACTOR,
1947                         "calculation failed for the test case:\n\n\tMoment: {:?} with expected: {} and calculated: {}\n\n",
1948                         $moment, $expected_value, $value);
1949            }
1950        }
1951    }
1952
1953    #[test]
1954    // Checks that ephemeris_correction gives the same values as the lisp reference code for the given RD test cases
1955    // (See function definition for lisp reference)
1956    fn check_ephemeris_correction() {
1957        let rd_vals = [
1958            -214193, -61387, 25469, 49217, 171307, 210155, 253427, 369740, 400085, 434355, 452605,
1959            470160, 473837, 507850, 524156, 544676, 567118, 569477, 601716, 613424, 626596, 645554,
1960            664224, 671401, 694799, 704424, 708842, 709409, 709580, 727274, 728714, 744313, 764652,
1961        ];
1962        let expected_ephemeris = [
1963            0.2141698518518519,
1964            0.14363257367091617,
1965            0.11444429141515931,
1966            0.10718320232694657,
1967            0.06949806372337948,
1968            0.05750681225096574,
1969            0.04475812294339828,
1970            0.017397257248984357,
1971            0.012796798891589713,
1972            0.008869421568656596,
1973            0.007262628304956149,
1974            0.005979700330107665,
1975            0.005740181544555194,
1976            0.0038756713829057486,
1977            0.0031575183970409424,
1978            0.0023931271439193596,
1979            0.0017316532690131062,
1980            0.0016698814624679225,
1981            6.150149905066665E-4,
1982            1.7716816592592584E-4,
1983            1.016458530046296E-4,
1984            1.7152348357870364E-4,
1985            1.3696411598154996E-4,
1986            6.153868613872005E-5,
1987            1.4168812498149138E-5,
1988            2.767107192307865E-4,
1989            2.9636802723679223E-4,
1990            3.028239003387824E-4,
1991            3.028239003387824E-4,
1992            6.75088347496296E-4,
1993            7.128242445629627E-4,
1994            9.633446296296293E-4,
1995            0.0029138888888888877,
1996        ];
1997        for (rd, expected_ephemeris) in rd_vals.iter().zip(expected_ephemeris.iter()) {
1998            let moment: Moment = Moment::new(*rd as f64);
1999            let ephemeris = Astronomical::ephemeris_correction(moment);
2000            let expected_ephemeris_value = expected_ephemeris;
2001            assert!(ephemeris > expected_ephemeris_value * TEST_LOWER_BOUND_FACTOR, "Ephemeris correction calculation failed for the test case:\n\n\tMoment: {moment:?} with expected: {expected_ephemeris_value} and calculated: {ephemeris}\n\n");
2002            assert!(ephemeris < expected_ephemeris_value * TEST_UPPER_BOUND_FACTOR, "Ephemeris correction calculation failed for the test case:\n\n\tMoment: {moment:?} with expected: {expected_ephemeris_value} and calculated: {ephemeris}\n\n");
2003        }
2004    }
2005
2006    #[test]
2007    // Checks that solar_longitude gives the same values as the lisp reference code for the given RD test cases
2008    // (See function definition for lisp reference)
2009    fn check_solar_longitude() {
2010        let rd_vals = [
2011            -214193, -61387, 25469, 49217, 171307, 210155, 253427, 369740, 400085, 434355, 452605,
2012            470160, 473837, 507850, 524156, 544676, 567118, 569477, 601716, 613424, 626596, 645554,
2013            664224, 671401, 694799, 704424, 708842, 709409, 709580, 727274, 728714, 744313, 764652,
2014        ];
2015        let expected_solar_long = [
2016            119.47343190503307,
2017            254.2489611345809,
2018            181.43599673954304,
2019            188.66392267483752,
2020            289.0915666249348,
2021            59.11974154849304,
2022            228.31455470912624,
2023            34.46076992887538,
2024            63.18799596698955,
2025            2.4575913259759545,
2026            350.475934906397,
2027            13.498220866371412,
2028            37.403920329437824,
2029            81.02813003520714,
2030            313.86049865107634,
2031            19.95443016415811,
2032            176.05943166351062,
2033            344.92295174632454,
2034            79.96492181924987,
2035            99.30231774304411,
2036            121.53530416596914,
2037            88.56742889029556,
2038            129.289884101192,
2039            6.146910693067184,
2040            28.25199345351575,
2041            151.7806330331332,
2042            185.94586701843946,
2043            28.55560762159439,
2044            193.3478921554779,
2045            357.15125499424175,
2046            336.1706924761211,
2047            228.18487947607719,
2048            116.43935225951282,
2049        ];
2050        for (rd, expected_solar_long) in rd_vals.iter().zip(expected_solar_long.iter()) {
2051            let moment: Moment = Moment::new(*rd as f64);
2052            let solar_long =
2053                Astronomical::solar_longitude(Astronomical::julian_centuries(moment + 0.5));
2054            let expected_solar_long_value = expected_solar_long;
2055            assert!(solar_long > expected_solar_long_value * TEST_LOWER_BOUND_FACTOR, "Solar longitude calculation failed for the test case:\n\n\tMoment: {moment:?} with expected: {expected_solar_long_value} and calculated: {solar_long}\n\n");
2056            assert!(solar_long < expected_solar_long_value * TEST_UPPER_BOUND_FACTOR, "Solar longitude calculation failed for the test case:\n\n\tMoment: {moment:?} with expected: {expected_solar_long_value} and calculated: {solar_long}\n\n");
2057        }
2058    }
2059
2060    #[test]
2061    // Checks that lunar_latitude gives the same values as the lisp reference code for the given RD test cases
2062    // (See function definition for lisp reference)
2063
2064    fn check_lunar_latitude() {
2065        let rd_vals = [
2066            -214193, -61387, 25469, 49217, 171307, 210155, 253427, 369740, 400085, 434355, 452605,
2067            470160, 473837, 507850, 524156, 544676, 567118, 569477, 601716, 613424, 626596, 645554,
2068            664224, 671401, 694799, 704424, 708842, 709409, 709580, 727274, 728714, 744313, 764652,
2069        ];
2070
2071        let expected_lunar_lat = [
2072            2.4527590208461576,
2073            -4.90223034654341,
2074            -2.9394693592610484,
2075            5.001904508580623,
2076            -3.208909826304433,
2077            0.894361559890105,
2078            -3.8633355687979827,
2079            -2.5224444701068927,
2080            1.0320696124422062,
2081            3.005689926794408,
2082            1.613842956502888,
2083            4.766740664556875,
2084            4.899202930916035,
2085            4.838473946607273,
2086            2.301475724501815,
2087            -0.8905637199828537,
2088            4.7657836433468495,
2089            -2.737358003826797,
2090            -4.035652608005429,
2091            -3.157214517184652,
2092            -1.8796147336498752,
2093            -3.379519408995276,
2094            -4.398341468078228,
2095            2.099198567294447,
2096            5.268746128633113,
2097            -1.6722994521634027,
2098            4.6820126551666865,
2099            3.705518210116447,
2100            2.493964063649065,
2101            -4.167774638752936,
2102            -2.873757531859998,
2103            -4.667251128743298,
2104            5.138562328560728,
2105        ];
2106
2107        for (rd, expected_lunar_lat) in rd_vals.iter().zip(expected_lunar_lat.iter()) {
2108            let moment: Moment = Moment::new(*rd as f64);
2109            let lunar_lat = Astronomical::lunar_latitude(Astronomical::julian_centuries(moment));
2110            let expected_lunar_lat_value = *expected_lunar_lat;
2111
2112            assert_eq_f64!(expected_lunar_lat_value, lunar_lat, moment)
2113        }
2114    }
2115
2116    #[test]
2117    // Checks that lunar_longitude gives the same values as the lisp reference code for the given RD test cases
2118    // (See function definition for lisp reference)
2119    fn check_lunar_longitude() {
2120        let rd_vals = [
2121            -214193, -61387, 25469, 49217, 171307, 210155, 253427, 369740, 400085, 434355, 452605,
2122            470160, 473837, 507850, 524156, 544676, 567118, 569477, 601716, 613424, 626596, 645554,
2123            664224, 671401, 694799, 704424, 708842, 709409, 709580, 727274, 728714, 744313, 764652,
2124        ];
2125        let expected_lunar_long = [
2126            244.85390528515035,
2127            208.85673853696503,
2128            213.74684265158967,
2129            292.04624333935743,
2130            156.81901407583166,
2131            108.0556329349528,
2132            39.35609790324581,
2133            98.56585102192106,
2134            332.95829627335894,
2135            92.25965175091615,
2136            78.13202909213766,
2137            274.9469953879383,
2138            128.3628442664409,
2139            89.51845094326185,
2140            24.607322526832988,
2141            53.4859568448797,
2142            187.89852001941696,
2143            320.1723620959754,
2144            314.0425667275923,
2145            145.47406514043587,
2146            185.03050779751646,
2147            142.18913274552065,
2148            253.74337531953228,
2149            151.64868501335397,
2150            287.9877436469169,
2151            25.626707154435444,
2152            290.28830064619893,
2153            189.91314245171338,
2154            284.93173002623826,
2155            152.3390442635215,
2156            51.66226507971774,
2157            26.68206023138705,
2158            175.5008226195208,
2159        ];
2160        for (rd, expected_lunar_long) in rd_vals.iter().zip(expected_lunar_long.iter()) {
2161            let moment: Moment = Moment::new(*rd as f64);
2162            let lunar_long = Astronomical::lunar_longitude(Astronomical::julian_centuries(moment));
2163            let expected_lunar_long_value = *expected_lunar_long;
2164
2165            assert_eq_f64!(expected_lunar_long_value, lunar_long, moment)
2166        }
2167    }
2168
2169    #[test]
2170    fn check_lunar_altitude() {
2171        let rd_vals = [
2172            -214193, -61387, 25469, 49217, 171307, 210155, 253427, 369740, 400085, 434355, 452605,
2173            470160, 473837, 507850, 524156, 544676, 567118, 569477, 601716, 613424, 626596, 645554,
2174            664224, 671401, 694799, 704424, 708842, 709409, 709580, 727274, 728714, 744313, 764652,
2175        ];
2176
2177        let expected_altitude_deg: [f64; 33] = [
2178            -13.163184128188277,
2179            -7.281425833096932,
2180            -77.1499009115812,
2181            -30.401178593900795,
2182            71.84857827681589,
2183            -43.79857984753659,
2184            40.65320421851649,
2185            -40.2787255279427,
2186            29.611156512065406,
2187            -19.973178784428228,
2188            -23.740743779700097,
2189            30.956688013173505,
2190            -18.88869091014726,
2191            -32.16116202243495,
2192            -45.68091943596022,
2193            -50.292110029959986,
2194            -54.3453056090807,
2195            -34.56600009726776,
2196            44.13198955291821,
2197            -57.539862986917285,
2198            -62.08243959461623,
2199            -54.07209109276471,
2200            -16.120452006695814,
2201            23.864594681196934,
2202            32.95014668614863,
2203            72.69165128891194,
2204            -29.849481790038908,
2205            31.610644151367637,
2206            -42.21968940776054,
2207            28.6478092363985,
2208            -38.95055354031621,
2209            27.601977078963245,
2210            -54.85468160086816,
2211        ];
2212
2213        for (rd, expected_alt) in rd_vals.iter().zip(expected_altitude_deg.iter()) {
2214            let moment: Moment = Moment::new(*rd as f64);
2215            let lunar_alt = Astronomical::lunar_altitude(moment, crate::islamic::MECCA);
2216            let expected_alt_value = *expected_alt;
2217
2218            assert_eq_f64!(expected_alt_value, lunar_alt, moment)
2219        }
2220    }
2221
2222    #[test]
2223    fn check_lunar_distance() {
2224        let rd_vals = [
2225            -214193, -61387, 25469, 49217, 171307, 210155, 253427, 369740, 400085, 434355, 452605,
2226            470160, 473837, 507850, 524156, 544676, 567118, 569477, 601716, 613424, 626596, 645554,
2227            664224, 671401, 694799, 704424, 708842, 709409, 709580, 727274, 728714, 744313, 764652,
2228        ];
2229
2230        let expected_distances = [
2231            387624532.22874624,
2232            393677431.9167689,
2233            402232943.80299366,
2234            392558548.8426357,
2235            366799795.8707107,
2236            365107305.3822873,
2237            401995197.0122423,
2238            404025417.6150537,
2239            377671971.8515077,
2240            403160628.6150732,
2241            375160036.9057225,
2242            369934038.34809774,
2243            402543074.28064245,
2244            374847147.6967837,
2245            403469151.42100906,
2246            386211365.4436033,
2247            385336015.6086019,
2248            400371744.7464432,
2249            395970218.00750065,
2250            383858113.5538787,
2251            389634540.7722341,
2252            390868707.6609328,
2253            368015493.693663,
2254            399800095.77937233,
2255            404273360.3039046,
2256            382777325.7053601,
2257            378047375.3350678,
2258            385774023.9948239,
2259            371763698.0990588,
2260            362461692.8996066,
2261            394214466.3812425,
2262            405787977.04490376,
2263            404202826.42484397,
2264        ];
2265
2266        for (rd, expected_distance) in rd_vals.iter().zip(expected_distances.iter()) {
2267            let moment: Moment = Moment::new(*rd as f64);
2268            let distance = Astronomical::lunar_distance(moment);
2269            let expected_distance_val = *expected_distance;
2270
2271            assert_eq_f64!(expected_distance_val, distance, moment)
2272        }
2273    }
2274
2275    #[test]
2276    fn check_lunar_parallax() {
2277        let rd_vals = [
2278            -214193, -61387, 25469, 49217, 171307, 210155, 253427, 369740, 400085, 434355, 452605,
2279            470160, 473837, 507850, 524156, 544676, 567118, 569477, 601716, 613424, 626596, 645554,
2280            664224, 671401, 694799, 704424, 708842, 709409, 709580, 727274, 728714, 744313, 764652,
2281        ];
2282
2283        let expected_parallax = [
2284            0.9180377088277034,
2285            0.9208275970231943,
2286            0.20205836298974478,
2287            0.8029475944705559,
2288            0.3103764190238057,
2289            0.7224552232666479,
2290            0.6896953754669151,
2291            0.6900664438899986,
2292            0.8412721901635796,
2293            0.8519504336914271,
2294            0.8916972264563727,
2295            0.8471706468502866,
2296            0.8589744596828851,
2297            0.8253387743371953,
2298            0.6328154405175959,
2299            0.60452566100182,
2300            0.5528114670829496,
2301            0.7516491660573382,
2302            0.6624140811593374,
2303            0.5109678575066725,
2304            0.4391324179474404,
2305            0.5486027633624313,
2306            0.9540023420545446,
2307            0.835939538308717,
2308            0.7585615249134946,
2309            0.284040095327141,
2310            0.8384425157447107,
2311            0.8067682261382678,
2312            0.7279971552035109,
2313            0.8848306274359499,
2314            0.720943806048675,
2315            0.7980998225232075,
2316            0.5204553405568378,
2317        ];
2318
2319        for (rd, parallax) in rd_vals.iter().zip(expected_parallax.iter()) {
2320            let moment: Moment = Moment::new(*rd as f64);
2321            let lunar_altitude_val = Astronomical::lunar_altitude(moment, crate::islamic::MECCA);
2322            let parallax_val = Astronomical::lunar_parallax(lunar_altitude_val, moment);
2323            let expected_parallax_val = *parallax;
2324
2325            assert_eq_f64!(expected_parallax_val, parallax_val, moment);
2326        }
2327    }
2328
2329    #[test]
2330    fn check_moonset() {
2331        let rd_vals = [
2332            -214193.0, -61387.0, 25469.0, 49217.0, 171307.0, 210155.0, 253427.0, 369740.0,
2333            400085.0, 434355.0, 452605.0, 470160.0, 473837.0, 507850.0, 524156.0, 544676.0,
2334            567118.0, 569477.0, 601716.0, 613424.0, 626596.0, 645554.0, 664224.0, 671401.0,
2335            694799.0, 704424.0, 708842.0, 709409.0, 709580.0, 727274.0, 728714.0, 744313.0,
2336            764652.0,
2337        ];
2338
2339        let expected_values = [
2340            -214192.91577491348,
2341            -61386.372392431986,
2342            25469.842646633304,
2343            49217.03030766261,
2344            171307.41988615665,
2345            210155.96578468647,
2346            253427.2528524993,
2347            0.0,
2348            400085.5281194299,
2349            434355.0524936674,
2350            452605.0379962325,
2351            470160.4931771927,
2352            473837.06032208423,
2353            507850.8560177605,
2354            0.0,
2355            544676.908706548,
2356            567118.8180096536,
2357            569477.7141856537,
2358            601716.4168627897,
2359            613424.9325031227,
2360            626596.9563783304,
2361            645554.9526297608,
2362            664224.070965863,
2363            671401.2004198332,
2364            694799.4892001058,
2365            704424.4299627786,
2366            708842.0314145002,
2367            709409.2245215117,
2368            0.0,
2369            727274.2148254914,
2370            0.0,
2371            744313.2118589033,
2372            764652.9631741203,
2373        ];
2374
2375        for (rd, expected_val) in rd_vals.iter().zip(expected_values.iter()) {
2376            let moment: Moment = Moment::new(*rd);
2377            let moonset_val = Astronomical::moonset(moment, crate::islamic::MECCA);
2378            let expected_moonset_val = *expected_val;
2379            if let Some(moonset_val) = moonset_val {
2380                assert_eq_f64!(expected_moonset_val, moonset_val.inner(), moment);
2381            } else {
2382                assert_eq!(expected_moonset_val, 0.0);
2383            }
2384        }
2385    }
2386
2387    #[test]
2388    fn check_sunset() {
2389        let rd_vals = [
2390            -214193.0, -61387.0, 25469.0, 49217.0, 171307.0, 210155.0, 253427.0, 369740.0,
2391            400085.0, 434355.0, 452605.0, 470160.0, 473837.0, 507850.0, 524156.0, 544676.0,
2392            567118.0, 569477.0, 601716.0, 613424.0, 626596.0, 645554.0, 664224.0, 671401.0,
2393            694799.0, 704424.0, 708842.0, 709409.0, 709580.0, 727274.0, 728714.0, 744313.0,
2394            764652.0,
2395        ];
2396
2397        let expected_values = [
2398            -214192.2194436165,
2399            -61386.30267524347,
2400            25469.734889564967,
2401            49217.72851448112,
2402            171307.70878832813,
2403            210155.77420199668,
2404            253427.70087725233,
2405            369740.7627365203,
2406            400085.77677703864,
2407            434355.74808897293,
2408            452605.7425360138,
2409            470160.75310216413,
2410            473837.76440251875,
2411            507850.7840412511,
2412            524156.7225351998,
2413            544676.7561346035,
2414            567118.7396585084,
2415            569477.7396636717,
2416            601716.784057734,
2417            613424.7870863203,
2418            626596.781969136,
2419            645554.7863087669,
2420            664224.778132625,
2421            671401.7496876866,
2422            694799.7602310368,
2423            704424.7619096127,
2424            708842.730647343,
2425            709409.7603906896,
2426            709580.7240122546,
2427            727274.745361792,
2428            728714.734750938,
2429            744313.699821144,
2430            764652.7844809336,
2431        ];
2432
2433        let jerusalem = Location {
2434            latitude: 31.78,
2435            longitude: 35.24,
2436            elevation: 740.0,
2437            utc_offset: (1_f64 / 12_f64),
2438        };
2439
2440        for (rd, expected_sunset_value) in rd_vals.iter().zip(expected_values.iter()) {
2441            let moment = Moment::new(*rd);
2442            let sunset_value = Astronomical::sunset(moment, jerusalem).unwrap();
2443            let expected_sunset_val = *expected_sunset_value;
2444            assert_eq_f64!(expected_sunset_val, sunset_value.inner(), moment)
2445        }
2446    }
2447
2448    #[test]
2449    // Checks that next_new_moon gives the same values as the lisp reference code for the given RD test cases
2450    // (See function definition for lisp reference)
2451    fn check_next_new_moon() {
2452        let rd_vals = [
2453            -214193, -61387, 25469, 49217, 171307, 210155, 253427, 369740, 400085, 434355, 452605,
2454            470160, 473837, 507850, 524156, 544676, 567118, 569477, 601716, 613424, 626596, 645554,
2455            664224, 671401, 694799, 704424, 708842, 709409, 709580, 727274, 728714, 744313, 764652,
2456        ];
2457        let expected_next_new_moon = [
2458            -214174.60582868298,
2459            -61382.99532831192,
2460            25495.80977675628,
2461            49238.50244808781,
2462            171318.43531326813,
2463            210180.69184966758,
2464            253442.85936730343,
2465            369763.74641362444,
2466            400091.5783431683,
2467            434376.5781067696,
2468            452627.1919724953,
2469            470167.57836052414,
2470            473858.8532764285,
2471            507878.6668429224,
2472            524179.2470620894,
2473            544702.7538732041,
2474            567146.5131819838,
2475            569479.2032589674,
2476            601727.0335578924,
2477            613449.7621296605,
2478            626620.3698017383,
2479            645579.0767485882,
2480            664242.8867184789,
2481            671418.970538101,
2482            694807.5633711396,
2483            704433.4911827276,
2484            708863.5970001582,
2485            709424.4049294397,
2486            709602.0826867367,
2487            727291.2094001573,
2488            728737.4476913146,
2489            744329.5739998783,
2490            764676.1912733881,
2491        ];
2492        for (rd, expected_next_new_moon) in rd_vals.iter().zip(expected_next_new_moon.iter()) {
2493            let moment: Moment = Moment::new(*rd as f64);
2494            let next_new_moon = Astronomical::new_moon_at_or_after(moment);
2495            let expected_next_new_moon_moment = Moment::new(*expected_next_new_moon);
2496            if *expected_next_new_moon > 0.0 {
2497                assert!(expected_next_new_moon_moment.inner() > next_new_moon.inner() * TEST_LOWER_BOUND_FACTOR, "New moon calculation failed for the test case:\n\n\tMoment: {moment:?} with expected: {expected_next_new_moon_moment:?} and calculated: {next_new_moon:?}\n\n");
2498                assert!(expected_next_new_moon_moment.inner() < next_new_moon.inner() * TEST_UPPER_BOUND_FACTOR, "New moon calculation failed for the test case:\n\n\tMoment: {moment:?} with expected: {expected_next_new_moon_moment:?} and calculated: {next_new_moon:?}\n\n");
2499            } else {
2500                assert!(expected_next_new_moon_moment.inner() > next_new_moon.inner() * TEST_UPPER_BOUND_FACTOR, "New moon calculation failed for the test case:\n\n\tMoment: {moment:?} with expected: {expected_next_new_moon_moment:?} and calculated: {next_new_moon:?}\n\n");
2501                assert!(expected_next_new_moon_moment.inner() < next_new_moon.inner() * TEST_LOWER_BOUND_FACTOR, "New moon calculation failed for the test case:\n\n\tMoment: {moment:?} with expected: {expected_next_new_moon_moment:?} and calculated: {next_new_moon:?}\n\n");
2502            }
2503        }
2504    }
2505
2506    #[test]
2507    fn check_astronomy_0th_new_moon() {
2508        // Checks the accuracy of the 0th new moon to be on January 11th
2509        let zeroth_new_moon = Astronomical::nth_new_moon(0);
2510        assert_eq!(
2511            zeroth_new_moon.inner() as i32,
2512            11,
2513            "0th new moon check failed with nth_new_moon(0) = {zeroth_new_moon:?}"
2514        );
2515    }
2516
2517    #[test]
2518    fn check_num_of_new_moon_0() {
2519        // Tests the function num_of_new_moon_at_or_after() returns 0 for moment 0
2520        assert_eq!(
2521            Astronomical::num_of_new_moon_at_or_after(Moment::new(0.0)),
2522            0
2523        );
2524    }
2525
2526    #[test]
2527    fn check_new_moon_directionality() {
2528        // Checks that new_moon_before is less than new_moon_at_or_after for a large number of Moments
2529        let mut moment: Moment = Moment::new(-15500.0);
2530        let max_moment = Moment::new(15501.0);
2531        let mut iters: i32 = 0;
2532        let max_iters = 10000;
2533        while iters < max_iters && moment < max_moment {
2534            let before = Astronomical::new_moon_before(moment);
2535            let at_or_after = Astronomical::new_moon_at_or_after(moment);
2536            assert!(before < at_or_after, "Directionality of fns new_moon_before and new_moon_at_or_after failed for Moment: {moment:?}");
2537            iters += 1;
2538            moment += 31.0;
2539        }
2540        assert!(
2541            iters > 500,
2542            "Testing failed: less than the expected number of testing iterations"
2543        );
2544        assert!(
2545            iters < max_iters,
2546            "Testing failed: more than the expected number of testing iterations"
2547        );
2548    }
2549
2550    #[test]
2551    fn check_location_valid_case() {
2552        // Checks that location construction and functions work for various valid lats and longs
2553        let mut long = -180.0;
2554        let mut lat = -90.0;
2555        let zone = 0.0;
2556        while long <= 180.0 {
2557            while lat <= 90.0 {
2558                let location: Location = Location::try_new(lat, long, 1000.0, zone).unwrap();
2559                assert_eq!(lat, location.latitude());
2560                assert_eq!(long, location.longitude());
2561
2562                lat += 1.0;
2563            }
2564            long += 1.0;
2565        }
2566    }
2567
2568    #[test]
2569    fn check_location_errors() {
2570        let lat_too_small = Location::try_new(-90.1, 15.0, 1000.0, 0.0).unwrap_err();
2571        assert_eq!(lat_too_small, LocationOutOfBoundsError::Latitude(-90.1));
2572        let lat_too_large = Location::try_new(90.1, -15.0, 1000.0, 0.0).unwrap_err();
2573        assert_eq!(lat_too_large, LocationOutOfBoundsError::Latitude(90.1));
2574        let long_too_small = Location::try_new(15.0, 180.1, 1000.0, 0.0).unwrap_err();
2575        assert_eq!(long_too_small, LocationOutOfBoundsError::Longitude(180.1));
2576        let long_too_large = Location::try_new(-15.0, -180.1, 1000.0, 0.0).unwrap_err();
2577        assert_eq!(long_too_large, LocationOutOfBoundsError::Longitude(-180.1));
2578    }
2579
2580    #[test]
2581    fn check_obliquity() {
2582        let rd_vals = [
2583            -214193, -61387, 25469, 49217, 171307, 210155, 253427, 369740, 400085, 434355, 452605,
2584            470160, 473837, 507850, 524156, 544676, 567118, 569477, 601716, 613424, 626596, 645554,
2585            664224, 671401, 694799, 704424, 708842, 709409, 709580, 727274, 728714, 744313, 764652,
2586        ];
2587
2588        let expected_obliquity_val = [
2589            23.766686762858193,
2590            23.715893268155952,
2591            23.68649428364133,
2592            23.678396646319815,
2593            23.636406172247575,
2594            23.622930685681105,
2595            23.607863050353394,
2596            23.567099369895143,
2597            23.556410268115442,
2598            23.544315732982724,
2599            23.5378658942414,
2600            23.531656189162007,
2601            23.53035487913322,
2602            23.518307553466993,
2603            23.512526100422757,
2604            23.50524564635773,
2605            23.49727762748816,
2606            23.49643975090472,
2607            23.48498365949255,
2608            23.48082101433542,
2609            23.476136639530452,
2610            23.469392588649566,
2611            23.46274905945532,
2612            23.460194773340504,
2613            23.451866181318085,
2614            23.44843969966849,
2615            23.44686683973517,
2616            23.446664978744177,
2617            23.44660409993624,
2618            23.440304562352033,
2619            23.43979187336218,
2620            23.434238093381342,
2621            23.426996977623215,
2622        ];
2623
2624        for (rd, expected_obl_val) in rd_vals.iter().zip(expected_obliquity_val.iter()) {
2625            let moment = Moment::new(*rd as f64);
2626            let obl_val = Astronomical::obliquity(moment);
2627            let expected_val = *expected_obl_val;
2628
2629            assert_eq_f64!(expected_val, obl_val, moment)
2630        }
2631    }
2632}