astroceleste_engine/
degree_qualities.rs1use serde::Serialize;
6
7use crate::catalog::{DegreeRun, SignDegrees, DEGREE_QUALITIES};
8use crate::pyfloat;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
12pub struct DegreeQualities {
13 pub sign: &'static str,
15 pub degree: u8,
17 pub gender: &'static str,
19 pub light: &'static str,
21 pub pitted: bool,
23 pub azimene: bool,
25 pub fortune: bool,
27}
28
29pub fn degree_quality_table() -> &'static [SignDegrees; 12] {
31 &DEGREE_QUALITIES
32}
33
34pub fn degree_qualities(longitude: f64) -> DegreeQualities {
37 let lon = pyfloat::rem(longitude, 360.0);
38 let sign = &DEGREE_QUALITIES[(pyfloat::floordiv(lon, 30.0) as usize) % 12];
39 let degree = (pyfloat::floordiv(pyfloat::rem(lon, 30.0), 1.0) as u8 + 1).clamp(1, 30);
40 DegreeQualities {
41 sign: sign.sign,
42 degree,
43 gender: run_quality(sign.gender, degree),
44 light: run_quality(sign.light, degree),
45 pitted: sign.pitted.contains(°ree),
46 azimene: sign.azimene.contains(°ree),
47 fortune: sign.fortune.contains(°ree),
48 }
49}
50
51fn run_quality(runs: &[DegreeRun], degree: u8) -> &'static str {
52 runs.iter()
53 .find(|run| degree <= run.end)
54 .map_or("", |run| run.quality)
55}
56
57#[cfg(test)]
58mod tests {
59 use super::*;
60
61 #[test]
62 fn every_sign_is_covered() {
63 for sign in degree_quality_table() {
64 for runs in [sign.gender, sign.light] {
65 assert!(
66 runs.windows(2).all(|w| w[0].end < w[1].end),
67 "{}",
68 sign.sign
69 );
70 assert!(runs.windows(2).all(|w| w[0].quality != w[1].quality));
71 assert_eq!(runs.last().unwrap().end, 30, "{}", sign.sign);
72 }
73 assert!(sign
74 .gender
75 .iter()
76 .all(|r| matches!(r.quality, "masculine" | "feminine")));
77 assert!(sign
78 .light
79 .iter()
80 .all(|r| matches!(r.quality, "light" | "dark" | "smoky" | "void")));
81 for list in [sign.pitted, sign.azimene, sign.fortune] {
82 assert!(list.windows(2).all(|w| w[0] < w[1]), "{}", sign.sign);
83 assert!(list.iter().all(|d| (1..=30).contains(d)));
84 }
85 }
86 let signs: Vec<_> = degree_quality_table().iter().map(|s| s.sign).collect();
87 assert_eq!(signs, crate::dignities::SIGNS);
88 }
89
90 #[test]
91 fn degrees_are_ordinal() {
92 let sixth = degree_qualities(5.99);
94 assert_eq!((sixth.sign, sixth.degree), ("Aries", 6));
95 assert!(sixth.pitted);
96 let seventh = degree_qualities(6.0);
97 assert_eq!(seventh.degree, 7);
98 assert!(!seventh.pitted);
99 assert_eq!(degree_qualities(0.0).degree, 1);
100 assert_eq!(degree_qualities(29.999).degree, 30);
101 }
102
103 #[test]
104 fn runs_end_on_their_last_degree() {
105 assert_eq!(degree_qualities(7.5).gender, "masculine");
107 assert_eq!(degree_qualities(8.5).gender, "feminine");
108 assert_eq!(degree_qualities(9.0).gender, "masculine");
109 assert_eq!(degree_qualities(2.9).light, "dark");
110 assert_eq!(degree_qualities(3.0).light, "light");
111 }
112
113 #[test]
114 fn longitudes_wrap() {
115 assert_eq!(degree_qualities(360.0), degree_qualities(0.0));
116 assert_eq!(degree_qualities(-0.5).sign, "Pisces");
117 assert_eq!(degree_qualities(-0.5).degree, 30);
118 assert_eq!(degree_qualities(725.0), degree_qualities(5.0));
119 }
120
121 #[test]
122 fn spot_checks() {
123 let taurus_8th = degree_qualities(37.5);
125 assert!(taurus_8th.azimene && !taurus_8th.pitted);
126 assert_eq!(taurus_8th.light, "void");
127 assert!(degree_qualities(32.0).fortune);
128 let leo_28th = degree_qualities(147.2);
130 assert_eq!(leo_28th.sign, "Leo");
131 assert!(leo_28th.pitted && leo_28th.azimene);
132 assert_eq!(leo_28th.light, "light");
133 let cap_13th = degree_qualities(282.5);
135 assert_eq!((cap_13th.light, cap_13th.fortune), ("smoky", true));
136 assert_eq!(cap_13th.gender, "feminine");
137 let last = degree_qualities(329.9);
139 assert_eq!((last.gender, last.light), ("feminine", "light"));
140 }
141}