use serde::Serialize;
use crate::aspects::Aspect;
use crate::chart::Placement;
use crate::pyfloat;
type Q = [f64; 4];
fn element_of(sign: &str) -> Option<&'static str> {
Some(match sign {
"Aries" | "Leo" | "Sagittarius" => "Fire",
"Taurus" | "Virgo" | "Capricorn" => "Earth",
"Gemini" | "Libra" | "Aquarius" => "Air",
"Cancer" | "Scorpio" | "Pisces" => "Water",
_ => return None,
})
}
fn element_qualities(element: &str) -> Q {
match element {
"Fire" => [1.0, 0.0, 0.0, 1.0],
"Air" => [1.0, 0.0, 1.0, 0.0],
"Water" => [0.0, 1.0, 1.0, 0.0],
"Earth" => [0.0, 1.0, 0.0, 1.0],
_ => [0.0; 4],
}
}
fn ruler_of(sign: &str) -> Option<&'static str> {
Some(match sign {
"Aries" | "Scorpio" => "Mars",
"Taurus" | "Libra" => "Venus",
"Gemini" | "Virgo" => "Mercury",
"Cancer" => "Moon",
"Leo" => "Sun",
"Sagittarius" | "Pisces" => "Jupiter",
"Capricorn" | "Aquarius" => "Saturn",
_ => return None,
})
}
fn planet_qualities(planet: &str) -> Option<Q> {
Some(match planet {
"Sun" | "Mars" | "Uranus" | "Pluto" => [1.0, 0.0, 0.0, 1.0],
"Moon" | "Venus" | "Neptune" => [0.0, 1.0, 1.0, 0.0],
"Mercury" | "Saturn" => [0.0, 1.0, 0.0, 1.0],
"Jupiter" => [1.0, 0.0, 1.0, 0.0],
_ => return None,
})
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct Qualities {
pub hot: f64,
pub cold: f64,
pub wet: f64,
pub dry: f64,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct Scores {
pub choleric: f64,
pub sanguine: f64,
pub phlegmatic: f64,
pub melancholic: f64,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct Factor {
pub factor: &'static str,
pub details: String,
pub hot: f64,
pub cold: f64,
pub wet: f64,
pub dry: f64,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct Temperament {
pub primary_temperament: &'static str,
pub secondary_temperament: &'static str,
pub scores: Scores,
pub percentages: Scores,
pub qualities: Qualities,
pub quality_percentages: Qualities,
pub breakdown: Vec<Factor>,
}
pub fn temperament(planets: &[Placement], aspects: &[Aspect]) -> Temperament {
let mut totals: Q = [0.0; 4];
let mut breakdown = Vec::new();
let mut add = |factor: &'static str, details: String, q: Q, weight: f64| {
let w = q.map(|v| v * weight);
for i in 0..4 {
totals[i] += w[i];
}
breakdown.push(Factor {
factor,
details,
hot: pyfloat::round(w[0], 1),
cold: pyfloat::round(w[1], 1),
wet: pyfloat::round(w[2], 1),
dry: pyfloat::round(w[3], 1),
});
};
let by_name = |name: &str| planets.iter().rev().find(|p| p.name == name);
let sun = by_name("Sun");
let moon = by_name("Moon");
if let Some(asc) = by_name("Ascendant") {
let element = element_of(asc.sign).unwrap_or("Fire");
add(
"Ascendant Sign",
format!("{} ({element})", asc.sign),
element_qualities(element),
4.0,
);
let lord = ruler_of(asc.sign).unwrap_or("Mars");
if let Some(lord_planet) = by_name(lord) {
add(
"Lord of Ascendant Nature",
lord.to_string(),
planet_qualities(lord).unwrap_or_default(),
3.0,
);
let lord_element = element_of(lord_planet.sign).unwrap_or("Fire");
add(
"Lord of Ascendant Sign",
format!("{lord} in {} ({lord_element})", lord_planet.sign),
element_qualities(lord_element),
3.0,
);
}
}
if let Some(sun) = sun {
let element = element_of(sun.sign).unwrap_or("Fire");
add(
"Sun Sign",
format!("{} ({element})", sun.sign),
element_qualities(element),
4.0,
);
let (season, q) = match sun.sign {
"Aries" | "Taurus" | "Gemini" => ("Spring (Hot & Wet)", [1.0, 0.0, 1.0, 0.0]),
"Cancer" | "Leo" | "Virgo" => ("Summer (Hot & Dry)", [1.0, 0.0, 0.0, 1.0]),
"Libra" | "Scorpio" | "Sagittarius" => ("Autumn (Cold & Dry)", [0.0, 1.0, 0.0, 1.0]),
_ => ("Winter (Cold & Wet)", [0.0, 1.0, 1.0, 0.0]),
};
add("Solar Season", season.to_string(), q, 2.0);
}
if let Some(moon) = moon {
let element = element_of(moon.sign).unwrap_or("Water");
add(
"Moon Sign",
format!("{} ({element})", moon.sign),
element_qualities(element),
4.0,
);
if let Some(sun) = sun {
let diff = pyfloat::rem(moon.ecliptic_longitude - sun.ecliptic_longitude, 360.0);
let (phase, q) = if diff < 90.0 {
("1st Quarter Waxing (Hot & Wet)", [1.0, 0.0, 1.0, 0.0])
} else if diff < 180.0 {
("2nd Quarter Gibbous (Hot & Dry)", [1.0, 0.0, 0.0, 1.0])
} else if diff < 270.0 {
("3rd Quarter Full/Waning (Cold & Dry)", [0.0, 1.0, 0.0, 1.0])
} else {
("4th Quarter Crescent (Cold & Wet)", [0.0, 1.0, 1.0, 0.0])
};
add("Lunar Phase", phase.to_string(), q, 2.0);
}
}
for p in planets {
if matches!(
p.name,
"Ascendant" | "Midheaven" | "North Node" | "South Node" | "Part of Fortune"
) {
continue;
}
if p.house == 1 {
let pq = planet_qualities(p.name).unwrap_or_default();
let sq = element_qualities(element_of(p.sign).unwrap_or("Fire"));
let combined = [0, 1, 2, 3].map(|i| pq[i] + sq[i]);
add(
"Planet in House 1",
format!("{} in {}", p.name, p.sign),
combined,
1.0,
);
}
}
for a in aspects {
if a.body1 == "Ascendant" || a.body2 == "Ascendant" {
let other = if a.body1 == "Ascendant" {
&a.body2
} else {
&a.body1
};
if let Some(q) = planet_qualities(other) {
add(
"Aspect to Ascendant",
format!("{other} {} ASC", a.aspect_type),
q,
1.0,
);
}
}
}
let [hot, cold, wet, dry] = totals;
let (choleric, sanguine, phlegmatic, melancholic) =
(hot + dry, hot + wet, cold + wet, cold + dry);
let total_q = (hot + cold + wet + dry).max(1.0);
let total_t = (choleric + sanguine + phlegmatic + melancholic).max(1.0);
let pct = |v: f64, total: f64| pyfloat::round((v / total) * 100.0, 1);
let mut ranked = [
("Choleric", choleric),
("Sanguine", sanguine),
("Phlegmatic", phlegmatic),
("Melancholic", melancholic),
];
ranked.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
Temperament {
primary_temperament: ranked[0].0,
secondary_temperament: if ranked[1].1 > 0.0 { ranked[1].0 } else { "" },
scores: Scores {
choleric: pyfloat::round(choleric, 1),
sanguine: pyfloat::round(sanguine, 1),
phlegmatic: pyfloat::round(phlegmatic, 1),
melancholic: pyfloat::round(melancholic, 1),
},
percentages: Scores {
choleric: pct(choleric, total_t),
sanguine: pct(sanguine, total_t),
phlegmatic: pct(phlegmatic, total_t),
melancholic: pct(melancholic, total_t),
},
qualities: Qualities {
hot: pyfloat::round(hot, 1),
cold: pyfloat::round(cold, 1),
wet: pyfloat::round(wet, 1),
dry: pyfloat::round(dry, 1),
},
quality_percentages: Qualities {
hot: pct(hot, total_q),
cold: pct(cold, total_q),
wet: pct(wet, total_q),
dry: pct(dry, total_q),
},
breakdown,
}
}