Coverage Report

Created: 2025-08-12 06:35

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