moon_phase/
lib.rs

1use std::{f64::consts::TAU, time::SystemTime};
2
3const MOON_SYNODIC_PERIOD: f64 = 29.530588853; // Period of moon cycle in days.
4const MOON_SYNODIC_OFFSET: f64 = 2451550.26; // Reference cycle offset in days.
5const MOON_DISTANCE_PERIOD: f64 = 27.55454988; // Period of distance oscillation
6const MOON_DISTANCE_OFFSET: f64 = 2451562.2;
7const MOON_LATITUDE_PERIOD: f64 = 27.212220817; // Latitude oscillation
8const MOON_LATITUDE_OFFSET: f64 = 2451565.2;
9const MOON_LONGITUDE_PERIOD: f64 = 27.321582241; // Longitude oscillation
10const MOON_LONGITUDE_OFFSET: f64 = 2451555.8;
11
12// Names of lunar phases
13const PHASE_NAMES: &[&str] = &[
14    "New",
15    "Waxing Crescent",
16    "First Quarter",
17    "Waxing Gibbous",
18    "Full",
19    "Waning Gibbous",
20    "Last Quarter",
21    "Waning Crescent",
22];
23// Names of Zodiac constellations
24const ZODIAC_NAMES: [&str; 12] = [
25    "Pisces",
26    "Aries",
27    "Taurus",
28    "Gemini",
29    "Cancer",
30    "Leo",
31    "Virgo",
32    "Libra",
33    "Scorpio",
34    "Sagittarius",
35    "Capricorn",
36    "Aquarius",
37];
38// Ecliptic angles of Zodiac constellations
39const ZODIAC_ANGLES: [f64; 12] = [
40    33.18, 51.16, 93.44, 119.48, 135.30, 173.34, 224.17, 242.57, 271.26,
41    302.49, 311.72, 348.58,
42];
43
44#[derive(Debug, Copy, Clone)]
45pub struct MoonPhase {
46    pub j_date: f64,
47    pub phase: f64,                // 0 - 1, 0.5 = full
48    pub age: f64,                  // Age in days of current cycle
49    pub fraction: f64,             // Fraction of illuminated disk
50    pub distance: f64,             // Moon distance in earth radii
51    pub latitude: f64,             // Moon ecliptic latitude
52    pub longitude: f64,            // Moon ecliptic longitude
53    pub phase_name: &'static str,  // New, Full, etc.
54    pub zodiac_name: &'static str, // Constellation
55}
56
57fn julian_date(time: SystemTime) -> f64 {
58    let secs = match time.duration_since(SystemTime::UNIX_EPOCH) {
59        Ok(duration) => duration.as_secs_f64(),
60        Err(earlier) => -1. * earlier.duration().as_secs_f64(),
61    };
62    secs / 86400. + 2440587.5
63}
64
65impl MoonPhase {
66    pub fn new(time: SystemTime) -> Self {
67        let j_date = julian_date(time);
68
69        // Calculate illumination (synodic) phase.
70        // From number of days since new moon on Julian date MOON_SYNODIC_OFFSET
71        // (1815UTC January 6, 2000), determine remainder of incomplete cycle.
72        let phase =
73            ((j_date - MOON_SYNODIC_OFFSET) / MOON_SYNODIC_PERIOD).fract();
74        // Calculate age and illuination fraction.
75        let age = phase * MOON_SYNODIC_PERIOD;
76        let fraction = (1. - (std::f64::consts::TAU * phase)).cos() / 2.;
77        let phase_name = PHASE_NAMES[(phase * 8.).round() as usize % 8];
78        // Calculate distance fro anoalistic phase.
79        let distance_phase =
80            ((j_date - MOON_DISTANCE_OFFSET) / MOON_DISTANCE_PERIOD).fract();
81        let distance_phase_tau = TAU * distance_phase;
82        let phase_tau = 2. * TAU * phase;
83        let phase_distance_tau_difference = phase_tau - distance_phase_tau;
84        let distance = 60.4
85            - 3.3 * distance_phase_tau.cos()
86            - 0.6 * (phase_distance_tau_difference).cos()
87            - 0.5 * (phase_tau).cos();
88
89        // Calculate ecliptic latitude from nodal (draconic) phase.
90        let lat_phase =
91            ((j_date - MOON_LATITUDE_OFFSET) / MOON_LATITUDE_PERIOD).fract();
92        let latitude = 5.1 * (TAU * lat_phase).sin();
93
94        // Calculate ecliptic longitude ffrom sidereal motion.
95        let long_phase =
96            ((j_date - MOON_LONGITUDE_OFFSET) / MOON_LONGITUDE_PERIOD).fract();
97        let longitude = (360. * long_phase
98            + 6.3 * (distance_phase_tau).sin()
99            + 1.3 * (phase_distance_tau_difference).sin()
100            + 0.7 * (phase_tau).sin())
101            % 360.;
102
103        let zodiac_name = ZODIAC_ANGLES
104            .iter()
105            .zip(ZODIAC_NAMES.iter())
106            .find_map(|(angle, name)| {
107                if longitude < *angle {
108                    Some(*name)
109                } else {
110                    None
111                }
112            })
113            .unwrap_or_else(|| ZODIAC_NAMES[0]);
114        MoonPhase {
115            j_date,
116            phase,
117            age,
118            fraction,
119            distance,
120            latitude,
121            longitude,
122            phase_name,
123            zodiac_name,
124        }
125    }
126}