Skip to main content

astroceleste_engine/
lots.rs

1//! Hermetic lots / Arabic parts (`charts/calc/arabic_parts.py`).
2
3use serde::Serialize;
4
5use crate::aspects::Point;
6use crate::pyfloat;
7use crate::symbolic::symbolic_degree_number;
8use crate::zodiac::{determine_house, longitude_to_zodiac};
9
10struct LotDefinition {
11    name: &'static str,
12    symbol: &'static str,
13    /// (base, add, subtract) by day; swapped add/subtract by night.
14    day: (&'static str, &'static str, &'static str),
15}
16
17const LOTS: [LotDefinition; 11] = [
18    LotDefinition {
19        name: "Fortune",
20        symbol: "⊗",
21        day: ("Ascendant", "Moon", "Sun"),
22    },
23    LotDefinition {
24        name: "Spirit",
25        symbol: "🜕",
26        day: ("Ascendant", "Sun", "Moon"),
27    },
28    LotDefinition {
29        name: "Eros",
30        symbol: "🏹",
31        day: ("Ascendant", "Venus", "Spirit"),
32    },
33    LotDefinition {
34        name: "Necessity",
35        symbol: "⛓",
36        day: ("Ascendant", "Fortune", "Mercury"),
37    },
38    LotDefinition {
39        name: "Courage",
40        symbol: "🛡",
41        day: ("Ascendant", "Fortune", "Mars"),
42    },
43    LotDefinition {
44        name: "Victory",
45        symbol: "🏆",
46        day: ("Ascendant", "Jupiter", "Fortune"),
47    },
48    LotDefinition {
49        name: "Nemesis",
50        symbol: "⚖",
51        day: ("Ascendant", "Fortune", "Saturn"),
52    },
53    LotDefinition {
54        name: "Marriage",
55        symbol: "💍",
56        day: ("Ascendant", "Venus", "Saturn"),
57    },
58    LotDefinition {
59        name: "Commerce",
60        symbol: "☤",
61        day: ("Ascendant", "Mercury", "Sun"),
62    },
63    LotDefinition {
64        name: "Sickness",
65        symbol: "⚕",
66        day: ("Ascendant", "Mars", "Saturn"),
67    },
68    LotDefinition {
69        name: "Death",
70        symbol: "☠",
71        day: ("Ascendant", "House 8", "Moon"),
72    },
73];
74
75/// An Arabic part (lot) placed in the chart.
76#[derive(Debug, Clone, PartialEq, Serialize)]
77pub struct Lot {
78    /// Lot name, e.g. "Fortune".
79    pub name: &'static str,
80    /// Lot glyph.
81    pub symbol: &'static str,
82    /// Zodiac sign name, e.g. "Taurus".
83    pub sign: &'static str,
84    /// Zodiac sign glyph, e.g. "♉".
85    pub sign_symbol: &'static str,
86    /// Whole degrees within the sign (0-29).
87    pub degree: i64,
88    /// Arc minutes past `degree` (0-59).
89    pub minute: i64,
90    /// Ecliptic longitude in degrees [0, 360), tropical or sidereal as requested.
91    pub ecliptic_longitude: f64,
92    /// House (1-12) the point falls in.
93    pub house: u8,
94    /// Formula applied, e.g. "ASC + Moon - Sun (Day)".
95    pub formula_used: String,
96    /// Whether the chart is diurnal (Sun above the horizon), which selects the formula.
97    pub is_diurnal: bool,
98    /// Symbolic degree (1-30) within the sign, as used by degree symbolism.
99    pub symbolic_degree: i64,
100}
101
102/// The eleven classical lots, with day/night formulas by the Sun's hemisphere.
103/// `points` are the planets and angles (by name), `cusps` the twelve house cusps.
104pub fn arabic_parts(points: &[Point], cusps: &[f64]) -> Vec<Lot> {
105    let find = |name: &str| points.iter().find(|p| p.name == name).map(|p| p.longitude);
106    let (Some(sun), Some(_)) = (find("Sun"), find("Ascendant")) else {
107        return Vec::new();
108    };
109    let sun_house = if cusps.is_empty() {
110        1
111    } else {
112        determine_house(sun, cusps)
113    };
114    let is_day = sun_house >= 7;
115    let period = if is_day { "Day" } else { "Night" };
116
117    let mut computed: Vec<(&str, f64)> = Vec::new();
118    let mut out = Vec::new();
119    for lot in &LOTS {
120        let (base, a, b) = if is_day {
121            lot.day
122        } else {
123            (lot.day.0, lot.day.2, lot.day.1)
124        };
125        let lon_of = |name: &str| {
126            if let Some((_, lon)) = computed.iter().find(|(n, _)| *n == name) {
127                return *lon;
128            }
129            if let Some(lon) = find(name) {
130                return lon;
131            }
132            if name == "House 8" && cusps.len() >= 8 {
133                return cusps[7];
134            }
135            0.0
136        };
137        let lon = pyfloat::rem(lon_of(base) + lon_of(a) - lon_of(b), 360.0);
138        computed.push((lot.name, lon));
139        let z = longitude_to_zodiac(lon);
140        let formula_used = if base == "Ascendant" {
141            format!("ASC + {a} - {b} ({period})")
142        } else {
143            format!("{base} + {a} - {b} ({period})")
144        };
145        out.push(Lot {
146            name: lot.name,
147            symbol: lot.symbol,
148            sign: z.sign.name,
149            sign_symbol: z.sign.symbol,
150            degree: z.degree,
151            minute: z.minute,
152            ecliptic_longitude: lon,
153            house: determine_house(lon, cusps),
154            formula_used,
155            is_diurnal: is_day,
156            symbolic_degree: symbolic_degree_number(z.degree, z.minute),
157        });
158    }
159    out
160}