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
460pub(crate) fn separation(a: f64, b: f64) -> f64 {
462 let d = pyfloat::rem((a - b).abs(), 360.0);
463 if d > 180.0 {
464 360.0 - d
465 } else {
466 d
467 }
468}
469
470fn is_above_horizon(longitude: f64, ascendant: f64) -> bool {
472 pyfloat::rem(longitude - ascendant, 360.0) >= 180.0
473}
474
475pub(crate) fn solar_phase(planet: &str, longitude: f64, sun: f64) -> Option<&'static str> {
477 if planet == "Sun" {
478 return None;
479 }
480 let d = separation(longitude, sun);
481 if d <= CAZIMI_ORB {
482 Some("cazimi")
483 } else if d < COMBUST_ORB {
484 Some("combust")
485 } else if d < UNDER_BEAMS_ORB {
486 Some("under_beams")
487 } else {
488 None
489 }
490}
491
492fn is_oriental(longitude: f64, sun: f64) -> bool {
494 let behind = pyfloat::rem(sun - longitude, 360.0);
495 behind > 0.0 && behind < 180.0
496}
497
498pub(crate) fn is_diurnal(sun: f64, ascendant: f64) -> bool {
500 is_above_horizon(sun, ascendant)
501}
502
503pub(crate) fn essential_dignity(
505 planet: &str,
506 longitude: f64,
507 diurnal: bool,
508 scheme: DignityScheme,
509) -> EssentialDignity {
510 let lords = lords_at(longitude, scheme);
511 let (sign, _) = sign_and_degree(longitude);
512 let opposite = SIGNS[(sign + 6) % 12];
513
514 let domicile = lords.domicile == planet;
515 let exalted = lords.exaltation == Some(planet);
516 let triplicity = match (scheme, lords.triplicity.as_slice()) {
517 (DignityScheme::Lilly, [day, night]) => planet == if diurnal { *day } else { *night },
518 (DignityScheme::Dorothean, [day, night, participating]) => {
519 planet == if diurnal { *day } else { *night } || planet == *participating
520 }
521 _ => false,
522 };
523 let bound = lords.bound == planet;
524 let face = lords.face == planet;
525 let detriment = traditional_ruler(opposite) == Some(planet);
526 let fall = exaltation(opposite).map(|(p, _)| p) == Some(planet);
527 let peregrine = !(domicile || exalted || triplicity || bound || face);
528
529 let score = 5 * domicile as i64
530 + 4 * exalted as i64
531 + 3 * triplicity as i64
532 + 2 * bound as i64
533 + face as i64
534 - 5 * detriment as i64
535 - 4 * fall as i64
536 - 5 * peregrine as i64;
537
538 EssentialDignity {
539 domicile,
540 exaltation: exalted,
541 triplicity,
542 bound,
543 face,
544 detriment,
545 fall,
546 peregrine,
547 score,
548 lords,
549 }
550}
551
552#[derive(Debug, Clone, Copy)]
554pub(crate) struct Body {
555 pub name: &'static str,
556 pub longitude: f64,
557 pub speed: f64,
558}
559
560impl From<&Placement> for Body {
561 fn from(p: &Placement) -> Self {
562 Body {
563 name: p.name,
564 longitude: p.ecliptic_longitude,
565 speed: p.speed,
566 }
567 }
568}
569
570pub(crate) fn condition(
572 planet: Body,
573 sun: f64,
574 ascendant: f64,
575 scheme: DignityScheme,
576) -> Option<Condition> {
577 if !SEVEN.contains(&planet.name) {
578 return None;
579 }
580 let lon = planet.longitude;
581 let diurnal = is_diurnal(sun, ascendant);
582 let orientality = match planet.name {
583 "Sun" | "Moon" => None,
584 _ if is_oriental(lon, sun) => Some("oriental"),
585 _ => Some("occidental"),
586 };
587 let sect = match planet.name {
588 "Sun" | "Jupiter" | "Saturn" => "diurnal",
589 "Mercury" if orientality == Some("oriental") => "diurnal",
590 _ => "nocturnal",
591 };
592 let mean = mean_motion(planet.name);
593 let speed = planet.speed.abs();
594 let motion = if !matches!(planet.name, "Sun" | "Moon") && speed < mean * STATIONARY_FRACTION {
595 "stationary"
596 } else if speed >= mean {
597 "fast"
598 } else {
599 "slow"
600 };
601
602 Some(Condition {
603 essential: essential_dignity(planet.name, lon, diurnal, scheme),
604 sect,
605 in_sect: (sect == "diurnal") == diurnal,
606 above_horizon: is_above_horizon(lon, ascendant),
607 solar_phase: solar_phase(planet.name, lon, sun),
608 orientality,
609 motion,
610 })
611}
612
613#[derive(Debug, Clone, PartialEq, Serialize)]
615pub struct Reception {
616 pub planet: &'static str,
618 pub receiver: &'static str,
620 pub dignities: Vec<&'static str>,
623 pub mutual: bool,
626 pub aspect: Option<&'static str>,
628}
629
630fn dignities_of(receiver: &str, longitude: f64, scheme: DignityScheme) -> Vec<&'static str> {
632 let lords = lords_at(longitude, scheme);
633 let mut out = Vec::new();
634 if lords.domicile == receiver {
635 out.push("domicile");
636 }
637 if lords.exaltation == Some(receiver) {
638 out.push("exaltation");
639 }
640 if lords.triplicity.contains(&receiver) {
641 out.push("triplicity");
642 }
643 if lords.bound == receiver {
644 out.push("bound");
645 }
646 if lords.face == receiver {
647 out.push("face");
648 }
649 out
650}
651
652fn is_strong(dignities: &[&str]) -> bool {
653 dignities
654 .iter()
655 .any(|d| matches!(*d, "domicile" | "exaltation"))
656}
657
658pub(crate) fn receptions(
661 planets: &[Body],
662 aspect_between: impl Fn(&str, &str) -> Option<&'static str>,
663 scheme: DignityScheme,
664) -> Vec<Reception> {
665 let seven: Vec<&Body> = SEVEN
666 .iter()
667 .filter_map(|name| planets.iter().find(|p| p.name == *name))
668 .collect();
669 let mut out = Vec::new();
670 for planet in &seven {
671 for receiver in &seven {
672 if planet.name == receiver.name {
673 continue;
674 }
675 let held = dignities_of(receiver.name, planet.longitude, scheme);
676 if held.is_empty() {
677 continue;
678 }
679 let back = dignities_of(planet.name, receiver.longitude, scheme);
680 let mutual = is_strong(&held) && is_strong(&back);
681 let aspect = aspect_between(planet.name, receiver.name);
682 if aspect.is_some() || mutual {
683 out.push(Reception {
684 planet: planet.name,
685 receiver: receiver.name,
686 dignities: held,
687 mutual,
688 aspect,
689 });
690 }
691 }
692 }
693 out
694}
695
696#[derive(Debug, Clone, PartialEq, Serialize)]
698pub struct PlanetCondition {
699 pub name: &'static str,
701 pub condition: Condition,
703}
704
705#[derive(Debug, Clone, PartialEq, Serialize)]
707pub struct ChartDignities {
708 pub dignity_scheme: &'static str,
710 pub sect: &'static str,
712 pub conditions: Vec<PlanetCondition>,
714 pub receptions: Vec<Reception>,
716}
717
718fn static_name(name: &str) -> Option<&'static str> {
720 SEVEN
721 .iter()
722 .copied()
723 .chain(["Ascendant"])
724 .find(|n| *n == name)
725}
726
727fn static_aspect(name: &str) -> Option<&'static str> {
729 ASPECT_RULES.iter().map(|r| r.name).find(|n| *n == name)
730}
731
732pub fn chart_dignities(chart: &Value, scheme: &str) -> Result<ChartDignities, EngineError> {
737 let scheme = DignityScheme::from_code(scheme);
738 let invalid = |what: &str| EngineError::InvalidInput(format!("chart_dignities: {what}"));
739 let planets = chart
740 .get("planets")
741 .and_then(Value::as_array)
742 .ok_or_else(|| invalid("the chart has no planets"))?;
743 let bodies: Vec<Body> = planets
744 .iter()
745 .filter_map(|p| {
746 Some(Body {
747 name: static_name(p.get("name")?.as_str()?)?,
748 longitude: p.get("ecliptic_longitude")?.as_f64()?,
749 speed: p.get("speed").and_then(Value::as_f64).unwrap_or(0.0),
750 })
751 })
752 .collect();
753 let at = |name: &str| bodies.iter().find(|b| b.name == name).map(|b| b.longitude);
754 let (sun, asc) = at("Sun")
755 .zip(at("Ascendant"))
756 .ok_or_else(|| invalid("the chart needs the Sun and the Ascendant"))?;
757
758 let aspects: Vec<(String, String, &'static str)> = chart
759 .get("aspects")
760 .and_then(Value::as_array)
761 .map(|list| {
762 list.iter()
763 .filter_map(|a| {
764 Some((
765 a.get("body1")?.as_str()?.to_string(),
766 a.get("body2")?.as_str()?.to_string(),
767 static_aspect(a.get("aspect_type")?.as_str()?)?,
768 ))
769 })
770 .collect()
771 })
772 .unwrap_or_default();
773 let aspect_between = |a: &str, b: &str| {
774 aspects
775 .iter()
776 .find(|(x, y, _)| (x == a && y == b) || (x == b && y == a))
777 .map(|(_, _, kind)| *kind)
778 };
779
780 Ok(ChartDignities {
781 dignity_scheme: scheme.code(),
782 sect: if is_diurnal(sun, asc) {
783 "diurnal"
784 } else {
785 "nocturnal"
786 },
787 conditions: bodies
788 .iter()
789 .filter_map(|b| {
790 condition(*b, sun, asc, scheme).map(|condition| PlanetCondition {
791 name: b.name,
792 condition,
793 })
794 })
795 .collect(),
796 receptions: receptions(&bodies, aspect_between, scheme),
797 })
798}
799
800#[derive(Debug, Clone, PartialEq, Serialize)]
803pub struct AntisciaContact {
804 pub body1: &'static str,
806 pub body2: &'static str,
808 pub kind: &'static str,
810 pub orb: f64,
812}
813
814pub(crate) fn antiscion(longitude: f64) -> f64 {
816 pyfloat::rem(180.0 - longitude, 360.0)
817}
818
819pub(crate) fn contra_antiscion(longitude: f64) -> f64 {
821 pyfloat::rem(360.0 - longitude, 360.0)
822}
823
824pub(crate) fn antiscia(planets: &[Placement]) -> Vec<AntisciaContact> {
826 let points: Vec<&Placement> = planets
827 .iter()
828 .filter(|p| SEVEN.contains(&p.name) || matches!(p.name, "Ascendant" | "Midheaven"))
829 .collect();
830 let mut out = Vec::new();
831 for (i, a) in points.iter().enumerate() {
832 for b in &points[i + 1..] {
833 for (kind, mirror) in [
834 ("antiscion", antiscion(a.ecliptic_longitude)),
835 ("contra_antiscion", contra_antiscion(a.ecliptic_longitude)),
836 ] {
837 let orb = separation(mirror, b.ecliptic_longitude);
838 if orb <= ANTISCIA_ORB {
839 out.push(AntisciaContact {
840 body1: a.name,
841 body2: b.name,
842 kind,
843 orb: pyfloat::round(orb, 2),
844 });
845 }
846 }
847 }
848 }
849 out
850}
851
852#[cfg(test)]
853mod tests {
854 use super::*;
855
856 fn lon(sign: &str, degree: f64) -> f64 {
857 sign_index(sign).unwrap() as f64 * 30.0 + degree
858 }
859
860 #[test]
861 fn bound_tables_partition_every_sign() {
862 for table in [&EGYPTIAN_BOUNDS, &LILLY_TERMS] {
863 for bounds in table.iter() {
864 let mut lords: Vec<&str> = bounds.iter().map(|(l, _)| *l).collect();
865 lords.sort_unstable();
866 assert_eq!(lords, ["Jupiter", "Mars", "Mercury", "Saturn", "Venus"]);
867 assert!(bounds.windows(2).all(|w| w[0].1 < w[1].1));
868 assert_eq!(bounds[4].1, 30.0);
869 }
870 }
871 }
872
873 #[test]
874 fn egyptian_bounds_give_each_planet_its_years() {
875 let mut total = std::collections::HashMap::new();
878 for bounds in EGYPTIAN_BOUNDS {
879 let mut start = 0.0;
880 for (lord, end) in bounds {
881 *total.entry(lord).or_insert(0.0) += end - start;
882 start = end;
883 }
884 }
885 assert_eq!(total["Saturn"], 57.0);
886 assert_eq!(total["Jupiter"], 79.0);
887 assert_eq!(total["Mars"], 66.0);
888 assert_eq!(total["Venus"], 82.0);
889 assert_eq!(total["Mercury"], 76.0);
890 }
891
892 #[test]
893 fn faces_follow_the_chaldean_order_from_mars() {
894 assert_eq!(face_lord(lon("Aries", 0.0)), "Mars");
895 assert_eq!(face_lord(lon("Aries", 15.0)), "Sun");
896 assert_eq!(face_lord(lon("Aries", 29.9)), "Venus");
897 assert_eq!(face_lord(lon("Taurus", 5.0)), "Mercury");
898 assert_eq!(face_lord(lon("Leo", 0.0)), "Saturn");
899 assert_eq!(face_lord(lon("Pisces", 25.0)), "Mars");
900 }
901
902 #[test]
903 fn bounds_by_scheme() {
904 assert_eq!(
907 bound_lord(lon("Aries", 10.0), DignityScheme::Lilly),
908 "Venus"
909 );
910 assert_eq!(
911 bound_lord(lon("Aries", 13.0), DignityScheme::Lilly),
912 "Venus"
913 );
914 assert_eq!(
915 bound_lord(lon("Aries", 13.0), DignityScheme::Dorothean),
916 "Mercury"
917 );
918 assert_eq!(
919 bound_lord(lon("Pisces", 29.99), DignityScheme::Lilly),
920 "Saturn"
921 );
922 assert_eq!(bound_lord(lon("Leo", 0.0), DignityScheme::Lilly), "Saturn");
923 assert_eq!(
924 bound_lord(lon("Leo", 0.0), DignityScheme::Dorothean),
925 "Jupiter"
926 );
927 }
928
929 #[test]
930 fn lillys_points() {
931 let sun = essential_dignity("Sun", lon("Aries", 19.0), true, DignityScheme::Lilly);
933 assert!(sun.exaltation && sun.triplicity && sun.face && !sun.domicile);
934 assert_eq!(sun.score, 8);
935 let saturn = essential_dignity("Saturn", lon("Aries", 5.0), true, DignityScheme::Lilly);
937 assert!(saturn.fall && saturn.peregrine && !saturn.detriment);
938 assert_eq!(saturn.score, -9);
939 let mars = essential_dignity("Mars", lon("Pisces", 2.0), true, DignityScheme::Lilly);
941 assert!(mars.triplicity);
942 let mars = essential_dignity("Mars", lon("Pisces", 2.0), true, DignityScheme::Dorothean);
943 assert!(!mars.triplicity);
944 let venus = essential_dignity("Venus", lon("Libra", 8.0), false, DignityScheme::Lilly);
946 assert!(venus.domicile && venus.bound);
947 assert_eq!(venus.lords.triplicity, ["Saturn", "Mercury"]);
948 let moon = essential_dignity(
950 "Moon",
951 lon("Capricorn", 1.0),
952 false,
953 DignityScheme::Dorothean,
954 );
955 assert!(moon.detriment && moon.triplicity && !moon.peregrine);
956 let moon = essential_dignity(
957 "Moon",
958 lon("Capricorn", 1.0),
959 true,
960 DignityScheme::Dorothean,
961 );
962 assert!(!moon.triplicity);
963 assert_eq!(moon.lords.triplicity, ["Venus", "Moon", "Mars"]);
964 }
965
966 #[test]
967 fn solar_phases() {
968 assert_eq!(solar_phase("Mercury", 100.1, 100.0), Some("cazimi"));
969 assert_eq!(solar_phase("Mercury", 105.0, 100.0), Some("combust"));
970 assert_eq!(solar_phase("Venus", 88.0, 100.0), Some("under_beams"));
971 assert_eq!(solar_phase("Mars", 130.0, 100.0), None);
972 assert_eq!(solar_phase("Sun", 100.0, 100.0), None);
973 assert_eq!(solar_phase("Moon", 355.0, 3.0), Some("combust"));
974 }
975
976 #[test]
977 fn mirrors() {
978 assert!((antiscion(lon("Gemini", 10.0)) - lon("Cancer", 20.0)).abs() < 1e-9);
980 assert!((contra_antiscion(lon("Gemini", 10.0)) - lon("Capricorn", 20.0)).abs() < 1e-9);
981 assert!((antiscion(lon("Capricorn", 5.0)) - lon("Sagittarius", 25.0)).abs() < 1e-9);
982 }
983
984 #[test]
985 fn sect_and_orientality() {
986 assert!(is_diurnal(0.0, 90.0));
988 assert!(!is_diurnal(0.0, 270.0));
990 assert!(is_oriental(350.0, 0.0));
991 assert!(!is_oriental(10.0, 0.0));
992 }
993}