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