1use crate::error::EngineError;
5
6pub 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
13pub enum Hora {
14 #[default]
16 Parashara,
17 Cyclic,
19}
20
21impl Hora {
22 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
33pub 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 let odd = sign % 2 == 0;
51 let modality = sign % 3;
53 let element = sign % 4; 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 } else {
64 3 }
66 }
67 Hora::Cyclic => from(sign * 2),
68 },
69 3 => (sign + part * 4) % 12,
71 4 => (sign + part * 3) % 12,
73 5 => {
76 if odd {
77 [0, 10, 8, 2, 6][part]
78 } else {
79 [1, 5, 11, 9, 7][part]
80 }
81 }
82 6 | 8 | 11 => from(sign * usize::from(n)),
84 7 => from(if odd { sign } else { sign + 6 }),
86 9 => from([0, 9, 6, 3][element]),
88 10 => from(if odd { sign } else { sign + 8 }),
90 12 => from(sign),
92 16 | 45 => from([0, 4, 8][modality]),
95 20 => from([0, 8, 4][modality]),
97 24 => from(if odd { 4 } else { 3 }),
99 27 => from([0, 3, 6, 9][element]),
102 30 => trimshamsha(within, odd),
104 40 => from(if odd { 0 } else { 6 }),
106 60 => from(sign),
108 _ => unreachable!("checked against VARGAS"),
109 };
110 Ok((target, degree))
111}
112
113fn trimshamsha(within: f64, odd: bool) -> usize {
115 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); assert_eq!(sign(29.9, 9), 8); assert_eq!(sign(30.0, 9), 9); assert_eq!(sign(45.0, 9), 1); assert_eq!(sign(90.0, 9), 3); assert_eq!(sign(359.9, 9), 11); }
143
144 #[test]
145 fn hora_parashara_and_cyclic() {
146 assert_eq!(sign(5.0, 2), 4); assert_eq!(sign(20.0, 2), 3); assert_eq!(sign(35.0, 2), 3); assert_eq!(sign(50.0, 2), 4); 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); assert_eq!(sign(5.0, 30), 10); assert_eq!(sign(18.0, 30), 2); assert_eq!(sign(30.0 + 12.0, 30), 11); assert_eq!(sign(30.0 + 29.9, 30), 7); }
162
163 #[test]
164 fn other_vargas() {
165 assert_eq!(sign(15.0, 3), 4); assert_eq!(sign(30.0 + 25.0, 7), 0); assert_eq!(sign(30.0 + 10.0, 10), (1 + 8 + 3) % 12); assert_eq!(sign(60.0 + 1.0, 16), 8); assert_eq!(sign(30.0 + 1.0, 20), 8); assert_eq!(sign(30.0 + 1.0, 24), 3); assert_eq!(sign(90.0 + 1.0, 27), 9); assert_eq!(sign(30.5, 40), 6); assert_eq!(sign(30.0 + 29.9, 60), (1 + 59) % 12);
174 assert_eq!(sign(30.0 + 1.0, 8), 8); 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}