Skip to main content

astroceleste_engine/
lunar.rs

1//! Lunar phase, illumination, speed, dignity and mansion (`charts/calc/lunar.py`).
2
3use serde::Serialize;
4
5use crate::catalog::{LunarMansion, LUNAR_MANSIONS};
6use crate::chart::Placement;
7use crate::pyfloat;
8
9/// Lunar phase, speed, dignity and mansion.
10#[derive(Debug, Clone, PartialEq, Serialize)]
11pub struct LunarStatus {
12    /// Phase identifier, e.g. "waxing_gibbous".
13    pub phase_key: &'static str,
14    /// Phase name, e.g. "Waxing Gibbous".
15    pub phase_name: &'static str,
16    /// Quarter (1-4) the phase belongs to.
17    pub phase_quarter: u8,
18    /// Phase emoji, e.g. "🌔".
19    pub glyph: &'static str,
20    /// Moon's longitude minus the Sun's, degrees [0, 360).
21    pub elongation: f64,
22    /// Illuminated fraction of the disc, percent.
23    pub illumination_percentage: f64,
24    /// Days since the new Moon (from the elongation and the mean synodic month).
25    pub moon_age_days: f64,
26    /// Whether the elongation is below 180°.
27    pub is_waxing: bool,
28    /// Sign of the Moon.
29    pub moon_sign: &'static str,
30    /// Whole degrees of the Moon within its sign.
31    pub moon_degree: i64,
32    /// Arc minutes past `moon_degree`.
33    pub moon_minute: i64,
34    /// Moon's ecliptic longitude in degrees.
35    pub moon_longitude: f64,
36    /// House (1-12) of the Moon.
37    pub moon_house: u8,
38    /// Moon's speed, degrees per day.
39    pub moon_speed: f64,
40    /// "swift" (> 13.5°/day), "slow" (< 12.5°/day) or "average".
41    pub speed_status: &'static str,
42    /// "Domicile", "Exaltation", "Detriment", "Fall" or "Peregrine".
43    pub essential_dignity: &'static str,
44    /// Lunar mansion the Moon is in.
45    pub lunar_mansion: LunarMansion,
46}
47
48/// Lunar status from the chart's planets; `None` when there is no Moon.
49pub fn lunar_status(planets: &[Placement]) -> Option<LunarStatus> {
50    let moon = planets.iter().find(|p| p.name == "Moon")?;
51    let sun_lon = planets
52        .iter()
53        .find(|p| p.name == "Sun")
54        .map_or(0.0, |s| s.ecliptic_longitude);
55    let moon_lon = moon.ecliptic_longitude;
56    let diff = pyfloat::rem(moon_lon - sun_lon, 360.0);
57
58    let (phase_key, phase_name, phase_quarter, glyph) = if !(22.5..337.5).contains(&diff) {
59        ("new_moon", "New Moon", 1, "🌑")
60    } else if diff < 67.5 {
61        ("waxing_crescent", "Waxing Crescent", 1, "🌒")
62    } else if diff < 112.5 {
63        ("first_quarter", "First Quarter", 2, "🌓")
64    } else if diff < 157.5 {
65        ("waxing_gibbous", "Waxing Gibbous", 2, "🌔")
66    } else if diff < 202.5 {
67        ("full_moon", "Full Moon", 3, "🌕")
68    } else if diff < 247.5 {
69        ("waning_gibbous", "Waning Gibbous", 3, "🌖")
70    } else if diff < 292.5 {
71        ("third_quarter", "Third Quarter", 4, "🌗")
72    } else {
73        ("waning_crescent", "Waning Crescent", 4, "🌘")
74    };
75
76    let speed = moon.speed;
77    let mansion_index = ((moon_lon / (360.0 / 28.0)).floor() as i64).rem_euclid(28) as usize;
78    Some(LunarStatus {
79        phase_key,
80        phase_name,
81        phase_quarter,
82        glyph,
83        elongation: pyfloat::round(diff, 2),
84        illumination_percentage: pyfloat::round(((1.0 - diff.to_radians().cos()) / 2.0) * 100.0, 1),
85        moon_age_days: pyfloat::round((diff / 360.0) * 29.530588853, 1),
86        is_waxing: diff < 180.0,
87        moon_sign: moon.sign,
88        moon_degree: moon.degree,
89        moon_minute: moon.minute,
90        moon_longitude: pyfloat::round(moon_lon, 3),
91        moon_house: moon.house,
92        moon_speed: pyfloat::round(speed, 2),
93        speed_status: if speed > 13.5 {
94            "swift"
95        } else if speed < 12.5 {
96            "slow"
97        } else {
98            "average"
99        },
100        essential_dignity: match moon.sign {
101            "Cancer" => "Domicile",
102            "Taurus" => "Exaltation",
103            "Capricorn" => "Detriment",
104            "Scorpio" => "Fall",
105            _ => "Peregrine",
106        },
107        lunar_mansion: LUNAR_MANSIONS[mansion_index],
108    })
109}