#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u8)]
pub enum Sign {
Aries = 0,
Taurus,
Gemini,
Cancer,
Leo,
Virgo,
Libra,
Scorpio,
Sagittarius,
Capricorn,
Aquarius,
Pisces,
}
impl Sign {
#[must_use]
pub fn from_index(idx: u8) -> Self {
match idx % 12 {
0 => Self::Aries,
1 => Self::Taurus,
2 => Self::Gemini,
3 => Self::Cancer,
4 => Self::Leo,
5 => Self::Virgo,
6 => Self::Libra,
7 => Self::Scorpio,
8 => Self::Sagittarius,
9 => Self::Capricorn,
10 => Self::Aquarius,
_ => Self::Pisces,
}
}
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::Aries => "Aries",
Self::Taurus => "Taurus",
Self::Gemini => "Gemini",
Self::Cancer => "Cancer",
Self::Leo => "Leo",
Self::Virgo => "Virgo",
Self::Libra => "Libra",
Self::Scorpio => "Scorpio",
Self::Sagittarius => "Sagittarius",
Self::Capricorn => "Capricorn",
Self::Aquarius => "Aquarius",
Self::Pisces => "Pisces",
}
}
#[must_use]
pub fn opposite(self) -> Self {
Self::from_index((self as u8 + 6) % 12)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DignityPlanet {
Sun,
Moon,
Mercury,
Venus,
Mars,
Jupiter,
Saturn,
Uranus,
Neptune,
Pluto,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DignityState {
Domicile,
Exaltation,
Detriment,
Fall,
Peregrine,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RulershipScheme {
Traditional,
Modern,
}
const TRADITIONAL_RULERS: [DignityPlanet; 12] = [
DignityPlanet::Mars, DignityPlanet::Venus, DignityPlanet::Mercury, DignityPlanet::Moon, DignityPlanet::Sun, DignityPlanet::Mercury, DignityPlanet::Venus, DignityPlanet::Mars, DignityPlanet::Jupiter, DignityPlanet::Saturn, DignityPlanet::Saturn, DignityPlanet::Jupiter, ];
const MODERN_RULERS: [DignityPlanet; 12] = [
DignityPlanet::Mars, DignityPlanet::Venus, DignityPlanet::Mercury, DignityPlanet::Moon, DignityPlanet::Sun, DignityPlanet::Mercury, DignityPlanet::Venus, DignityPlanet::Pluto, DignityPlanet::Jupiter, DignityPlanet::Saturn, DignityPlanet::Uranus, DignityPlanet::Neptune, ];
const EXALTATIONS: [(u8, DignityPlanet); 7] = [
(0, DignityPlanet::Sun), (1, DignityPlanet::Moon), (5, DignityPlanet::Mercury), (11, DignityPlanet::Venus), (9, DignityPlanet::Mars), (3, DignityPlanet::Jupiter), (6, DignityPlanet::Saturn), ];
#[must_use]
pub fn sign_of(longitude_deg: f64) -> Sign {
let normalized = vedaksha_math::angle::normalize_degrees(longitude_deg);
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let sign_idx = (normalized / 30.0) as u8;
Sign::from_index(sign_idx)
}
#[must_use]
pub fn domicile_ruler(sign: Sign, scheme: RulershipScheme) -> DignityPlanet {
let idx = sign as usize;
match scheme {
RulershipScheme::Traditional => TRADITIONAL_RULERS[idx],
RulershipScheme::Modern => MODERN_RULERS[idx],
}
}
#[must_use]
pub fn exaltation_ruler(sign: Sign) -> Option<DignityPlanet> {
let idx = sign as u8;
EXALTATIONS.iter().find(|(s, _)| *s == idx).map(|(_, p)| *p)
}
#[must_use]
pub fn dignity_of(planet: DignityPlanet, sign: Sign, scheme: RulershipScheme) -> DignityState {
if domicile_ruler(sign, scheme) == planet {
return DignityState::Domicile;
}
if exaltation_ruler(sign) == Some(planet) {
return DignityState::Exaltation;
}
let opposite = sign.opposite();
if domicile_ruler(opposite, scheme) == planet {
return DignityState::Detriment;
}
if let Some(exalt_planet) = exaltation_ruler(opposite) {
if exalt_planet == planet {
return DignityState::Fall;
}
}
DignityState::Peregrine
}
#[must_use]
pub fn dignity_at_longitude(
planet: DignityPlanet,
longitude_deg: f64,
scheme: RulershipScheme,
) -> DignityState {
let sign = sign_of(longitude_deg);
dignity_of(planet, sign, scheme)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AccidentalDignity {
Cazimi,
Combust,
UnderSunBeams,
Retrograde,
DirectAndFast,
Angular,
Succedent,
Cadent,
}
#[must_use]
pub fn accidental_dignities(
planet_longitude: f64,
sun_longitude: f64,
speed: f64,
house: u8,
) -> Vec<AccidentalDignity> {
let mut dignities = Vec::new();
let sun_separation = vedaksha_math::angle::angular_separation(planet_longitude, sun_longitude);
if sun_separation < 17.0 / 60.0 {
dignities.push(AccidentalDignity::Cazimi);
} else if sun_separation < 8.5 {
dignities.push(AccidentalDignity::Combust);
} else if sun_separation < 17.0 {
dignities.push(AccidentalDignity::UnderSunBeams);
}
if speed < 0.0 {
dignities.push(AccidentalDignity::Retrograde);
} else if speed > 1.0 {
dignities.push(AccidentalDignity::DirectAndFast);
}
match house {
1 | 4 | 7 | 10 => dignities.push(AccidentalDignity::Angular),
2 | 5 | 8 | 11 => dignities.push(AccidentalDignity::Succedent),
3 | 6 | 9 | 12 => dignities.push(AccidentalDignity::Cadent),
_ => {}
}
dignities
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sign_of_0_is_aries() {
assert_eq!(sign_of(0.0), Sign::Aries);
}
#[test]
fn sign_of_30_is_taurus() {
assert_eq!(sign_of(30.0), Sign::Taurus);
}
#[test]
fn sign_of_359_is_pisces() {
assert_eq!(sign_of(359.0), Sign::Pisces);
}
#[test]
fn sign_of_negative_wraps_correctly() {
assert_eq!(sign_of(-10.0), Sign::Pisces);
assert_eq!(sign_of(-360.0), Sign::Aries);
}
#[test]
fn sign_of_boundary_values() {
assert_eq!(sign_of(60.0), Sign::Gemini);
assert_eq!(sign_of(90.0), Sign::Cancer);
assert_eq!(sign_of(180.0), Sign::Libra);
assert_eq!(sign_of(270.0), Sign::Capricorn);
}
#[test]
fn opposite_aries_is_libra() {
assert_eq!(Sign::Aries.opposite(), Sign::Libra);
}
#[test]
fn opposite_cancer_is_capricorn() {
assert_eq!(Sign::Cancer.opposite(), Sign::Capricorn);
}
#[test]
fn opposite_is_symmetric() {
for idx in 0u8..12 {
let sign = Sign::from_index(idx);
assert_eq!(
sign.opposite().opposite(),
sign,
"{} opposite not symmetric",
sign.name()
);
}
}
#[test]
fn sun_rules_leo_in_both_schemes() {
assert_eq!(
domicile_ruler(Sign::Leo, RulershipScheme::Traditional),
DignityPlanet::Sun
);
assert_eq!(
domicile_ruler(Sign::Leo, RulershipScheme::Modern),
DignityPlanet::Sun
);
}
#[test]
fn moon_rules_cancer_traditional() {
assert_eq!(
domicile_ruler(Sign::Cancer, RulershipScheme::Traditional),
DignityPlanet::Moon
);
}
#[test]
fn scorpio_ruler_mars_traditional_pluto_modern() {
assert_eq!(
domicile_ruler(Sign::Scorpio, RulershipScheme::Traditional),
DignityPlanet::Mars
);
assert_eq!(
domicile_ruler(Sign::Scorpio, RulershipScheme::Modern),
DignityPlanet::Pluto
);
}
#[test]
fn aquarius_ruler_saturn_traditional_uranus_modern() {
assert_eq!(
domicile_ruler(Sign::Aquarius, RulershipScheme::Traditional),
DignityPlanet::Saturn
);
assert_eq!(
domicile_ruler(Sign::Aquarius, RulershipScheme::Modern),
DignityPlanet::Uranus
);
}
#[test]
fn pisces_ruler_jupiter_traditional_neptune_modern() {
assert_eq!(
domicile_ruler(Sign::Pisces, RulershipScheme::Traditional),
DignityPlanet::Jupiter
);
assert_eq!(
domicile_ruler(Sign::Pisces, RulershipScheme::Modern),
DignityPlanet::Neptune
);
}
#[test]
fn sun_exalted_in_aries() {
assert_eq!(exaltation_ruler(Sign::Aries), Some(DignityPlanet::Sun));
}
#[test]
fn saturn_exalted_in_libra() {
assert_eq!(exaltation_ruler(Sign::Libra), Some(DignityPlanet::Saturn));
}
#[test]
fn mars_exalted_in_capricorn() {
assert_eq!(exaltation_ruler(Sign::Capricorn), Some(DignityPlanet::Mars));
}
#[test]
fn no_exaltation_in_aries_for_moon() {
assert_ne!(exaltation_ruler(Sign::Aries), Some(DignityPlanet::Moon));
}
#[test]
fn gemini_has_no_classical_exaltation() {
assert_eq!(exaltation_ruler(Sign::Gemini), None);
}
#[test]
fn sun_domicile_in_leo() {
assert_eq!(
dignity_of(DignityPlanet::Sun, Sign::Leo, RulershipScheme::Traditional),
DignityState::Domicile
);
}
#[test]
fn sun_detriment_in_aquarius() {
assert_eq!(
dignity_of(
DignityPlanet::Sun,
Sign::Aquarius,
RulershipScheme::Traditional
),
DignityState::Detriment
);
}
#[test]
fn sun_exaltation_in_aries() {
assert_eq!(
dignity_of(
DignityPlanet::Sun,
Sign::Aries,
RulershipScheme::Traditional
),
DignityState::Exaltation
);
}
#[test]
fn sun_fall_in_libra() {
assert_eq!(
dignity_of(
DignityPlanet::Sun,
Sign::Libra,
RulershipScheme::Traditional
),
DignityState::Fall
);
}
#[test]
fn mars_exaltation_in_capricorn() {
assert_eq!(
dignity_of(
DignityPlanet::Mars,
Sign::Capricorn,
RulershipScheme::Traditional
),
DignityState::Exaltation
);
}
#[test]
fn mars_fall_in_cancer() {
assert_eq!(
dignity_of(
DignityPlanet::Mars,
Sign::Cancer,
RulershipScheme::Traditional
),
DignityState::Fall
);
}
#[test]
fn moon_domicile_in_cancer_traditional() {
assert_eq!(
dignity_of(
DignityPlanet::Moon,
Sign::Cancer,
RulershipScheme::Traditional
),
DignityState::Domicile
);
}
#[test]
fn sun_peregrine_in_gemini() {
assert_eq!(
dignity_of(
DignityPlanet::Sun,
Sign::Gemini,
RulershipScheme::Traditional
),
DignityState::Peregrine
);
}
#[test]
fn pluto_domicile_in_scorpio_modern_only() {
assert_eq!(
dignity_of(DignityPlanet::Pluto, Sign::Scorpio, RulershipScheme::Modern),
DignityState::Domicile
);
assert_eq!(
dignity_of(
DignityPlanet::Pluto,
Sign::Scorpio,
RulershipScheme::Traditional
),
DignityState::Peregrine
);
}
#[test]
fn uranus_domicile_in_aquarius_modern_only() {
assert_eq!(
dignity_of(
DignityPlanet::Uranus,
Sign::Aquarius,
RulershipScheme::Modern
),
DignityState::Domicile
);
assert_eq!(
dignity_of(
DignityPlanet::Uranus,
Sign::Aquarius,
RulershipScheme::Traditional
),
DignityState::Peregrine
);
}
#[test]
fn dignity_at_longitude_sun_in_leo_by_longitude() {
assert_eq!(
dignity_at_longitude(DignityPlanet::Sun, 130.0, RulershipScheme::Traditional),
DignityState::Domicile
);
}
#[test]
fn dignity_at_longitude_wraps_negative() {
assert_eq!(
dignity_at_longitude(DignityPlanet::Jupiter, -10.0, RulershipScheme::Traditional),
DignityState::Domicile
);
}
#[test]
fn cazimi_within_17_arcminutes_of_sun() {
let result = accidental_dignities(100.1, 100.0, 1.0, 1);
assert!(
result.contains(&AccidentalDignity::Cazimi),
"Expected Cazimi, got {result:?}"
);
}
#[test]
fn combust_within_8_5_degrees_of_sun() {
let result = accidental_dignities(105.0, 100.0, 1.0, 1);
assert!(
result.contains(&AccidentalDignity::Combust),
"Expected Combust, got {result:?}"
);
}
#[test]
fn under_sun_beams_within_17_degrees() {
let result = accidental_dignities(115.0, 100.0, 1.0, 1);
assert!(
result.contains(&AccidentalDignity::UnderSunBeams),
"Expected UnderSunBeams, got {result:?}"
);
}
#[test]
fn retrograde_with_negative_speed() {
let result = accidental_dignities(200.0, 100.0, -0.5, 1);
assert!(
result.contains(&AccidentalDignity::Retrograde),
"Expected Retrograde, got {result:?}"
);
}
#[test]
fn direct_and_fast_with_high_speed() {
let result = accidental_dignities(200.0, 100.0, 1.5, 1);
assert!(
result.contains(&AccidentalDignity::DirectAndFast),
"Expected DirectAndFast, got {result:?}"
);
}
#[test]
fn angular_house() {
let result = accidental_dignities(200.0, 100.0, 1.0, 1);
assert!(
result.contains(&AccidentalDignity::Angular),
"Expected Angular for house 1, got {result:?}"
);
let result10 = accidental_dignities(200.0, 100.0, 1.0, 10);
assert!(
result10.contains(&AccidentalDignity::Angular),
"Expected Angular for house 10, got {result10:?}"
);
}
#[test]
fn cadent_house() {
let result = accidental_dignities(200.0, 100.0, 1.0, 3);
assert!(
result.contains(&AccidentalDignity::Cadent),
"Expected Cadent for house 3, got {result:?}"
);
let result12 = accidental_dignities(200.0, 100.0, 1.0, 12);
assert!(
result12.contains(&AccidentalDignity::Cadent),
"Expected Cadent for house 12, got {result12:?}"
);
}
#[test]
fn succedent_house() {
let result = accidental_dignities(200.0, 100.0, 1.0, 2);
assert!(
result.contains(&AccidentalDignity::Succedent),
"Expected Succedent for house 2, got {result:?}"
);
}
#[test]
fn no_sun_condition_beyond_17_degrees() {
let result = accidental_dignities(200.0, 100.0, 1.0, 1);
assert!(
!result.contains(&AccidentalDignity::Cazimi),
"Should not be Cazimi at 100° separation"
);
assert!(
!result.contains(&AccidentalDignity::Combust),
"Should not be Combust at 100° separation"
);
assert!(
!result.contains(&AccidentalDignity::UnderSunBeams),
"Should not be UnderSunBeams at 100° separation"
);
}
}