Skip to main content

astroceleste_engine/vedic/
varga.rs

1//! Divisional charts (vargas): each sign is cut into `n` parts, and each part maps to a
2//! sign of the varga by Parashara's rules (Brihat Parashara Hora Shastra, ch. 6-7).
3
4use crate::error::EngineError;
5
6/// The vargas computed: the sixteen of Parashara's shodashavarga plus D5, D6, D8 and D11.
7pub const VARGAS: [u8; 20] = [
8    1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 16, 20, 24, 27, 30, 40, 45, 60,
9];
10
11/// How the hora (D2) is cast.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
13pub enum Hora {
14    /// Parashara: the Sun's hora (Leo) and the Moon's (Cancer), Leo first in odd signs.
15    #[default]
16    Parashara,
17    /// Cyclic (parivritti): the halves run through the zodiac from Aries.
18    Cyclic,
19}
20
21impl Hora {
22    /// "cyclic" (any case) or "parivritti" is [`Hora::Cyclic`]; anything else is Parashara's.
23    pub fn from_code(code: &str) -> Self {
24        let code = code.trim();
25        if code.eq_ignore_ascii_case("cyclic") || code.eq_ignore_ascii_case("parivritti") {
26            Hora::Cyclic
27        } else {
28            Hora::Parashara
29        }
30    }
31}
32
33/// The sign (0 = Aries) and degree within it (0-30) of sidereal longitude `longitude` in
34/// varga D`n`. The degree is the position within the part, stretched to a whole sign.
35pub fn varga_position(longitude: f64, n: u8, hora: Hora) -> Result<(usize, f64), EngineError> {
36    if !VARGAS.contains(&n) {
37        return Err(EngineError::InvalidInput(format!("no varga D{n}")));
38    }
39    if !longitude.is_finite() {
40        return Err(EngineError::InvalidInput(format!(
41            "longitude must be finite, got {longitude}"
42        )));
43    }
44    let longitude = longitude.rem_euclid(360.0);
45    let sign = ((longitude / 30.0) as usize).min(11);
46    let within = (longitude - sign as f64 * 30.0).clamp(0.0, 30.0);
47    let part = ((within * f64::from(n) / 30.0) as usize).min(usize::from(n) - 1);
48    let degree = (within * f64::from(n)).rem_euclid(30.0);
49    // Signs by their 1-based parity: Aries (index 0) is odd.
50    let odd = sign % 2 == 0;
51    // Movable (cardinal), fixed and dual (mutable) signs.
52    let modality = sign % 3;
53    let element = sign % 4; // fire, earth, air, water
54
55    let from = |start: usize| (start + part) % 12;
56    let target = match n {
57        1 => sign,
58        2 => match hora {
59            Hora::Parashara => {
60                let first_half = part == 0;
61                if odd == first_half {
62                    4 // Leo, the Sun's
63                } else {
64                    3 // Cancer, the Moon's
65                }
66            }
67            Hora::Cyclic => from(sign * 2),
68        },
69        // Drekkana: the sign itself, its 5th and its 9th.
70        3 => (sign + part * 4) % 12,
71        // Chaturthamsha: the sign and its kendras.
72        4 => (sign + part * 3) % 12,
73        // Panchamsha: the lords' odd signs (Mars, Saturn, Jupiter, Mercury, Venus), even
74        // signs their even ones.
75        5 => {
76            if odd {
77                [0, 10, 8, 2, 6][part]
78            } else {
79                [1, 5, 11, 9, 7][part]
80            }
81        }
82        // Shashthamsha, Ashtamsha and Rudramsha: cyclic from Aries.
83        6 | 8 | 11 => from(sign * usize::from(n)),
84        // Saptamsha: odd signs from themselves, even signs from their 7th.
85        7 => from(if odd { sign } else { sign + 6 }),
86        // Navamsha: fire from Aries, earth from Capricorn, air from Libra, water from Cancer.
87        9 => from([0, 9, 6, 3][element]),
88        // Dashamsha: odd signs from themselves, even signs from their 9th.
89        10 => from(if odd { sign } else { sign + 8 }),
90        // Dwadashamsha: from the sign itself.
91        12 => from(sign),
92        // Shodashamsha and Akshavedamsha: movable from Aries, fixed from Leo, dual from
93        // Sagittarius.
94        16 | 45 => from([0, 4, 8][modality]),
95        // Vimshamsha: movable from Aries, fixed from Sagittarius, dual from Leo.
96        20 => from([0, 8, 4][modality]),
97        // Siddhamsha: odd signs from Leo, even signs from Cancer.
98        24 => from(if odd { 4 } else { 3 }),
99        // Nakshatramsha: fire from Aries, earth from Cancer, air from Libra, water from
100        // Capricorn.
101        27 => from([0, 3, 6, 9][element]),
102        // Trimshamsha: unequal parts ruled by Mars, Saturn, Jupiter, Mercury and Venus.
103        30 => trimshamsha(within, odd),
104        // Khavedamsha: odd signs from Aries, even signs from Libra.
105        40 => from(if odd { 0 } else { 6 }),
106        // Shashtyamsha: from the sign itself.
107        60 => from(sign),
108        _ => unreachable!("checked against VARGAS"),
109    };
110    Ok((target, degree))
111}
112
113/// Trimshamsha sign of `within` degrees of an odd or even sign.
114fn trimshamsha(within: f64, odd: bool) -> usize {
115    // (upper bound, sign): odd signs Mars 5°, Saturn 5°, Jupiter 8°, Mercury 7°, Venus 5°;
116    // even signs the reverse with the lords' even signs.
117    const ODD: [(f64, usize); 5] = [(5.0, 0), (10.0, 10), (18.0, 8), (25.0, 2), (30.0, 6)];
118    const EVEN: [(f64, usize); 5] = [(5.0, 1), (12.0, 5), (20.0, 11), (25.0, 9), (30.0, 7)];
119    let table = if odd { &ODD } else { &EVEN };
120    table
121        .iter()
122        .find(|(upper, _)| within < *upper)
123        .map_or(table[4].1, |(_, sign)| *sign)
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129
130    fn sign(longitude: f64, n: u8) -> usize {
131        varga_position(longitude, n, Hora::Parashara).unwrap().0
132    }
133
134    #[test]
135    fn navamsha_follows_the_elements() {
136        assert_eq!(sign(0.0, 9), 0); // 0° Aries: Aries
137        assert_eq!(sign(29.9, 9), 8); // end of Aries: Sagittarius
138        assert_eq!(sign(30.0, 9), 9); // 0° Taurus: Capricorn
139        assert_eq!(sign(45.0, 9), 1); // 15° Taurus: part 4 from Capricorn, Taurus
140        assert_eq!(sign(90.0, 9), 3); // 0° Cancer: Cancer
141        assert_eq!(sign(359.9, 9), 11); // end of Pisces: Pisces (vargottama)
142    }
143
144    #[test]
145    fn hora_parashara_and_cyclic() {
146        assert_eq!(sign(5.0, 2), 4); // Aries 1st half: Leo
147        assert_eq!(sign(20.0, 2), 3); // Aries 2nd half: Cancer
148        assert_eq!(sign(35.0, 2), 3); // Taurus 1st half: Cancer
149        assert_eq!(sign(50.0, 2), 4); // Taurus 2nd half: Leo
150        let cyclic = |lon| varga_position(lon, 2, Hora::Cyclic).unwrap().0;
151        assert_eq!((cyclic(5.0), cyclic(20.0), cyclic(35.0)), (0, 1, 2));
152    }
153
154    #[test]
155    fn trimshamsha_boundaries_are_half_open() {
156        assert_eq!(sign(4.99, 30), 0); // odd, Mars
157        assert_eq!(sign(5.0, 30), 10); // odd, Saturn from 5°
158        assert_eq!(sign(18.0, 30), 2); // odd, Mercury from 18°
159        assert_eq!(sign(30.0 + 12.0, 30), 11); // even, Jupiter from 12°
160        assert_eq!(sign(30.0 + 29.9, 30), 7); // even, Mars
161    }
162
163    #[test]
164    fn other_vargas() {
165        assert_eq!(sign(15.0, 3), 4); // Aries 2nd drekkana: Leo
166        assert_eq!(sign(30.0 + 25.0, 7), 0); // Taurus part 5 from Scorpio: Aries
167        assert_eq!(sign(30.0 + 10.0, 10), (1 + 8 + 3) % 12); // Taurus part 3 from Capricorn
168        assert_eq!(sign(60.0 + 1.0, 16), 8); // Gemini (dual) from Sagittarius
169        assert_eq!(sign(30.0 + 1.0, 20), 8); // Taurus (fixed) from Sagittarius
170        assert_eq!(sign(30.0 + 1.0, 24), 3); // even from Cancer
171        assert_eq!(sign(90.0 + 1.0, 27), 9); // water from Capricorn
172        assert_eq!(sign(30.5, 40), 6); // even from Libra
173        assert_eq!(sign(30.0 + 29.9, 60), (1 + 59) % 12);
174        assert_eq!(sign(30.0 + 1.0, 8), 8); // Taurus: Sagittarius
175        let (s, degree) = varga_position(3.5, 9, Hora::Parashara).unwrap();
176        assert_eq!(s, 1);
177        assert!((degree - 1.5).abs() < 1e-9);
178    }
179
180    #[test]
181    fn unknown_vargas_are_rejected() {
182        assert!(varga_position(10.0, 13, Hora::Parashara).is_err());
183        assert!(varga_position(f64::NAN, 9, Hora::Parashara).is_err());
184    }
185}