1use serde::Serialize;
7
8use crate::dignities::{is_diurnal, traditional_ruler, SEVEN, SIGNS};
9use crate::instant::UtcInstant;
10use crate::pyfloat;
11
12const YEAR_DAYS: f64 = 365.242_19;
14
15const DIURNAL_FIRDARIA: [(&str, f64); 9] = [
17 ("Sun", 10.0),
18 ("Venus", 8.0),
19 ("Mercury", 13.0),
20 ("Moon", 9.0),
21 ("Saturn", 11.0),
22 ("Jupiter", 12.0),
23 ("Mars", 7.0),
24 ("North Node", 3.0),
25 ("South Node", 2.0),
26];
27
28const NOCTURNAL_FIRDARIA: [(&str, f64); 9] = [
30 ("Moon", 9.0),
31 ("Saturn", 11.0),
32 ("Jupiter", 12.0),
33 ("Mars", 7.0),
34 ("Sun", 10.0),
35 ("Venus", 8.0),
36 ("Mercury", 13.0),
37 ("North Node", 3.0),
38 ("South Node", 2.0),
39];
40
41const FIRDARIA_CYCLE: f64 = 75.0;
43
44#[derive(Debug, Clone, PartialEq, Serialize)]
46pub struct Profection {
47 pub age: u32,
49 pub start: String,
51 pub end: String,
53 pub sign: &'static str,
55 pub lord: &'static str,
57 pub house: u8,
59}
60
61#[derive(Debug, Clone, PartialEq, Serialize)]
63pub struct Period {
64 pub lord: &'static str,
66 pub start: String,
68 pub end: String,
70}
71
72#[derive(Debug, Clone, PartialEq, Serialize)]
74pub struct Firdaria {
75 pub lord: &'static str,
77 pub start: String,
79 pub end: String,
81 pub sub_periods: Vec<Period>,
83}
84
85#[derive(Debug, Clone, PartialEq, Serialize)]
87pub struct TimeLords {
88 pub diurnal: bool,
90 pub profections: Vec<Profection>,
92 pub firdaria: Vec<Firdaria>,
94}
95
96fn at(birth: UtcInstant, years: f64) -> UtcInstant {
97 birth.plus_days(years * YEAR_DAYS)
98}
99
100pub fn time_lords(
103 birth: UtcInstant,
104 sun_longitude: f64,
105 ascendant_longitude: f64,
106 start: UtcInstant,
107 end: UtcInstant,
108) -> TimeLords {
109 let (start, end) = if end < start {
110 (end, start)
111 } else {
112 (start, end)
113 };
114 let start = start.max(birth);
115 let end = end.max(birth);
116 let diurnal = is_diurnal(sun_longitude, ascendant_longitude);
117 let age_at = |t: UtcInstant| t.seconds_since(&birth) / 86_400.0 / YEAR_DAYS;
118
119 let asc_sign =
120 (pyfloat::floordiv(pyfloat::rem(ascendant_longitude, 360.0), 30.0) as usize).min(11);
121 let first = age_at(start).floor() as u32;
122 let last = age_at(end).floor() as u32;
123 let profections = (first..=last)
124 .map(|age| {
125 let sign = SIGNS[(asc_sign + age as usize) % 12];
126 Profection {
127 age,
128 start: at(birth, age as f64).isoformat(),
129 end: at(birth, age as f64 + 1.0).isoformat(),
130 sign,
131 lord: traditional_ruler(sign).unwrap_or("Mars"),
132 house: (age % 12) as u8 + 1,
133 }
134 })
135 .collect();
136
137 let sequence = if diurnal {
138 DIURNAL_FIRDARIA
139 } else {
140 NOCTURNAL_FIRDARIA
141 };
142 let (from, to) = (age_at(start), age_at(end));
143 let mut firdaria = Vec::new();
144 let mut cycle_start = (from / FIRDARIA_CYCLE).floor() * FIRDARIA_CYCLE;
145 'cycles: loop {
146 let mut offset = cycle_start;
147 for (lord, years) in sequence {
148 let (period_start, period_end) = (offset, offset + years);
149 offset = period_end;
150 if period_start > to {
151 break 'cycles;
152 }
153 if period_end <= from {
154 continue;
155 }
156 let sub_periods = if SEVEN.contains(&lord) {
157 let first = SEVEN.iter().position(|p| *p == lord).unwrap_or(0);
158 (0..7)
159 .map(|i| {
160 let length = years / 7.0;
161 Period {
162 lord: SEVEN[(first + i) % 7],
163 start: at(birth, period_start + length * i as f64).isoformat(),
164 end: at(birth, period_start + length * (i + 1) as f64).isoformat(),
165 }
166 })
167 .collect()
168 } else {
169 Vec::new()
170 };
171 firdaria.push(Firdaria {
172 lord,
173 start: at(birth, period_start).isoformat(),
174 end: at(birth, period_end).isoformat(),
175 sub_periods,
176 });
177 }
178 cycle_start += FIRDARIA_CYCLE;
179 }
180
181 TimeLords {
182 diurnal,
183 profections,
184 firdaria,
185 }
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191
192 fn utc(text: &str) -> UtcInstant {
193 UtcInstant::parse(text).unwrap()
194 }
195
196 #[test]
197 fn firdaria_sequences_last_seventy_five_years() {
198 for sequence in [DIURNAL_FIRDARIA, NOCTURNAL_FIRDARIA] {
199 assert_eq!(sequence.iter().map(|(_, y)| y).sum::<f64>(), FIRDARIA_CYCLE);
200 }
201 }
202
203 #[test]
204 fn profections_move_one_sign_a_year() {
205 let birth = utc("1990-01-01T12:00:00Z");
207 let lords = time_lords(birth, 280.0, 130.0, birth, utc("2003-01-01T00:00:00Z"));
208 let p = &lords.profections;
209 assert_eq!(p[0].sign, "Leo");
210 assert_eq!(p[0].lord, "Sun");
211 assert_eq!(p[0].house, 1);
212 assert_eq!(p[1].sign, "Virgo");
213 assert_eq!(p[1].lord, "Mercury");
214 assert_eq!(p[12].sign, "Leo");
215 assert_eq!(p[12].house, 1);
216 assert_eq!(p[3].house, 4);
217 }
218
219 #[test]
220 fn a_day_birth_starts_with_the_sun() {
221 let birth = utc("2000-03-20T12:00:00Z");
223 let lords = time_lords(birth, 0.0, 90.0, birth, birth.plus_days(1.0));
224 assert!(lords.diurnal);
225 let first = &lords.firdaria[0];
226 assert_eq!(first.lord, "Sun");
227 let subs: Vec<&str> = first.sub_periods.iter().map(|s| s.lord).collect();
228 assert_eq!(
229 subs,
230 ["Sun", "Venus", "Mercury", "Moon", "Saturn", "Jupiter", "Mars"]
231 );
232 assert_eq!(first.end, at(birth, 10.0).isoformat());
233 }
234
235 #[test]
236 fn a_night_birth_starts_with_the_moon_and_the_nodes_have_no_sub_periods() {
237 let birth = utc("2000-03-20T00:00:00Z");
238 let lords = time_lords(birth, 0.0, 270.0, birth, at(birth, 74.0));
240 assert!(!lords.diurnal);
241 let names: Vec<&str> = lords.firdaria.iter().map(|f| f.lord).collect();
242 assert_eq!(
243 names,
244 [
245 "Moon",
246 "Saturn",
247 "Jupiter",
248 "Mars",
249 "Sun",
250 "Venus",
251 "Mercury",
252 "North Node",
253 "South Node"
254 ]
255 );
256 assert!(lords.firdaria[7].sub_periods.is_empty());
257 assert_eq!(lords.firdaria[1].sub_periods[0].lord, "Saturn");
258 assert_eq!(lords.firdaria[1].sub_periods[1].lord, "Jupiter");
259 }
260
261 #[test]
262 fn a_span_late_in_life_wraps_into_a_new_round() {
263 let birth = utc("1900-01-01T12:00:00Z");
264 let lords = time_lords(birth, 280.0, 130.0, at(birth, 76.0), at(birth, 77.0));
265 assert_eq!(lords.firdaria.len(), 1);
267 assert_eq!(lords.firdaria[0].lord, "Moon");
268 assert_eq!(lords.profections[0].age, 76);
269 }
270}