1use serde::Serialize;
10
11use crate::aspects::Aspect;
12use crate::chart::Placement;
13use crate::pyfloat;
14
15pub(crate) const SEVEN: [&str; 7] = [
17 "Saturn", "Jupiter", "Mars", "Sun", "Venus", "Mercury", "Moon",
18];
19
20pub(crate) const SIGNS: [&str; 12] = [
21 "Aries",
22 "Taurus",
23 "Gemini",
24 "Cancer",
25 "Leo",
26 "Virgo",
27 "Libra",
28 "Scorpio",
29 "Sagittarius",
30 "Capricorn",
31 "Aquarius",
32 "Pisces",
33];
34
35pub(crate) const CAZIMI_ORB: f64 = 17.0 / 60.0;
37pub(crate) const COMBUST_ORB: f64 = 8.5;
39const UNDER_BEAMS_ORB: f64 = 15.0;
41const ANTISCIA_ORB: f64 = 1.0;
43const STATIONARY_FRACTION: f64 = 0.1;
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
48pub enum DignityScheme {
49 #[default]
52 Lilly,
53 Dorothean,
56}
57
58impl DignityScheme {
59 pub fn from_code(code: &str) -> Self {
61 match code.trim().to_lowercase().as_str() {
62 "dorothean" | "dorotheus" | "egyptian" => DignityScheme::Dorothean,
63 _ => DignityScheme::Lilly,
64 }
65 }
66
67 pub fn code(self) -> &'static str {
69 match self {
70 DignityScheme::Lilly => "lilly",
71 DignityScheme::Dorothean => "dorothean",
72 }
73 }
74}
75
76pub(crate) fn sign_index(sign: &str) -> Option<usize> {
77 SIGNS.iter().position(|s| *s == sign)
78}
79
80pub(crate) fn traditional_ruler(sign: &str) -> Option<&'static str> {
82 Some(match sign {
83 "Aries" | "Scorpio" => "Mars",
84 "Taurus" | "Libra" => "Venus",
85 "Gemini" | "Virgo" => "Mercury",
86 "Cancer" => "Moon",
87 "Leo" => "Sun",
88 "Sagittarius" | "Pisces" => "Jupiter",
89 "Capricorn" | "Aquarius" => "Saturn",
90 _ => return None,
91 })
92}
93
94fn exaltation(sign: &str) -> Option<(&'static str, i64)> {
96 Some(match sign {
97 "Aries" => ("Sun", 19),
98 "Taurus" => ("Moon", 3),
99 "Cancer" => ("Jupiter", 15),
100 "Virgo" => ("Mercury", 15),
101 "Libra" => ("Saturn", 21),
102 "Capricorn" => ("Mars", 28),
103 "Pisces" => ("Venus", 27),
104 _ => return None,
105 })
106}
107
108pub(crate) fn is_dignified(planet: &str, sign: &str) -> bool {
110 traditional_ruler(sign) == Some(planet) || exaltation(sign).map(|(p, _)| p) == Some(planet)
111}
112
113pub(crate) fn is_debilitated(planet: &str, sign: &str) -> bool {
115 sign_index(sign).is_some_and(|i| is_dignified(planet, SIGNS[(i + 6) % 12]))
116}
117
118pub(crate) fn element(sign: &str) -> Option<&'static str> {
119 Some(match sign {
120 "Aries" | "Leo" | "Sagittarius" => "Fire",
121 "Taurus" | "Virgo" | "Capricorn" => "Earth",
122 "Gemini" | "Libra" | "Aquarius" => "Air",
123 "Cancer" | "Scorpio" | "Pisces" => "Water",
124 _ => return None,
125 })
126}
127
128fn dorothean_triplicity(element: &str) -> Option<[&'static str; 3]> {
130 Some(match element {
131 "Fire" => ["Sun", "Jupiter", "Saturn"],
132 "Earth" => ["Venus", "Moon", "Mars"],
133 "Air" => ["Saturn", "Mercury", "Jupiter"],
134 "Water" => ["Venus", "Mars", "Moon"],
135 _ => return None,
136 })
137}
138
139pub(crate) fn triplicity_ruler(element: &str, is_day: bool) -> Option<&'static str> {
142 dorothean_triplicity(element).map(|[day, night, _]| if is_day { day } else { night })
143}
144
145fn lilly_triplicity(element: &str) -> Option<[&'static str; 2]> {
147 Some(match element {
148 "Fire" => ["Sun", "Jupiter"],
149 "Earth" => ["Venus", "Moon"],
150 "Air" => ["Saturn", "Mercury"],
151 "Water" => ["Mars", "Mars"],
152 _ => return None,
153 })
154}
155
156type Bounds = [(&'static str, f64); 5];
158
159const EGYPTIAN_BOUNDS: [Bounds; 12] = [
161 [
162 ("Jupiter", 6.0),
163 ("Venus", 12.0),
164 ("Mercury", 20.0),
165 ("Mars", 25.0),
166 ("Saturn", 30.0),
167 ],
168 [
169 ("Venus", 8.0),
170 ("Mercury", 14.0),
171 ("Jupiter", 22.0),
172 ("Saturn", 27.0),
173 ("Mars", 30.0),
174 ],
175 [
176 ("Mercury", 6.0),
177 ("Jupiter", 12.0),
178 ("Venus", 17.0),
179 ("Mars", 24.0),
180 ("Saturn", 30.0),
181 ],
182 [
183 ("Mars", 7.0),
184 ("Venus", 13.0),
185 ("Mercury", 19.0),
186 ("Jupiter", 26.0),
187 ("Saturn", 30.0),
188 ],
189 [
190 ("Jupiter", 6.0),
191 ("Venus", 11.0),
192 ("Saturn", 18.0),
193 ("Mercury", 24.0),
194 ("Mars", 30.0),
195 ],
196 [
197 ("Mercury", 7.0),
198 ("Venus", 17.0),
199 ("Jupiter", 21.0),
200 ("Mars", 28.0),
201 ("Saturn", 30.0),
202 ],
203 [
204 ("Saturn", 6.0),
205 ("Mercury", 14.0),
206 ("Jupiter", 21.0),
207 ("Venus", 28.0),
208 ("Mars", 30.0),
209 ],
210 [
211 ("Mars", 7.0),
212 ("Venus", 11.0),
213 ("Mercury", 19.0),
214 ("Jupiter", 24.0),
215 ("Saturn", 30.0),
216 ],
217 [
218 ("Jupiter", 12.0),
219 ("Venus", 17.0),
220 ("Mercury", 21.0),
221 ("Saturn", 26.0),
222 ("Mars", 30.0),
223 ],
224 [
225 ("Mercury", 7.0),
226 ("Jupiter", 14.0),
227 ("Venus", 22.0),
228 ("Saturn", 26.0),
229 ("Mars", 30.0),
230 ],
231 [
232 ("Mercury", 7.0),
233 ("Venus", 13.0),
234 ("Jupiter", 20.0),
235 ("Mars", 25.0),
236 ("Saturn", 30.0),
237 ],
238 [
239 ("Venus", 12.0),
240 ("Jupiter", 16.0),
241 ("Mercury", 19.0),
242 ("Mars", 28.0),
243 ("Saturn", 30.0),
244 ],
245];
246
247const LILLY_TERMS: [Bounds; 12] = [
249 [
250 ("Jupiter", 6.0),
251 ("Venus", 14.0),
252 ("Mercury", 21.0),
253 ("Mars", 26.0),
254 ("Saturn", 30.0),
255 ],
256 [
257 ("Venus", 8.0),
258 ("Mercury", 15.0),
259 ("Jupiter", 22.0),
260 ("Saturn", 26.0),
261 ("Mars", 30.0),
262 ],
263 [
264 ("Mercury", 7.0),
265 ("Jupiter", 14.0),
266 ("Venus", 21.0),
267 ("Saturn", 25.0),
268 ("Mars", 30.0),
269 ],
270 [
271 ("Mars", 6.0),
272 ("Jupiter", 13.0),
273 ("Mercury", 20.0),
274 ("Venus", 27.0),
275 ("Saturn", 30.0),
276 ],
277 [
278 ("Saturn", 6.0),
279 ("Mercury", 13.0),
280 ("Venus", 19.0),
281 ("Jupiter", 25.0),
282 ("Mars", 30.0),
283 ],
284 [
285 ("Mercury", 7.0),
286 ("Venus", 13.0),
287 ("Jupiter", 18.0),
288 ("Saturn", 24.0),
289 ("Mars", 30.0),
290 ],
291 [
292 ("Saturn", 6.0),
293 ("Venus", 11.0),
294 ("Jupiter", 19.0),
295 ("Mercury", 24.0),
296 ("Mars", 30.0),
297 ],
298 [
299 ("Mars", 6.0),
300 ("Jupiter", 14.0),
301 ("Venus", 21.0),
302 ("Mercury", 27.0),
303 ("Saturn", 30.0),
304 ],
305 [
306 ("Jupiter", 8.0),
307 ("Venus", 14.0),
308 ("Mercury", 19.0),
309 ("Saturn", 25.0),
310 ("Mars", 30.0),
311 ],
312 [
313 ("Venus", 6.0),
314 ("Mercury", 12.0),
315 ("Jupiter", 19.0),
316 ("Mars", 25.0),
317 ("Saturn", 30.0),
318 ],
319 [
320 ("Saturn", 6.0),
321 ("Mercury", 12.0),
322 ("Venus", 20.0),
323 ("Jupiter", 25.0),
324 ("Mars", 30.0),
325 ],
326 [
327 ("Venus", 8.0),
328 ("Jupiter", 14.0),
329 ("Mercury", 20.0),
330 ("Mars", 25.0),
331 ("Saturn", 30.0),
332 ],
333];
334
335fn sign_and_degree(longitude: f64) -> (usize, f64) {
337 let lon = pyfloat::rem(longitude, 360.0);
338 let index = (pyfloat::floordiv(lon, 30.0) as usize).min(11);
339 (index, lon - index as f64 * 30.0)
340}
341
342fn bound_lord(longitude: f64, scheme: DignityScheme) -> &'static str {
344 let (sign, degree) = sign_and_degree(longitude);
345 let table = match scheme {
346 DignityScheme::Lilly => &LILLY_TERMS,
347 DignityScheme::Dorothean => &EGYPTIAN_BOUNDS,
348 };
349 table[sign]
350 .iter()
351 .find(|(_, end)| degree < *end)
352 .map_or(table[sign][4].0, |(lord, _)| lord)
353}
354
355fn face_lord(longitude: f64) -> &'static str {
357 let (sign, degree) = sign_and_degree(longitude);
358 let decan = sign * 3 + ((degree / 10.0) as usize).min(2);
359 SEVEN[(decan + 2) % 7]
361}
362
363#[derive(Debug, Clone, PartialEq, Serialize)]
365pub struct DignityLords {
366 pub domicile: &'static str,
368 pub exaltation: Option<&'static str>,
370 pub triplicity: Vec<&'static str>,
373 pub bound: &'static str,
375 pub face: &'static str,
377}
378
379fn lords_at(longitude: f64, scheme: DignityScheme) -> DignityLords {
381 let (sign, _) = sign_and_degree(longitude);
382 let sign = SIGNS[sign];
383 let el = element(sign).unwrap_or("Fire");
384 DignityLords {
385 domicile: traditional_ruler(sign).unwrap_or("Mars"),
386 exaltation: exaltation(sign).map(|(planet, _)| planet),
387 triplicity: match scheme {
388 DignityScheme::Lilly => lilly_triplicity(el).map(|t| t.to_vec()),
389 DignityScheme::Dorothean => dorothean_triplicity(el).map(|t| t.to_vec()),
390 }
391 .unwrap_or_default(),
392 bound: bound_lord(longitude, scheme),
393 face: face_lord(longitude),
394 }
395}
396
397#[derive(Debug, Clone, PartialEq, Serialize)]
399pub struct EssentialDignity {
400 pub domicile: bool,
402 pub exaltation: bool,
404 pub triplicity: bool,
407 pub bound: bool,
409 pub face: bool,
411 pub detriment: bool,
413 pub fall: bool,
415 pub peregrine: bool,
417 pub score: i64,
420 pub lords: DignityLords,
422}
423
424#[derive(Debug, Clone, PartialEq, Serialize)]
426pub struct Condition {
427 pub essential: EssentialDignity,
429 pub sect: &'static str,
432 pub in_sect: bool,
434 pub above_horizon: bool,
436 pub solar_phase: Option<&'static str>,
439 pub orientality: Option<&'static str>,
441 pub motion: &'static str,
443}
444
445fn mean_motion(planet: &str) -> f64 {
447 match planet {
448 "Saturn" => 2.0 / 60.0 + 1.0 / 3600.0,
449 "Jupiter" => 4.0 / 60.0 + 59.0 / 3600.0,
450 "Mars" => 31.0 / 60.0 + 27.0 / 3600.0,
451 "Moon" => 13.0 + 10.0 / 60.0 + 36.0 / 3600.0,
452 _ => 59.0 / 60.0 + 8.0 / 3600.0,
454 }
455}
456
457pub(crate) fn separation(a: f64, b: f64) -> f64 {
459 let d = pyfloat::rem((a - b).abs(), 360.0);
460 if d > 180.0 {
461 360.0 - d
462 } else {
463 d
464 }
465}
466
467fn is_above_horizon(longitude: f64, ascendant: f64) -> bool {
469 pyfloat::rem(longitude - ascendant, 360.0) >= 180.0
470}
471
472pub(crate) fn solar_phase(planet: &str, longitude: f64, sun: f64) -> Option<&'static str> {
474 if planet == "Sun" {
475 return None;
476 }
477 let d = separation(longitude, sun);
478 if d <= CAZIMI_ORB {
479 Some("cazimi")
480 } else if d < COMBUST_ORB {
481 Some("combust")
482 } else if d < UNDER_BEAMS_ORB {
483 Some("under_beams")
484 } else {
485 None
486 }
487}
488
489fn is_oriental(longitude: f64, sun: f64) -> bool {
491 let behind = pyfloat::rem(sun - longitude, 360.0);
492 behind > 0.0 && behind < 180.0
493}
494
495pub(crate) fn is_diurnal(sun: f64, ascendant: f64) -> bool {
497 is_above_horizon(sun, ascendant)
498}
499
500pub(crate) fn essential_dignity(
502 planet: &str,
503 longitude: f64,
504 diurnal: bool,
505 scheme: DignityScheme,
506) -> EssentialDignity {
507 let lords = lords_at(longitude, scheme);
508 let (sign, _) = sign_and_degree(longitude);
509 let opposite = SIGNS[(sign + 6) % 12];
510
511 let domicile = lords.domicile == planet;
512 let exalted = lords.exaltation == Some(planet);
513 let triplicity = match (scheme, lords.triplicity.as_slice()) {
514 (DignityScheme::Lilly, [day, night]) => planet == if diurnal { *day } else { *night },
515 (DignityScheme::Dorothean, [day, night, participating]) => {
516 planet == if diurnal { *day } else { *night } || planet == *participating
517 }
518 _ => false,
519 };
520 let bound = lords.bound == planet;
521 let face = lords.face == planet;
522 let detriment = traditional_ruler(opposite) == Some(planet);
523 let fall = exaltation(opposite).map(|(p, _)| p) == Some(planet);
524 let peregrine = !(domicile || exalted || triplicity || bound || face);
525
526 let score = 5 * domicile as i64
527 + 4 * exalted as i64
528 + 3 * triplicity as i64
529 + 2 * bound as i64
530 + face as i64
531 - 5 * detriment as i64
532 - 4 * fall as i64
533 - 5 * peregrine as i64;
534
535 EssentialDignity {
536 domicile,
537 exaltation: exalted,
538 triplicity,
539 bound,
540 face,
541 detriment,
542 fall,
543 peregrine,
544 score,
545 lords,
546 }
547}
548
549pub(crate) fn condition(
551 planet: &Placement,
552 sun: f64,
553 ascendant: f64,
554 scheme: DignityScheme,
555) -> Option<Condition> {
556 if !SEVEN.contains(&planet.name) {
557 return None;
558 }
559 let lon = planet.ecliptic_longitude;
560 let diurnal = is_diurnal(sun, ascendant);
561 let orientality = match planet.name {
562 "Sun" | "Moon" => None,
563 _ if is_oriental(lon, sun) => Some("oriental"),
564 _ => Some("occidental"),
565 };
566 let sect = match planet.name {
567 "Sun" | "Jupiter" | "Saturn" => "diurnal",
568 "Mercury" if orientality == Some("oriental") => "diurnal",
569 _ => "nocturnal",
570 };
571 let mean = mean_motion(planet.name);
572 let speed = planet.speed.abs();
573 let motion = if !matches!(planet.name, "Sun" | "Moon") && speed < mean * STATIONARY_FRACTION {
574 "stationary"
575 } else if speed >= mean {
576 "fast"
577 } else {
578 "slow"
579 };
580
581 Some(Condition {
582 essential: essential_dignity(planet.name, lon, diurnal, scheme),
583 sect,
584 in_sect: (sect == "diurnal") == diurnal,
585 above_horizon: is_above_horizon(lon, ascendant),
586 solar_phase: solar_phase(planet.name, lon, sun),
587 orientality,
588 motion,
589 })
590}
591
592#[derive(Debug, Clone, PartialEq, Serialize)]
594pub struct Reception {
595 pub planet: &'static str,
597 pub receiver: &'static str,
599 pub dignities: Vec<&'static str>,
602 pub mutual: bool,
605 pub aspect: Option<&'static str>,
607}
608
609fn dignities_of(receiver: &str, longitude: f64, scheme: DignityScheme) -> Vec<&'static str> {
611 let lords = lords_at(longitude, scheme);
612 let mut out = Vec::new();
613 if lords.domicile == receiver {
614 out.push("domicile");
615 }
616 if lords.exaltation == Some(receiver) {
617 out.push("exaltation");
618 }
619 if lords.triplicity.contains(&receiver) {
620 out.push("triplicity");
621 }
622 if lords.bound == receiver {
623 out.push("bound");
624 }
625 if lords.face == receiver {
626 out.push("face");
627 }
628 out
629}
630
631fn is_strong(dignities: &[&str]) -> bool {
632 dignities
633 .iter()
634 .any(|d| matches!(*d, "domicile" | "exaltation"))
635}
636
637pub(crate) fn receptions(
640 planets: &[Placement],
641 aspects: &[Aspect],
642 scheme: DignityScheme,
643) -> Vec<Reception> {
644 let seven: Vec<&Placement> = SEVEN
645 .iter()
646 .filter_map(|name| planets.iter().find(|p| p.name == *name))
647 .collect();
648 let aspect_between = |a: &str, b: &str| {
649 aspects
650 .iter()
651 .find(|x| (x.body1 == a && x.body2 == b) || (x.body1 == b && x.body2 == a))
652 .map(|x| x.aspect_type)
653 };
654 let mut out = Vec::new();
655 for planet in &seven {
656 for receiver in &seven {
657 if planet.name == receiver.name {
658 continue;
659 }
660 let held = dignities_of(receiver.name, planet.ecliptic_longitude, scheme);
661 if held.is_empty() {
662 continue;
663 }
664 let back = dignities_of(planet.name, receiver.ecliptic_longitude, scheme);
665 let mutual = is_strong(&held) && is_strong(&back);
666 let aspect = aspect_between(planet.name, receiver.name);
667 if aspect.is_some() || mutual {
668 out.push(Reception {
669 planet: planet.name,
670 receiver: receiver.name,
671 dignities: held,
672 mutual,
673 aspect,
674 });
675 }
676 }
677 }
678 out
679}
680
681#[derive(Debug, Clone, PartialEq, Serialize)]
684pub struct AntisciaContact {
685 pub body1: &'static str,
687 pub body2: &'static str,
689 pub kind: &'static str,
691 pub orb: f64,
693}
694
695pub(crate) fn antiscion(longitude: f64) -> f64 {
697 pyfloat::rem(180.0 - longitude, 360.0)
698}
699
700pub(crate) fn contra_antiscion(longitude: f64) -> f64 {
702 pyfloat::rem(360.0 - longitude, 360.0)
703}
704
705pub(crate) fn antiscia(planets: &[Placement]) -> Vec<AntisciaContact> {
707 let points: Vec<&Placement> = planets
708 .iter()
709 .filter(|p| SEVEN.contains(&p.name) || matches!(p.name, "Ascendant" | "Midheaven"))
710 .collect();
711 let mut out = Vec::new();
712 for (i, a) in points.iter().enumerate() {
713 for b in &points[i + 1..] {
714 for (kind, mirror) in [
715 ("antiscion", antiscion(a.ecliptic_longitude)),
716 ("contra_antiscion", contra_antiscion(a.ecliptic_longitude)),
717 ] {
718 let orb = separation(mirror, b.ecliptic_longitude);
719 if orb <= ANTISCIA_ORB {
720 out.push(AntisciaContact {
721 body1: a.name,
722 body2: b.name,
723 kind,
724 orb: pyfloat::round(orb, 2),
725 });
726 }
727 }
728 }
729 }
730 out
731}
732
733#[cfg(test)]
734mod tests {
735 use super::*;
736
737 fn lon(sign: &str, degree: f64) -> f64 {
738 sign_index(sign).unwrap() as f64 * 30.0 + degree
739 }
740
741 #[test]
742 fn bound_tables_partition_every_sign() {
743 for table in [&EGYPTIAN_BOUNDS, &LILLY_TERMS] {
744 for bounds in table.iter() {
745 let mut lords: Vec<&str> = bounds.iter().map(|(l, _)| *l).collect();
746 lords.sort_unstable();
747 assert_eq!(lords, ["Jupiter", "Mars", "Mercury", "Saturn", "Venus"]);
748 assert!(bounds.windows(2).all(|w| w[0].1 < w[1].1));
749 assert_eq!(bounds[4].1, 30.0);
750 }
751 }
752 }
753
754 #[test]
755 fn egyptian_bounds_give_each_planet_its_years() {
756 let mut total = std::collections::HashMap::new();
759 for bounds in EGYPTIAN_BOUNDS {
760 let mut start = 0.0;
761 for (lord, end) in bounds {
762 *total.entry(lord).or_insert(0.0) += end - start;
763 start = end;
764 }
765 }
766 assert_eq!(total["Saturn"], 57.0);
767 assert_eq!(total["Jupiter"], 79.0);
768 assert_eq!(total["Mars"], 66.0);
769 assert_eq!(total["Venus"], 82.0);
770 assert_eq!(total["Mercury"], 76.0);
771 }
772
773 #[test]
774 fn faces_follow_the_chaldean_order_from_mars() {
775 assert_eq!(face_lord(lon("Aries", 0.0)), "Mars");
776 assert_eq!(face_lord(lon("Aries", 15.0)), "Sun");
777 assert_eq!(face_lord(lon("Aries", 29.9)), "Venus");
778 assert_eq!(face_lord(lon("Taurus", 5.0)), "Mercury");
779 assert_eq!(face_lord(lon("Leo", 0.0)), "Saturn");
780 assert_eq!(face_lord(lon("Pisces", 25.0)), "Mars");
781 }
782
783 #[test]
784 fn bounds_by_scheme() {
785 assert_eq!(
788 bound_lord(lon("Aries", 10.0), DignityScheme::Lilly),
789 "Venus"
790 );
791 assert_eq!(
792 bound_lord(lon("Aries", 13.0), DignityScheme::Lilly),
793 "Venus"
794 );
795 assert_eq!(
796 bound_lord(lon("Aries", 13.0), DignityScheme::Dorothean),
797 "Mercury"
798 );
799 assert_eq!(
800 bound_lord(lon("Pisces", 29.99), DignityScheme::Lilly),
801 "Saturn"
802 );
803 assert_eq!(bound_lord(lon("Leo", 0.0), DignityScheme::Lilly), "Saturn");
804 assert_eq!(
805 bound_lord(lon("Leo", 0.0), DignityScheme::Dorothean),
806 "Jupiter"
807 );
808 }
809
810 #[test]
811 fn lillys_points() {
812 let sun = essential_dignity("Sun", lon("Aries", 19.0), true, DignityScheme::Lilly);
814 assert!(sun.exaltation && sun.triplicity && sun.face && !sun.domicile);
815 assert_eq!(sun.score, 8);
816 let saturn = essential_dignity("Saturn", lon("Aries", 5.0), true, DignityScheme::Lilly);
818 assert!(saturn.fall && saturn.peregrine && !saturn.detriment);
819 assert_eq!(saturn.score, -9);
820 let mars = essential_dignity("Mars", lon("Pisces", 2.0), true, DignityScheme::Lilly);
822 assert!(mars.triplicity);
823 let mars = essential_dignity("Mars", lon("Pisces", 2.0), true, DignityScheme::Dorothean);
824 assert!(!mars.triplicity);
825 let venus = essential_dignity("Venus", lon("Libra", 8.0), false, DignityScheme::Lilly);
827 assert!(venus.domicile && venus.bound);
828 assert_eq!(venus.lords.triplicity, ["Saturn", "Mercury"]);
829 let moon = essential_dignity(
831 "Moon",
832 lon("Capricorn", 1.0),
833 false,
834 DignityScheme::Dorothean,
835 );
836 assert!(moon.detriment && moon.triplicity && !moon.peregrine);
837 let moon = essential_dignity(
838 "Moon",
839 lon("Capricorn", 1.0),
840 true,
841 DignityScheme::Dorothean,
842 );
843 assert!(!moon.triplicity);
844 assert_eq!(moon.lords.triplicity, ["Venus", "Moon", "Mars"]);
845 }
846
847 #[test]
848 fn solar_phases() {
849 assert_eq!(solar_phase("Mercury", 100.1, 100.0), Some("cazimi"));
850 assert_eq!(solar_phase("Mercury", 105.0, 100.0), Some("combust"));
851 assert_eq!(solar_phase("Venus", 88.0, 100.0), Some("under_beams"));
852 assert_eq!(solar_phase("Mars", 130.0, 100.0), None);
853 assert_eq!(solar_phase("Sun", 100.0, 100.0), None);
854 assert_eq!(solar_phase("Moon", 355.0, 3.0), Some("combust"));
855 }
856
857 #[test]
858 fn mirrors() {
859 assert!((antiscion(lon("Gemini", 10.0)) - lon("Cancer", 20.0)).abs() < 1e-9);
861 assert!((contra_antiscion(lon("Gemini", 10.0)) - lon("Capricorn", 20.0)).abs() < 1e-9);
862 assert!((antiscion(lon("Capricorn", 5.0)) - lon("Sagittarius", 25.0)).abs() < 1e-9);
863 }
864
865 #[test]
866 fn sect_and_orientality() {
867 assert!(is_diurnal(0.0, 90.0));
869 assert!(!is_diurnal(0.0, 270.0));
871 assert!(is_oriental(350.0, 0.0));
872 assert!(!is_oriental(10.0, 0.0));
873 }
874}