1use serde::Serialize;
10
11use serde_json::Value;
12
13use crate::aspects::ASPECT_RULES;
14use crate::chart::Placement;
15use crate::error::EngineError;
16use crate::pyfloat;
17
18pub(crate) const SEVEN: [&str; 7] = [
20 "Saturn", "Jupiter", "Mars", "Sun", "Venus", "Mercury", "Moon",
21];
22
23pub(crate) const SIGNS: [&str; 12] = [
24 "Aries",
25 "Taurus",
26 "Gemini",
27 "Cancer",
28 "Leo",
29 "Virgo",
30 "Libra",
31 "Scorpio",
32 "Sagittarius",
33 "Capricorn",
34 "Aquarius",
35 "Pisces",
36];
37
38pub(crate) const CAZIMI_ORB: f64 = 17.0 / 60.0;
40pub(crate) const COMBUST_ORB: f64 = 8.5;
42const UNDER_BEAMS_ORB: f64 = 15.0;
44const ANTISCIA_ORB: f64 = 1.0;
46const STATIONARY_FRACTION: f64 = 0.1;
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
51pub enum DignityScheme {
52 #[default]
55 Lilly,
56 Dorothean,
59}
60
61impl DignityScheme {
62 pub fn from_code(code: &str) -> Self {
64 match code.trim().to_lowercase().as_str() {
65 "dorothean" | "dorotheus" | "egyptian" => DignityScheme::Dorothean,
66 _ => DignityScheme::Lilly,
67 }
68 }
69
70 pub fn code(self) -> &'static str {
72 match self {
73 DignityScheme::Lilly => "lilly",
74 DignityScheme::Dorothean => "dorothean",
75 }
76 }
77}
78
79pub(crate) fn sign_index(sign: &str) -> Option<usize> {
80 SIGNS.iter().position(|s| *s == sign)
81}
82
83pub(crate) fn traditional_ruler(sign: &str) -> Option<&'static str> {
85 Some(match sign {
86 "Aries" | "Scorpio" => "Mars",
87 "Taurus" | "Libra" => "Venus",
88 "Gemini" | "Virgo" => "Mercury",
89 "Cancer" => "Moon",
90 "Leo" => "Sun",
91 "Sagittarius" | "Pisces" => "Jupiter",
92 "Capricorn" | "Aquarius" => "Saturn",
93 _ => return None,
94 })
95}
96
97fn exaltation(sign: &str) -> Option<(&'static str, i64)> {
99 Some(match sign {
100 "Aries" => ("Sun", 19),
101 "Taurus" => ("Moon", 3),
102 "Cancer" => ("Jupiter", 15),
103 "Virgo" => ("Mercury", 15),
104 "Libra" => ("Saturn", 21),
105 "Capricorn" => ("Mars", 28),
106 "Pisces" => ("Venus", 27),
107 _ => return None,
108 })
109}
110
111pub(crate) fn is_dignified(planet: &str, sign: &str) -> bool {
113 traditional_ruler(sign) == Some(planet) || exaltation(sign).map(|(p, _)| p) == Some(planet)
114}
115
116pub(crate) fn is_debilitated(planet: &str, sign: &str) -> bool {
118 sign_index(sign).is_some_and(|i| is_dignified(planet, SIGNS[(i + 6) % 12]))
119}
120
121pub(crate) fn element(sign: &str) -> Option<&'static str> {
122 Some(match sign {
123 "Aries" | "Leo" | "Sagittarius" => "Fire",
124 "Taurus" | "Virgo" | "Capricorn" => "Earth",
125 "Gemini" | "Libra" | "Aquarius" => "Air",
126 "Cancer" | "Scorpio" | "Pisces" => "Water",
127 _ => return None,
128 })
129}
130
131fn dorothean_triplicity(element: &str) -> Option<[&'static str; 3]> {
133 Some(match element {
134 "Fire" => ["Sun", "Jupiter", "Saturn"],
135 "Earth" => ["Venus", "Moon", "Mars"],
136 "Air" => ["Saturn", "Mercury", "Jupiter"],
137 "Water" => ["Venus", "Mars", "Moon"],
138 _ => return None,
139 })
140}
141
142pub(crate) fn triplicity_ruler(element: &str, is_day: bool) -> Option<&'static str> {
145 dorothean_triplicity(element).map(|[day, night, _]| if is_day { day } else { night })
146}
147
148fn lilly_triplicity(element: &str) -> Option<[&'static str; 2]> {
150 Some(match element {
151 "Fire" => ["Sun", "Jupiter"],
152 "Earth" => ["Venus", "Moon"],
153 "Air" => ["Saturn", "Mercury"],
154 "Water" => ["Mars", "Mars"],
155 _ => return None,
156 })
157}
158
159type Bounds = [(&'static str, f64); 5];
161
162const EGYPTIAN_BOUNDS: [Bounds; 12] = [
164 [
165 ("Jupiter", 6.0),
166 ("Venus", 12.0),
167 ("Mercury", 20.0),
168 ("Mars", 25.0),
169 ("Saturn", 30.0),
170 ],
171 [
172 ("Venus", 8.0),
173 ("Mercury", 14.0),
174 ("Jupiter", 22.0),
175 ("Saturn", 27.0),
176 ("Mars", 30.0),
177 ],
178 [
179 ("Mercury", 6.0),
180 ("Jupiter", 12.0),
181 ("Venus", 17.0),
182 ("Mars", 24.0),
183 ("Saturn", 30.0),
184 ],
185 [
186 ("Mars", 7.0),
187 ("Venus", 13.0),
188 ("Mercury", 19.0),
189 ("Jupiter", 26.0),
190 ("Saturn", 30.0),
191 ],
192 [
193 ("Jupiter", 6.0),
194 ("Venus", 11.0),
195 ("Saturn", 18.0),
196 ("Mercury", 24.0),
197 ("Mars", 30.0),
198 ],
199 [
200 ("Mercury", 7.0),
201 ("Venus", 17.0),
202 ("Jupiter", 21.0),
203 ("Mars", 28.0),
204 ("Saturn", 30.0),
205 ],
206 [
207 ("Saturn", 6.0),
208 ("Mercury", 14.0),
209 ("Jupiter", 21.0),
210 ("Venus", 28.0),
211 ("Mars", 30.0),
212 ],
213 [
214 ("Mars", 7.0),
215 ("Venus", 11.0),
216 ("Mercury", 19.0),
217 ("Jupiter", 24.0),
218 ("Saturn", 30.0),
219 ],
220 [
221 ("Jupiter", 12.0),
222 ("Venus", 17.0),
223 ("Mercury", 21.0),
224 ("Saturn", 26.0),
225 ("Mars", 30.0),
226 ],
227 [
228 ("Mercury", 7.0),
229 ("Jupiter", 14.0),
230 ("Venus", 22.0),
231 ("Saturn", 26.0),
232 ("Mars", 30.0),
233 ],
234 [
235 ("Mercury", 7.0),
236 ("Venus", 13.0),
237 ("Jupiter", 20.0),
238 ("Mars", 25.0),
239 ("Saturn", 30.0),
240 ],
241 [
242 ("Venus", 12.0),
243 ("Jupiter", 16.0),
244 ("Mercury", 19.0),
245 ("Mars", 28.0),
246 ("Saturn", 30.0),
247 ],
248];
249
250const LILLY_TERMS: [Bounds; 12] = [
252 [
253 ("Jupiter", 6.0),
254 ("Venus", 14.0),
255 ("Mercury", 21.0),
256 ("Mars", 26.0),
257 ("Saturn", 30.0),
258 ],
259 [
260 ("Venus", 8.0),
261 ("Mercury", 15.0),
262 ("Jupiter", 22.0),
263 ("Saturn", 26.0),
264 ("Mars", 30.0),
265 ],
266 [
267 ("Mercury", 7.0),
268 ("Jupiter", 14.0),
269 ("Venus", 21.0),
270 ("Saturn", 25.0),
271 ("Mars", 30.0),
272 ],
273 [
274 ("Mars", 6.0),
275 ("Jupiter", 13.0),
276 ("Mercury", 20.0),
277 ("Venus", 27.0),
278 ("Saturn", 30.0),
279 ],
280 [
281 ("Saturn", 6.0),
282 ("Mercury", 13.0),
283 ("Venus", 19.0),
284 ("Jupiter", 25.0),
285 ("Mars", 30.0),
286 ],
287 [
288 ("Mercury", 7.0),
289 ("Venus", 13.0),
290 ("Jupiter", 18.0),
291 ("Saturn", 24.0),
292 ("Mars", 30.0),
293 ],
294 [
295 ("Saturn", 6.0),
296 ("Venus", 11.0),
297 ("Jupiter", 19.0),
298 ("Mercury", 24.0),
299 ("Mars", 30.0),
300 ],
301 [
302 ("Mars", 6.0),
303 ("Jupiter", 14.0),
304 ("Venus", 21.0),
305 ("Mercury", 27.0),
306 ("Saturn", 30.0),
307 ],
308 [
309 ("Jupiter", 8.0),
310 ("Venus", 14.0),
311 ("Mercury", 19.0),
312 ("Saturn", 25.0),
313 ("Mars", 30.0),
314 ],
315 [
316 ("Venus", 6.0),
317 ("Mercury", 12.0),
318 ("Jupiter", 19.0),
319 ("Mars", 25.0),
320 ("Saturn", 30.0),
321 ],
322 [
323 ("Saturn", 6.0),
324 ("Mercury", 12.0),
325 ("Venus", 20.0),
326 ("Jupiter", 25.0),
327 ("Mars", 30.0),
328 ],
329 [
330 ("Venus", 8.0),
331 ("Jupiter", 14.0),
332 ("Mercury", 20.0),
333 ("Mars", 25.0),
334 ("Saturn", 30.0),
335 ],
336];
337
338fn sign_and_degree(longitude: f64) -> (usize, f64) {
340 let lon = pyfloat::rem(longitude, 360.0);
341 let index = (pyfloat::floordiv(lon, 30.0) as usize).min(11);
342 (index, lon - index as f64 * 30.0)
343}
344
345fn bound_lord(longitude: f64, scheme: DignityScheme) -> &'static str {
347 let (sign, degree) = sign_and_degree(longitude);
348 let table = match scheme {
349 DignityScheme::Lilly => &LILLY_TERMS,
350 DignityScheme::Dorothean => &EGYPTIAN_BOUNDS,
351 };
352 table[sign]
353 .iter()
354 .find(|(_, end)| degree < *end)
355 .map_or(table[sign][4].0, |(lord, _)| lord)
356}
357
358fn face_lord(longitude: f64) -> &'static str {
360 let (sign, degree) = sign_and_degree(longitude);
361 let decan = sign * 3 + ((degree / 10.0) as usize).min(2);
362 SEVEN[(decan + 2) % 7]
364}
365
366#[derive(Debug, Clone, PartialEq, Serialize)]
368pub struct DignityLords {
369 pub domicile: &'static str,
371 pub exaltation: Option<&'static str>,
373 pub triplicity: Vec<&'static str>,
376 pub bound: &'static str,
378 pub face: &'static str,
380}
381
382fn lords_at(longitude: f64, scheme: DignityScheme) -> DignityLords {
384 let (sign, _) = sign_and_degree(longitude);
385 let sign = SIGNS[sign];
386 let el = element(sign).unwrap_or("Fire");
387 DignityLords {
388 domicile: traditional_ruler(sign).unwrap_or("Mars"),
389 exaltation: exaltation(sign).map(|(planet, _)| planet),
390 triplicity: match scheme {
391 DignityScheme::Lilly => lilly_triplicity(el).map(|t| t.to_vec()),
392 DignityScheme::Dorothean => dorothean_triplicity(el).map(|t| t.to_vec()),
393 }
394 .unwrap_or_default(),
395 bound: bound_lord(longitude, scheme),
396 face: face_lord(longitude),
397 }
398}
399
400#[derive(Debug, Clone, PartialEq, Serialize)]
402pub struct EssentialDignity {
403 pub domicile: bool,
405 pub exaltation: bool,
407 pub triplicity: bool,
410 pub bound: bool,
412 pub face: bool,
414 pub detriment: bool,
416 pub fall: bool,
418 pub peregrine: bool,
420 pub score: i64,
423 pub lords: DignityLords,
425}
426
427#[derive(Debug, Clone, PartialEq, Serialize)]
429pub struct Condition {
430 pub essential: EssentialDignity,
432 pub sect: &'static str,
435 pub in_sect: bool,
437 pub above_horizon: bool,
439 pub solar_phase: Option<&'static str>,
442 pub orientality: Option<&'static str>,
444 pub motion: &'static str,
446}
447
448fn mean_motion(planet: &str) -> f64 {
450 match planet {
451 "Saturn" => 2.0 / 60.0 + 1.0 / 3600.0,
452 "Jupiter" => 4.0 / 60.0 + 59.0 / 3600.0,
453 "Mars" => 31.0 / 60.0 + 27.0 / 3600.0,
454 "Moon" => 13.0 + 10.0 / 60.0 + 36.0 / 3600.0,
455 _ => 59.0 / 60.0 + 8.0 / 3600.0,
457 }
458}
459
460fn is_above_horizon(longitude: f64, ascendant: f64) -> bool {
462 pyfloat::rem(longitude - ascendant, 360.0) >= 180.0
463}
464
465pub(crate) fn solar_phase(planet: &str, longitude: f64, sun: f64) -> Option<&'static str> {
467 if planet == "Sun" {
468 return None;
469 }
470 let d = pyfloat::separation(longitude, sun);
471 if d <= CAZIMI_ORB {
472 Some("cazimi")
473 } else if d < COMBUST_ORB {
474 Some("combust")
475 } else if d < UNDER_BEAMS_ORB {
476 Some("under_beams")
477 } else {
478 None
479 }
480}
481
482fn is_oriental(longitude: f64, sun: f64) -> bool {
484 let behind = pyfloat::rem(sun - longitude, 360.0);
485 behind > 0.0 && behind < 180.0
486}
487
488pub(crate) fn is_diurnal(sun: f64, ascendant: f64) -> bool {
490 is_above_horizon(sun, ascendant)
491}
492
493pub(crate) fn essential_dignity(
495 planet: &str,
496 longitude: f64,
497 diurnal: bool,
498 scheme: DignityScheme,
499) -> EssentialDignity {
500 let lords = lords_at(longitude, scheme);
501 let (sign, _) = sign_and_degree(longitude);
502 let opposite = SIGNS[(sign + 6) % 12];
503
504 let domicile = lords.domicile == planet;
505 let exalted = lords.exaltation == Some(planet);
506 let triplicity = match (scheme, lords.triplicity.as_slice()) {
507 (DignityScheme::Lilly, [day, night]) => planet == if diurnal { *day } else { *night },
508 (DignityScheme::Dorothean, [day, night, participating]) => {
509 planet == if diurnal { *day } else { *night } || planet == *participating
510 }
511 _ => false,
512 };
513 let bound = lords.bound == planet;
514 let face = lords.face == planet;
515 let detriment = traditional_ruler(opposite) == Some(planet);
516 let fall = exaltation(opposite).map(|(p, _)| p) == Some(planet);
517 let peregrine = !(domicile || exalted || triplicity || bound || face);
518
519 let score = 5 * domicile as i64
520 + 4 * exalted as i64
521 + 3 * triplicity as i64
522 + 2 * bound as i64
523 + face as i64
524 - 5 * detriment as i64
525 - 4 * fall as i64
526 - 5 * peregrine as i64;
527
528 EssentialDignity {
529 domicile,
530 exaltation: exalted,
531 triplicity,
532 bound,
533 face,
534 detriment,
535 fall,
536 peregrine,
537 score,
538 lords,
539 }
540}
541
542#[derive(Debug, Clone, Copy)]
544pub(crate) struct Body {
545 pub name: &'static str,
546 pub longitude: f64,
547 pub speed: f64,
548}
549
550impl From<&Placement> for Body {
551 fn from(p: &Placement) -> Self {
552 Body {
553 name: p.name,
554 longitude: p.ecliptic_longitude,
555 speed: p.speed,
556 }
557 }
558}
559
560pub(crate) fn condition(
562 planet: Body,
563 sun: f64,
564 ascendant: f64,
565 scheme: DignityScheme,
566) -> Option<Condition> {
567 if !SEVEN.contains(&planet.name) {
568 return None;
569 }
570 let lon = planet.longitude;
571 let diurnal = is_diurnal(sun, ascendant);
572 let orientality = match planet.name {
573 "Sun" | "Moon" => None,
574 _ if is_oriental(lon, sun) => Some("oriental"),
575 _ => Some("occidental"),
576 };
577 let sect = match planet.name {
578 "Sun" | "Jupiter" | "Saturn" => "diurnal",
579 "Mercury" if orientality == Some("oriental") => "diurnal",
580 _ => "nocturnal",
581 };
582 let mean = mean_motion(planet.name);
583 let speed = planet.speed.abs();
584 let motion = if !matches!(planet.name, "Sun" | "Moon") && speed < mean * STATIONARY_FRACTION {
585 "stationary"
586 } else if speed >= mean {
587 "fast"
588 } else {
589 "slow"
590 };
591
592 Some(Condition {
593 essential: essential_dignity(planet.name, lon, diurnal, scheme),
594 sect,
595 in_sect: (sect == "diurnal") == diurnal,
596 above_horizon: is_above_horizon(lon, ascendant),
597 solar_phase: solar_phase(planet.name, lon, sun),
598 orientality,
599 motion,
600 })
601}
602
603#[derive(Debug, Clone, PartialEq, Serialize)]
605pub struct Reception {
606 pub planet: &'static str,
608 pub receiver: &'static str,
610 pub dignities: Vec<&'static str>,
613 pub mutual: bool,
616 pub aspect: Option<&'static str>,
618}
619
620fn dignities_of(receiver: &str, longitude: f64, scheme: DignityScheme) -> Vec<&'static str> {
622 let lords = lords_at(longitude, scheme);
623 let mut out = Vec::new();
624 if lords.domicile == receiver {
625 out.push("domicile");
626 }
627 if lords.exaltation == Some(receiver) {
628 out.push("exaltation");
629 }
630 if lords.triplicity.contains(&receiver) {
631 out.push("triplicity");
632 }
633 if lords.bound == receiver {
634 out.push("bound");
635 }
636 if lords.face == receiver {
637 out.push("face");
638 }
639 out
640}
641
642fn is_strong(dignities: &[&str]) -> bool {
643 dignities
644 .iter()
645 .any(|d| matches!(*d, "domicile" | "exaltation"))
646}
647
648pub(crate) fn receptions(
651 planets: &[Body],
652 aspect_between: impl Fn(&str, &str) -> Option<&'static str>,
653 scheme: DignityScheme,
654) -> Vec<Reception> {
655 let seven: Vec<&Body> = SEVEN
656 .iter()
657 .filter_map(|name| planets.iter().find(|p| p.name == *name))
658 .collect();
659 let mut out = Vec::new();
660 for planet in &seven {
661 for receiver in &seven {
662 if planet.name == receiver.name {
663 continue;
664 }
665 let held = dignities_of(receiver.name, planet.longitude, scheme);
666 if held.is_empty() {
667 continue;
668 }
669 let back = dignities_of(planet.name, receiver.longitude, scheme);
670 let mutual = is_strong(&held) && is_strong(&back);
671 let aspect = aspect_between(planet.name, receiver.name);
672 if aspect.is_some() || mutual {
673 out.push(Reception {
674 planet: planet.name,
675 receiver: receiver.name,
676 dignities: held,
677 mutual,
678 aspect,
679 });
680 }
681 }
682 }
683 out
684}
685
686#[derive(Debug, Clone, PartialEq, Serialize)]
688pub struct PlanetCondition {
689 pub name: &'static str,
691 pub condition: Condition,
693}
694
695#[derive(Debug, Clone, PartialEq, Serialize)]
697pub struct ChartDignities {
698 pub dignity_scheme: &'static str,
700 pub sect: &'static str,
702 pub conditions: Vec<PlanetCondition>,
704 pub receptions: Vec<Reception>,
706}
707
708fn static_name(name: &str) -> Option<&'static str> {
710 SEVEN
711 .iter()
712 .copied()
713 .chain(["Ascendant"])
714 .find(|n| *n == name)
715}
716
717fn static_aspect(name: &str) -> Option<&'static str> {
719 ASPECT_RULES.iter().map(|r| r.name).find(|n| *n == name)
720}
721
722pub fn chart_dignities(chart: &Value, scheme: &str) -> Result<ChartDignities, EngineError> {
727 let scheme = DignityScheme::from_code(scheme);
728 let invalid = |what: &str| EngineError::InvalidInput(format!("chart_dignities: {what}"));
729 let planets = chart
730 .get("planets")
731 .and_then(Value::as_array)
732 .ok_or_else(|| invalid("the chart has no planets"))?;
733 let bodies: Vec<Body> = planets
734 .iter()
735 .filter_map(|p| {
736 Some(Body {
737 name: static_name(p.get("name")?.as_str()?)?,
738 longitude: p.get("ecliptic_longitude")?.as_f64()?,
739 speed: p.get("speed").and_then(Value::as_f64).unwrap_or(0.0),
740 })
741 })
742 .collect();
743 let at = |name: &str| bodies.iter().find(|b| b.name == name).map(|b| b.longitude);
744 let (sun, asc) = at("Sun")
745 .zip(at("Ascendant"))
746 .ok_or_else(|| invalid("the chart needs the Sun and the Ascendant"))?;
747
748 let aspects: Vec<(String, String, &'static str)> = chart
749 .get("aspects")
750 .and_then(Value::as_array)
751 .map(|list| {
752 list.iter()
753 .filter_map(|a| {
754 Some((
755 a.get("body1")?.as_str()?.to_string(),
756 a.get("body2")?.as_str()?.to_string(),
757 static_aspect(a.get("aspect_type")?.as_str()?)?,
758 ))
759 })
760 .collect()
761 })
762 .unwrap_or_default();
763 let aspect_between = |a: &str, b: &str| {
764 aspects
765 .iter()
766 .find(|(x, y, _)| (x == a && y == b) || (x == b && y == a))
767 .map(|(_, _, kind)| *kind)
768 };
769
770 Ok(ChartDignities {
771 dignity_scheme: scheme.code(),
772 sect: if is_diurnal(sun, asc) {
773 "diurnal"
774 } else {
775 "nocturnal"
776 },
777 conditions: bodies
778 .iter()
779 .filter_map(|b| {
780 condition(*b, sun, asc, scheme).map(|condition| PlanetCondition {
781 name: b.name,
782 condition,
783 })
784 })
785 .collect(),
786 receptions: receptions(&bodies, aspect_between, scheme),
787 })
788}
789
790#[derive(Debug, Clone, PartialEq, Serialize)]
793pub struct AntisciaContact {
794 pub body1: &'static str,
796 pub body2: &'static str,
798 pub kind: &'static str,
800 pub orb: f64,
802}
803
804pub(crate) fn antiscion(longitude: f64) -> f64 {
806 pyfloat::rem(180.0 - longitude, 360.0)
807}
808
809pub(crate) fn contra_antiscion(longitude: f64) -> f64 {
811 pyfloat::rem(360.0 - longitude, 360.0)
812}
813
814pub(crate) fn antiscia(planets: &[Placement]) -> Vec<AntisciaContact> {
816 let points: Vec<&Placement> = planets
817 .iter()
818 .filter(|p| SEVEN.contains(&p.name) || matches!(p.name, "Ascendant" | "Midheaven"))
819 .collect();
820 let mut out = Vec::new();
821 for (i, a) in points.iter().enumerate() {
822 for b in &points[i + 1..] {
823 for (kind, mirror) in [
824 ("antiscion", antiscion(a.ecliptic_longitude)),
825 ("contra_antiscion", contra_antiscion(a.ecliptic_longitude)),
826 ] {
827 let orb = pyfloat::separation(mirror, b.ecliptic_longitude);
828 if orb <= ANTISCIA_ORB {
829 out.push(AntisciaContact {
830 body1: a.name,
831 body2: b.name,
832 kind,
833 orb: pyfloat::round(orb, 2),
834 });
835 }
836 }
837 }
838 }
839 out
840}
841
842#[cfg(test)]
843mod tests {
844 use super::*;
845
846 fn lon(sign: &str, degree: f64) -> f64 {
847 sign_index(sign).unwrap() as f64 * 30.0 + degree
848 }
849
850 #[test]
851 fn bound_tables_partition_every_sign() {
852 for table in [&EGYPTIAN_BOUNDS, &LILLY_TERMS] {
853 for bounds in table.iter() {
854 let mut lords: Vec<&str> = bounds.iter().map(|(l, _)| *l).collect();
855 lords.sort_unstable();
856 assert_eq!(lords, ["Jupiter", "Mars", "Mercury", "Saturn", "Venus"]);
857 assert!(bounds.windows(2).all(|w| w[0].1 < w[1].1));
858 assert_eq!(bounds[4].1, 30.0);
859 }
860 }
861 }
862
863 #[test]
864 fn egyptian_bounds_give_each_planet_its_years() {
865 let mut total = std::collections::HashMap::new();
868 for bounds in EGYPTIAN_BOUNDS {
869 let mut start = 0.0;
870 for (lord, end) in bounds {
871 *total.entry(lord).or_insert(0.0) += end - start;
872 start = end;
873 }
874 }
875 assert_eq!(total["Saturn"], 57.0);
876 assert_eq!(total["Jupiter"], 79.0);
877 assert_eq!(total["Mars"], 66.0);
878 assert_eq!(total["Venus"], 82.0);
879 assert_eq!(total["Mercury"], 76.0);
880 }
881
882 #[test]
883 fn faces_follow_the_chaldean_order_from_mars() {
884 assert_eq!(face_lord(lon("Aries", 0.0)), "Mars");
885 assert_eq!(face_lord(lon("Aries", 15.0)), "Sun");
886 assert_eq!(face_lord(lon("Aries", 29.9)), "Venus");
887 assert_eq!(face_lord(lon("Taurus", 5.0)), "Mercury");
888 assert_eq!(face_lord(lon("Leo", 0.0)), "Saturn");
889 assert_eq!(face_lord(lon("Pisces", 25.0)), "Mars");
890 }
891
892 #[test]
893 fn bounds_by_scheme() {
894 assert_eq!(
897 bound_lord(lon("Aries", 10.0), DignityScheme::Lilly),
898 "Venus"
899 );
900 assert_eq!(
901 bound_lord(lon("Aries", 13.0), DignityScheme::Lilly),
902 "Venus"
903 );
904 assert_eq!(
905 bound_lord(lon("Aries", 13.0), DignityScheme::Dorothean),
906 "Mercury"
907 );
908 assert_eq!(
909 bound_lord(lon("Pisces", 29.99), DignityScheme::Lilly),
910 "Saturn"
911 );
912 assert_eq!(bound_lord(lon("Leo", 0.0), DignityScheme::Lilly), "Saturn");
913 assert_eq!(
914 bound_lord(lon("Leo", 0.0), DignityScheme::Dorothean),
915 "Jupiter"
916 );
917 }
918
919 #[test]
920 fn lillys_points() {
921 let sun = essential_dignity("Sun", lon("Aries", 19.0), true, DignityScheme::Lilly);
923 assert!(sun.exaltation && sun.triplicity && sun.face && !sun.domicile);
924 assert_eq!(sun.score, 8);
925 let saturn = essential_dignity("Saturn", lon("Aries", 5.0), true, DignityScheme::Lilly);
927 assert!(saturn.fall && saturn.peregrine && !saturn.detriment);
928 assert_eq!(saturn.score, -9);
929 let mars = essential_dignity("Mars", lon("Pisces", 2.0), true, DignityScheme::Lilly);
931 assert!(mars.triplicity);
932 let mars = essential_dignity("Mars", lon("Pisces", 2.0), true, DignityScheme::Dorothean);
933 assert!(!mars.triplicity);
934 let venus = essential_dignity("Venus", lon("Libra", 8.0), false, DignityScheme::Lilly);
936 assert!(venus.domicile && venus.bound);
937 assert_eq!(venus.lords.triplicity, ["Saturn", "Mercury"]);
938 let moon = essential_dignity(
940 "Moon",
941 lon("Capricorn", 1.0),
942 false,
943 DignityScheme::Dorothean,
944 );
945 assert!(moon.detriment && moon.triplicity && !moon.peregrine);
946 let moon = essential_dignity(
947 "Moon",
948 lon("Capricorn", 1.0),
949 true,
950 DignityScheme::Dorothean,
951 );
952 assert!(!moon.triplicity);
953 assert_eq!(moon.lords.triplicity, ["Venus", "Moon", "Mars"]);
954 }
955
956 #[test]
957 fn solar_phases() {
958 assert_eq!(solar_phase("Mercury", 100.1, 100.0), Some("cazimi"));
959 assert_eq!(solar_phase("Mercury", 105.0, 100.0), Some("combust"));
960 assert_eq!(solar_phase("Venus", 88.0, 100.0), Some("under_beams"));
961 assert_eq!(solar_phase("Mars", 130.0, 100.0), None);
962 assert_eq!(solar_phase("Sun", 100.0, 100.0), None);
963 assert_eq!(solar_phase("Moon", 355.0, 3.0), Some("combust"));
964 }
965
966 #[test]
967 fn mirrors() {
968 assert!((antiscion(lon("Gemini", 10.0)) - lon("Cancer", 20.0)).abs() < 1e-9);
970 assert!((contra_antiscion(lon("Gemini", 10.0)) - lon("Capricorn", 20.0)).abs() < 1e-9);
971 assert!((antiscion(lon("Capricorn", 5.0)) - lon("Sagittarius", 25.0)).abs() < 1e-9);
972 }
973
974 #[test]
975 fn sect_and_orientality() {
976 assert!(is_diurnal(0.0, 90.0));
978 assert!(!is_diurnal(0.0, 270.0));
980 assert!(is_oriental(350.0, 0.0));
981 assert!(!is_oriental(10.0, 0.0));
982 }
983}