1use serde::Serialize;
4
5use crate::aspects::Aspect;
6use crate::chart::Placement;
7use crate::dignities::{element as element_of, traditional_ruler};
8use crate::minor_bodies::is_minor_body;
9use crate::pyfloat;
10
11type Q = [f64; 4];
13
14fn element_qualities(element: &str) -> Q {
15 match element {
16 "Fire" => [1.0, 0.0, 0.0, 1.0],
17 "Air" => [1.0, 0.0, 1.0, 0.0],
18 "Water" => [0.0, 1.0, 1.0, 0.0],
19 "Earth" => [0.0, 1.0, 0.0, 1.0],
20 _ => [0.0; 4],
21 }
22}
23
24fn planet_qualities(planet: &str) -> Option<Q> {
25 Some(match planet {
26 "Sun" | "Mars" | "Uranus" | "Pluto" => [1.0, 0.0, 0.0, 1.0],
27 "Moon" | "Venus" | "Neptune" => [0.0, 1.0, 1.0, 0.0],
28 "Mercury" | "Saturn" => [0.0, 1.0, 0.0, 1.0],
29 "Jupiter" => [1.0, 0.0, 1.0, 0.0],
30 _ => return None,
31 })
32}
33
34#[derive(Debug, Clone, PartialEq, Serialize)]
36pub struct Qualities {
37 pub hot: f64,
39 pub cold: f64,
41 pub wet: f64,
43 pub dry: f64,
45}
46
47#[derive(Debug, Clone, PartialEq, Serialize)]
49pub struct Scores {
50 pub choleric: f64,
52 pub sanguine: f64,
54 pub phlegmatic: f64,
56 pub melancholic: f64,
58}
59
60#[derive(Debug, Clone, PartialEq, Serialize)]
62pub struct Factor {
63 pub factor: &'static str,
65 pub details: String,
67 pub hot: f64,
69 pub cold: f64,
71 pub wet: f64,
73 pub dry: f64,
75}
76
77#[derive(Debug, Clone, PartialEq, Serialize)]
79pub struct Temperament {
80 pub primary_temperament: &'static str,
82 pub secondary_temperament: &'static str,
84 pub scores: Scores,
86 pub percentages: Scores,
88 pub qualities: Qualities,
90 pub quality_percentages: Qualities,
92 pub breakdown: Vec<Factor>,
94}
95
96pub fn temperament(planets: &[Placement], aspects: &[Aspect]) -> Temperament {
97 let mut totals: Q = [0.0; 4];
98 let mut breakdown = Vec::new();
99 let mut add = |factor: &'static str, details: String, q: Q, weight: f64| {
100 let w = q.map(|v| v * weight);
101 for i in 0..4 {
102 totals[i] += w[i];
103 }
104 breakdown.push(Factor {
105 factor,
106 details,
107 hot: pyfloat::round(w[0], 1),
108 cold: pyfloat::round(w[1], 1),
109 wet: pyfloat::round(w[2], 1),
110 dry: pyfloat::round(w[3], 1),
111 });
112 };
113 let by_name = |name: &str| planets.iter().rev().find(|p| p.name == name);
115 let sun = by_name("Sun");
116 let moon = by_name("Moon");
117
118 if let Some(asc) = by_name("Ascendant") {
119 let element = element_of(asc.sign).unwrap_or("Fire");
120 add(
121 "Ascendant Sign",
122 format!("{} ({element})", asc.sign),
123 element_qualities(element),
124 4.0,
125 );
126 let lord = traditional_ruler(asc.sign).unwrap_or("Mars");
127 if let Some(lord_planet) = by_name(lord) {
128 add(
129 "Lord of Ascendant Nature",
130 lord.to_string(),
131 planet_qualities(lord).unwrap_or_default(),
132 3.0,
133 );
134 let lord_element = element_of(lord_planet.sign).unwrap_or("Fire");
135 add(
136 "Lord of Ascendant Sign",
137 format!("{lord} in {} ({lord_element})", lord_planet.sign),
138 element_qualities(lord_element),
139 3.0,
140 );
141 }
142 }
143
144 if let Some(sun) = sun {
145 let element = element_of(sun.sign).unwrap_or("Fire");
146 add(
147 "Sun Sign",
148 format!("{} ({element})", sun.sign),
149 element_qualities(element),
150 4.0,
151 );
152 let (season, q) = match sun.sign {
153 "Aries" | "Taurus" | "Gemini" => ("Spring (Hot & Wet)", [1.0, 0.0, 1.0, 0.0]),
154 "Cancer" | "Leo" | "Virgo" => ("Summer (Hot & Dry)", [1.0, 0.0, 0.0, 1.0]),
155 "Libra" | "Scorpio" | "Sagittarius" => ("Autumn (Cold & Dry)", [0.0, 1.0, 0.0, 1.0]),
156 _ => ("Winter (Cold & Wet)", [0.0, 1.0, 1.0, 0.0]),
157 };
158 add("Solar Season", season.to_string(), q, 2.0);
159 }
160
161 if let Some(moon) = moon {
162 let element = element_of(moon.sign).unwrap_or("Water");
163 add(
164 "Moon Sign",
165 format!("{} ({element})", moon.sign),
166 element_qualities(element),
167 4.0,
168 );
169 if let Some(sun) = sun {
170 let diff = pyfloat::rem(moon.ecliptic_longitude - sun.ecliptic_longitude, 360.0);
171 let (phase, q) = if diff < 90.0 {
172 ("1st Quarter Waxing (Hot & Wet)", [1.0, 0.0, 1.0, 0.0])
173 } else if diff < 180.0 {
174 ("2nd Quarter Gibbous (Hot & Dry)", [1.0, 0.0, 0.0, 1.0])
175 } else if diff < 270.0 {
176 ("3rd Quarter Full/Waning (Cold & Dry)", [0.0, 1.0, 0.0, 1.0])
177 } else {
178 ("4th Quarter Crescent (Cold & Wet)", [0.0, 1.0, 1.0, 0.0])
179 };
180 add("Lunar Phase", phase.to_string(), q, 2.0);
181 }
182 }
183
184 for p in planets {
185 if matches!(
187 p.name,
188 "Ascendant" | "Midheaven" | "North Node" | "South Node" | "Part of Fortune"
189 ) || is_minor_body(p.name)
190 {
191 continue;
192 }
193 if p.house == 1 {
194 let pq = planet_qualities(p.name).unwrap_or_default();
195 let sq = element_qualities(element_of(p.sign).unwrap_or("Fire"));
196 let combined = [0, 1, 2, 3].map(|i| pq[i] + sq[i]);
197 add(
198 "Planet in House 1",
199 format!("{} in {}", p.name, p.sign),
200 combined,
201 1.0,
202 );
203 }
204 }
205
206 for a in aspects {
207 if a.body1 == "Ascendant" || a.body2 == "Ascendant" {
208 let other = if a.body1 == "Ascendant" {
209 &a.body2
210 } else {
211 &a.body1
212 };
213 if let Some(q) = planet_qualities(other) {
214 add(
215 "Aspect to Ascendant",
216 format!("{other} {} ASC", a.aspect_type),
217 q,
218 1.0,
219 );
220 }
221 }
222 }
223
224 let [hot, cold, wet, dry] = totals;
225 let (choleric, sanguine, phlegmatic, melancholic) =
226 (hot + dry, hot + wet, cold + wet, cold + dry);
227 let total_q = (hot + cold + wet + dry).max(1.0);
228 let total_t = (choleric + sanguine + phlegmatic + melancholic).max(1.0);
229 let pct = |v: f64, total: f64| pyfloat::round((v / total) * 100.0, 1);
230
231 let mut ranked = [
233 ("Choleric", choleric),
234 ("Sanguine", sanguine),
235 ("Phlegmatic", phlegmatic),
236 ("Melancholic", melancholic),
237 ];
238 ranked.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
239
240 Temperament {
241 primary_temperament: ranked[0].0,
242 secondary_temperament: if ranked[1].1 > 0.0 { ranked[1].0 } else { "" },
243 scores: Scores {
244 choleric: pyfloat::round(choleric, 1),
245 sanguine: pyfloat::round(sanguine, 1),
246 phlegmatic: pyfloat::round(phlegmatic, 1),
247 melancholic: pyfloat::round(melancholic, 1),
248 },
249 percentages: Scores {
250 choleric: pct(choleric, total_t),
251 sanguine: pct(sanguine, total_t),
252 phlegmatic: pct(phlegmatic, total_t),
253 melancholic: pct(melancholic, total_t),
254 },
255 qualities: Qualities {
256 hot: pyfloat::round(hot, 1),
257 cold: pyfloat::round(cold, 1),
258 wet: pyfloat::round(wet, 1),
259 dry: pyfloat::round(dry, 1),
260 },
261 quality_percentages: Qualities {
262 hot: pct(hot, total_q),
263 cold: pct(cold, total_q),
264 wet: pct(wet, total_q),
265 dry: pct(dry, total_q),
266 },
267 breakdown,
268 }
269}