Skip to main content

egui_charts/studies/builtin/
lunar_phases.rs

1//! Lunar phases: the instants of new and full moon, and a study that marks them on the chart.
2//!
3//! The instants follow Jean Meeus, "Astronomical Algorithms" (2nd ed., 1998), chapter 49: a mean
4//! phase from the lunation number `k`, corrected by the periodic terms of the Sun's and the
5//! Moon's anomalies, the Moon's argument of latitude and the planetary arguments. The result is in
6//! dynamical time and is converted to UTC with the ΔT polynomials of Espenak and Meeus.
7//!
8//! # Time basis, range and accuracy
9//!
10//! Times are UTC instants, like [`Bar::time`]. [`lunar_phases`] answers for 1900-01-01 up to but
11//! excluding 2100-01-01; a request outside it is cut to that range. Meeus gives the periodic
12//! terms to a few seconds, and ΔT in that range is known to a few seconds, so a phase is within
13//! about one minute of the published almanac time. The calculation reads no data and does not
14//! depend on how a request is split: the phases of two adjacent intervals are those of their union.
15//!
16//! # Study
17//!
18//! The study marks a phase on the bar whose interval contains it: from the bar's time up to the
19//! next bar's time, and for the last bar up to its own length again. A phase before the first bar
20//! or after the last bar's interval, and a phase of a bar without finite prices, is not marked; a
21//! phase in a gap between bars, such as a weekend, belongs to the bar before it. A bar of more
22//! than a lunar half-month holds only its first phase of each kind. Lines, in order: new moon,
23//! at the low of its bar, and full moon, at the high of its bar. Because a value is a function of
24//! the bar times alone, paging more history in, scrolling or restoring the study never repeats or
25//! moves a phase.
26//!
27//! # Example
28//! ```
29//! use chrono::{TimeZone, Utc};
30//! use egui_charts::studies::{MoonPhase, lunar_phases};
31//!
32//! // The new moon of the total solar eclipse of 2024-04-08 was at 18:21 UTC.
33//! let from = Utc.with_ymd_and_hms(2024, 4, 1, 0, 0, 0).unwrap();
34//! let to = Utc.with_ymd_and_hms(2024, 4, 30, 0, 0, 0).unwrap();
35//! let phases = lunar_phases(from, to);
36//! assert_eq!(phases[0].phase, MoonPhase::New);
37//! let almanac = Utc.with_ymd_and_hms(2024, 4, 8, 18, 21, 0).unwrap();
38//! assert!((phases[0].time - almanac).num_seconds().abs() < 60);
39//! assert_eq!(phases[1].phase, MoonPhase::Full);
40//! ```
41
42use super::over_finite_bars;
43use crate::model::Bar;
44use crate::studies::palette;
45use crate::studies::stepper::{update_window, usable_behind};
46use crate::studies::{Indicator, IndicatorValues};
47use chrono::{DateTime, TimeZone, Utc};
48use egui::Color32;
49
50/// A phase of the Moon with a published instant.
51#[derive(Clone, Copy, Debug, PartialEq, Eq)]
52pub enum MoonPhase {
53    /// The Moon passes between the Earth and the Sun.
54    New,
55    /// The Moon is opposite the Sun.
56    Full,
57}
58
59/// One new or full moon.
60#[derive(Clone, Copy, Debug, PartialEq, Eq)]
61pub struct LunarPhase {
62    /// Which phase.
63    pub phase: MoonPhase,
64    /// The instant, in UTC, rounded to the millisecond.
65    pub time: DateTime<Utc>,
66}
67
68/// Julian day of the Unix epoch.
69const UNIX_EPOCH_JD: f64 = 2_440_587.5;
70/// Mean length of the Gregorian year in days, which locates a date among the lunations.
71const GREGORIAN_YEAR_DAYS: f64 = 365.2425;
72
73/// ΔT, dynamical time minus UT in seconds, at the decimal `year`: the polynomials of Espenak and
74/// Meeus for 1900 to 2150.
75fn delta_t(year: f64) -> f64 {
76    let poly = |t: f64, c: &[f64]| c.iter().rev().fold(0.0, |acc, c| acc * t + c);
77    match year {
78        y if y < 1920.0 => poly(
79            y - 1900.0,
80            &[-2.79, 1.494119, -0.0598939, 0.0061966, -0.000197],
81        ),
82        y if y < 1941.0 => poly(y - 1920.0, &[21.20, 0.84493, -0.0761, 0.0020936]),
83        y if y < 1961.0 => poly(y - 1950.0, &[29.07, 0.407, -1.0 / 233.0, 1.0 / 2547.0]),
84        y if y < 1986.0 => poly(y - 1975.0, &[45.45, 1.067, -1.0 / 260.0, -1.0 / 718.0]),
85        y if y < 2005.0 => poly(
86            y - 2000.0,
87            &[
88                63.86,
89                0.3345,
90                -0.060374,
91                0.0017275,
92                0.000651814,
93                0.00002373599,
94            ],
95        ),
96        y if y < 2050.0 => poly(y - 2000.0, &[62.92, 0.32217, 0.005589]),
97        y => -20.0 + 32.0 * ((y - 1820.0) / 100.0).powi(2) - 0.5628 * (2150.0 - y),
98    }
99}
100
101/// The instant of the phase of lunation `k` (an integer for a new moon, a half-integer for a full
102/// moon) as a Julian day in UT, after Meeus chapter 49.
103fn phase_jd(k: f64) -> f64 {
104    let t = k / 1236.85;
105    let (t2, t3, t4) = (t * t, t * t * t, t * t * t * t);
106    let full = k.fract() != 0.0;
107    let jde = 2_451_550.097_66 + 29.530_588_861 * k + 0.000_154_37 * t2 - 0.000_000_150 * t3
108        + 0.000_000_000_73 * t4;
109    let e = 1.0 - 0.002_516 * t - 0.000_007_4 * t2;
110    let rad = f64::to_radians;
111    // The Sun's mean anomaly, the Moon's mean anomaly and argument of latitude, and the longitude
112    // of the Moon's ascending node, in radians.
113    let m = rad(2.5534 + 29.105_356_70 * k - 0.000_001_4 * t2 - 0.000_000_11 * t3);
114    let mp = rad(
115        201.5643 + 385.816_935_28 * k + 0.010_758_2 * t2 + 0.000_012_38 * t3 - 0.000_000_058 * t4,
116    );
117    let f = rad(
118        160.7108 + 390.670_502_84 * k - 0.001_611_8 * t2 - 0.000_002_27 * t3 + 0.000_000_011 * t4,
119    );
120    let omega = rad(124.7746 - 1.563_755_88 * k + 0.002_067_2 * t2 + 0.000_002_15 * t3);
121    // Coefficient, power of E, then the multiples of M', M and F in the argument. The first four
122    // coefficients differ between the phases.
123    let (c0, c1, c2, c3) = if full {
124        (-0.40614, 0.17302, 0.01614, 0.01043)
125    } else {
126        (-0.40720, 0.17241, 0.01608, 0.01039)
127    };
128    let (c4, c5, c6) = if full {
129        (0.00734, -0.00515, 0.00209)
130    } else {
131        (0.00739, -0.00514, 0.00208)
132    };
133    #[rustfmt::skip]
134    let terms: [(f64, i32, f64, f64, f64); 24] = [
135        (c0, 0, 1.0, 0.0, 0.0),
136        (c1, 1, 0.0, 1.0, 0.0),
137        (c2, 0, 2.0, 0.0, 0.0),
138        (c3, 0, 0.0, 0.0, 2.0),
139        (c4, 1, 1.0, -1.0, 0.0),
140        (c5, 1, 1.0, 1.0, 0.0),
141        (c6, 2, 0.0, 2.0, 0.0),
142        (-0.00111, 0, 1.0, 0.0, -2.0),
143        (-0.00057, 0, 1.0, 0.0, 2.0),
144        (0.00056, 1, 2.0, 1.0, 0.0),
145        (-0.00042, 0, 3.0, 0.0, 0.0),
146        (0.00042, 1, 0.0, 1.0, 2.0),
147        (0.00038, 1, 0.0, 1.0, -2.0),
148        (-0.00024, 1, 2.0, -1.0, 0.0),
149        (-0.00007, 0, 1.0, 2.0, 0.0),
150        (0.00004, 0, 2.0, 0.0, -2.0),
151        (0.00004, 0, 0.0, 3.0, 0.0),
152        (0.00003, 0, 1.0, 1.0, -2.0),
153        (0.00003, 0, 2.0, 0.0, 2.0),
154        (-0.00003, 0, 1.0, 1.0, 2.0),
155        (0.00003, 0, 1.0, -1.0, 2.0),
156        (-0.00002, 0, 1.0, -1.0, -2.0),
157        (-0.00002, 0, 3.0, 1.0, 0.0),
158        (0.00002, 0, 4.0, 0.0, 0.0),
159    ];
160    let mut correction = -0.00017 * omega.sin();
161    for (c, e_power, a, b, d) in terms {
162        correction += c * e.powi(e_power) * (a * mp + b * m + d * f).sin();
163    }
164    // Planetary arguments in degrees: constant, per lunation, and per T squared.
165    #[rustfmt::skip]
166    let planetary: [(f64, f64, f64, f64); 14] = [
167        (299.77, 0.107408, -0.009173, 0.000325),
168        (251.88, 0.016321, 0.0, 0.000165),
169        (251.83, 26.651886, 0.0, 0.000164),
170        (349.42, 36.412478, 0.0, 0.000126),
171        (84.66, 18.206239, 0.0, 0.000110),
172        (141.74, 53.303771, 0.0, 0.000062),
173        (207.14, 2.453732, 0.0, 0.000060),
174        (154.84, 7.306860, 0.0, 0.000056),
175        (34.52, 27.261239, 0.0, 0.000047),
176        (207.19, 0.121824, 0.0, 0.000042),
177        (291.34, 1.844379, 0.0, 0.000040),
178        (161.72, 24.198154, 0.0, 0.000037),
179        (239.56, 25.513099, 0.0, 0.000035),
180        (331.55, 3.592518, 0.0, 0.000023),
181    ];
182    for (base, per_lunation, per_t2, amplitude) in planetary {
183        correction += amplitude * rad(base + per_lunation * k + per_t2 * t2).sin();
184    }
185    let jde = jde + correction;
186    let year = 2000.0 + (jde - 2_451_545.0) / GREGORIAN_YEAR_DAYS;
187    jde - delta_t(year) / 86_400.0
188}
189
190/// The new and full moons in `from..to`, in time order, over 1900-01-01 to 2100-01-01 UTC; see the
191/// module documentation for the time basis and accuracy.
192pub fn lunar_phases(from: DateTime<Utc>, to: DateTime<Utc>) -> Vec<LunarPhase> {
193    let from = from.max(Utc.with_ymd_and_hms(1900, 1, 1, 0, 0, 0).unwrap());
194    let to = to.min(Utc.with_ymd_and_hms(2100, 1, 1, 0, 0, 0).unwrap());
195    // A lunation near a `from` beyond the range could fall outside the representable times.
196    if from >= to {
197        return Vec::new();
198    }
199    let at = |ms: f64| Utc.timestamp_millis_opt(ms.round() as i64).unwrap();
200    // Lunations are counted in halves, the new moons at the even ones; start a lunation early.
201    let year = 1970.0 + from.timestamp() as f64 / 86_400.0 / GREGORIAN_YEAR_DAYS;
202    let mut half = ((year - 2000.0) * 12.3685 * 2.0).floor() as i64 - 2;
203    let mut phases = Vec::new();
204    loop {
205        let jd = phase_jd(half as f64 / 2.0);
206        let time = at((jd - UNIX_EPOCH_JD) * 86_400_000.0);
207        if time >= to {
208            return phases;
209        }
210        if time >= from {
211            let phase = if half % 2 == 0 {
212                MoonPhase::New
213            } else {
214                MoonPhase::Full
215            };
216            phases.push(LunarPhase { phase, time });
217        }
218        half += 1;
219    }
220}
221
222/// Lunar phases: a marker at the low of the bar holding each new moon and at the high of the bar
223/// holding each full moon.
224#[derive(Clone)]
225pub struct LunarPhases {
226    values: IndicatorValues,
227    new_color: Color32,
228    full_color: Color32,
229    visible: bool,
230}
231
232impl LunarPhases {
233    /// Create the study.
234    pub fn new() -> Self {
235        Self {
236            values: IndicatorValues::new(),
237            new_color: palette::BLUE.gamma_multiply_u8(128),
238            full_color: palette::WHITE.gamma_multiply_u8(128),
239            visible: true,
240        }
241    }
242
243    /// The values over `data`, every bar of which has a finite high and low.
244    fn compute(data: &[Bar]) -> IndicatorValues {
245        let mut values = IndicatorValues::missing(data.len());
246        if !data.is_empty() {
247            // Both columns exist, so a bar with one phase still has a value per line.
248            values.set_value(1, 0, f64::NAN);
249        }
250        let [.., before, last] = data else {
251            return values;
252        };
253        let end = last.time + (last.time - before.time);
254        for LunarPhase { phase, time } in lunar_phases(data[0].time, end) {
255            let bar = data.partition_point(|b| b.time <= time) - 1;
256            let (line, price) = match phase {
257                MoonPhase::New => (0, data[bar].low),
258                MoonPhase::Full => (1, data[bar].high),
259            };
260            if values.value(line, bar).is_none() {
261                values.set_value(line, bar, price);
262            }
263        }
264        values
265    }
266}
267
268impl Default for LunarPhases {
269    fn default() -> Self {
270        Self::new()
271    }
272}
273
274impl Indicator for LunarPhases {
275    fn name(&self) -> &str {
276        "Lunar Phases"
277    }
278
279    fn desc(&self) -> &str {
280        "Lunar Phases - New and full moons marked on the bars that hold them"
281    }
282
283    fn calculate(&mut self, data: &[Bar]) {
284        self.update(data, 0);
285    }
286
287    fn update(&mut self, data: &[Bar], from: usize) {
288        let mut values = std::mem::take(&mut self.values);
289        let finite = |b: &Bar| b.high.is_finite() && b.low.is_finite();
290        // The last usable bar before the change owns the phases up to the first changed bar, and
291        // the bar before it gives the length of the last bar.
292        let from = usable_behind(1, finite)(data, from.min(values.len()).min(data.len()));
293        update_window(&mut values, data, from, usable_behind(1, finite), |data| {
294            over_finite_bars(data, finite, Self::compute)
295        });
296        self.values = values;
297    }
298
299    fn is_incremental(&self) -> bool {
300        true
301    }
302
303    fn plot_style(&self, line: usize) -> crate::studies::PlotStyle {
304        crate::studies::PlotStyle {
305            color: if line == 0 {
306                self.new_color
307            } else {
308                self.full_color
309            },
310            width: 2.0,
311            pattern: crate::model::LineStyle::Solid,
312            plot_type: crate::studies::PlotType::Circles,
313            visible: true,
314        }
315    }
316
317    fn plot_text(&self, line: usize, bar: usize) -> Option<String> {
318        self.values
319            .value(line, bar)
320            .map(|_| if line == 0 { "New" } else { "Full" }.to_owned())
321    }
322
323    fn values(&self) -> &IndicatorValues {
324        &self.values
325    }
326
327    fn colors(&self) -> Vec<Color32> {
328        vec![self.new_color, self.full_color]
329    }
330
331    fn set_colors(&mut self, colors: Vec<Color32>) {
332        if let Some(&color) = colors.first() {
333            self.new_color = color;
334        }
335        if let Some(&color) = colors.get(1) {
336            self.full_color = color;
337        }
338    }
339
340    fn is_visible(&self) -> bool {
341        self.visible
342    }
343
344    fn set_visible(&mut self, visible: bool) {
345        self.visible = visible;
346    }
347
348    fn clone_box(&self) -> Box<dyn Indicator> {
349        Box::new(self.clone())
350    }
351
352    fn line_names(&self) -> Vec<String> {
353        vec!["New Moon".to_string(), "Full Moon".to_string()]
354    }
355}
356
357#[cfg(test)]
358mod tests {
359    use super::*;
360    use chrono::Datelike;
361
362    fn utc(y: i32, mo: u32, d: u32, h: u32, mi: u32) -> DateTime<Utc> {
363        Utc.with_ymd_and_hms(y, mo, d, h, mi, 0).unwrap()
364    }
365
366    /// The phases agree with the minute of published almanac times, and with the example of Meeus
367    /// chapter 49, the new moon of 1977-02-18 at 03:37:42 in dynamical time, at the edges of the
368    /// supported range too.
369    #[test]
370    fn phases_match_published_times() {
371        use MoonPhase::{Full, New};
372        let published = [
373            (New, utc(1977, 2, 18, 3, 36)),
374            (New, utc(2000, 1, 6, 18, 14)),
375            (Full, utc(2000, 1, 21, 4, 40)),
376            (New, utc(2024, 1, 11, 11, 57)),
377            (Full, utc(2024, 1, 25, 17, 54)),
378            (Full, utc(2024, 3, 25, 7, 0)),
379            (New, utc(2024, 4, 8, 18, 21)),
380            (Full, utc(2024, 4, 23, 23, 49)),
381        ];
382        for (phase, expected) in published {
383            let found = lunar_phases(
384                expected - chrono::Duration::days(1),
385                expected + chrono::Duration::days(1),
386            );
387            assert_eq!(found.len(), 1, "{expected}");
388            assert_eq!(found[0].phase, phase, "{expected}");
389            // The almanac gives the minute, which the phase is within.
390            assert!(
391                (found[0].time - expected).num_seconds().abs() <= 60,
392                "{expected}"
393            );
394        }
395        // Adjacent intervals give the phases of their union.
396        let cut = utc(2024, 2, 10, 0, 0);
397        let mut split = lunar_phases(utc(2024, 1, 1, 0, 0), cut);
398        split.extend(lunar_phases(cut, utc(2024, 4, 1, 0, 0)));
399        assert_eq!(
400            split,
401            lunar_phases(utc(2024, 1, 1, 0, 0), utc(2024, 4, 1, 0, 0))
402        );
403        assert!(lunar_phases(utc(1899, 1, 1, 0, 0), utc(1900, 1, 1, 0, 0)).is_empty());
404        assert!(lunar_phases(utc(2100, 1, 1, 0, 0), utc(2101, 1, 1, 0, 0)).is_empty());
405        assert!(lunar_phases(DateTime::<Utc>::MAX_UTC, DateTime::<Utc>::MAX_UTC).is_empty());
406    }
407
408    /// The study marks a phase on the bar holding it, new moons at the low and full moons at the
409    /// high, and a series with earlier bars loaded in front or later bars cut off marks the same
410    /// bars, once each.
411    #[test]
412    fn study_marks_the_bar_holding_each_phase_once() {
413        // Weekday bars from 2024-01-08; the new moon of Thursday 11 January at 11:57 and the full
414        // moon of Thursday 25 January at 17:54 fall on their own bars.
415        let bars: Vec<Bar> = (0..24)
416            .map(|i| utc(2024, 1, 8, 0, 0) + chrono::Duration::days(i))
417            .filter(|t| t.weekday().number_from_monday() <= 5)
418            .map(|t| Bar::new(t, 10.0, 12.0, 8.0, 11.0, 1.0))
419            .collect();
420        let mut study = LunarPhases::new();
421        study.calculate(&bars);
422        let marked =
423            |study: &LunarPhases, bars: &[Bar], line: usize| -> Vec<(DateTime<Utc>, f64)> {
424                bars.iter()
425                    .enumerate()
426                    .filter_map(|(i, b)| study.values().value(line, i).map(|v| (b.time, v)))
427                    .collect()
428            };
429        assert_eq!(marked(&study, &bars, 0), [(utc(2024, 1, 11, 0, 0), 8.0)]);
430        assert_eq!(marked(&study, &bars, 1), [(utc(2024, 1, 25, 0, 0), 12.0)]);
431        assert_eq!(study.plot_text(1, 13).as_deref(), Some("Full"));
432        let mut tail = LunarPhases::new();
433        tail.calculate(&bars[2..]);
434        for line in 0..2 {
435            assert_eq!(marked(&tail, &bars[2..], line), marked(&study, &bars, line));
436        }
437        // A later bar moves the end of the last bar's interval, not the marks.
438        let mut longer = bars.clone();
439        longer.push(Bar::new(utc(2024, 2, 1, 0, 0), 10.0, 12.0, 8.0, 11.0, 1.0));
440        study.update(&longer, bars.len());
441        let mut fresh = LunarPhases::new();
442        fresh.calculate(&longer);
443        for line in 0..2 {
444            assert_eq!(marked(&study, &longer, line), marked(&fresh, &longer, line));
445        }
446    }
447}