use crate::error::EngineError;
pub const VARGAS: [u8; 20] = [
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 16, 20, 24, 27, 30, 40, 45, 60,
];
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Hora {
#[default]
Parashara,
Cyclic,
}
impl Hora {
pub fn from_code(code: &str) -> Self {
let code = code.trim();
if code.eq_ignore_ascii_case("cyclic") || code.eq_ignore_ascii_case("parivritti") {
Hora::Cyclic
} else {
Hora::Parashara
}
}
}
pub fn varga_position(longitude: f64, n: u8, hora: Hora) -> Result<(usize, f64), EngineError> {
if !VARGAS.contains(&n) {
return Err(EngineError::InvalidInput(format!("no varga D{n}")));
}
if !longitude.is_finite() {
return Err(EngineError::InvalidInput(format!(
"longitude must be finite, got {longitude}"
)));
}
let longitude = longitude.rem_euclid(360.0);
let sign = ((longitude / 30.0) as usize).min(11);
let within = (longitude - sign as f64 * 30.0).clamp(0.0, 30.0);
let part = ((within * f64::from(n) / 30.0) as usize).min(usize::from(n) - 1);
let degree = (within * f64::from(n)).rem_euclid(30.0);
let odd = sign % 2 == 0;
let modality = sign % 3;
let element = sign % 4;
let from = |start: usize| (start + part) % 12;
let target = match n {
1 => sign,
2 => match hora {
Hora::Parashara => {
let first_half = part == 0;
if odd == first_half {
4 } else {
3 }
}
Hora::Cyclic => from(sign * 2),
},
3 => (sign + part * 4) % 12,
4 => (sign + part * 3) % 12,
5 => {
if odd {
[0, 10, 8, 2, 6][part]
} else {
[1, 5, 11, 9, 7][part]
}
}
6 | 8 | 11 => from(sign * usize::from(n)),
7 => from(if odd { sign } else { sign + 6 }),
9 => from([0, 9, 6, 3][element]),
10 => from(if odd { sign } else { sign + 8 }),
12 => from(sign),
16 | 45 => from([0, 4, 8][modality]),
20 => from([0, 8, 4][modality]),
24 => from(if odd { 4 } else { 3 }),
27 => from([0, 3, 6, 9][element]),
30 => trimshamsha(within, odd),
40 => from(if odd { 0 } else { 6 }),
60 => from(sign),
_ => unreachable!("checked against VARGAS"),
};
Ok((target, degree))
}
fn trimshamsha(within: f64, odd: bool) -> usize {
const ODD: [(f64, usize); 5] = [(5.0, 0), (10.0, 10), (18.0, 8), (25.0, 2), (30.0, 6)];
const EVEN: [(f64, usize); 5] = [(5.0, 1), (12.0, 5), (20.0, 11), (25.0, 9), (30.0, 7)];
let table = if odd { &ODD } else { &EVEN };
table
.iter()
.find(|(upper, _)| within < *upper)
.map_or(table[4].1, |(_, sign)| *sign)
}
#[cfg(test)]
mod tests {
use super::*;
fn sign(longitude: f64, n: u8) -> usize {
varga_position(longitude, n, Hora::Parashara).unwrap().0
}
#[test]
fn navamsha_follows_the_elements() {
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); }
#[test]
fn hora_parashara_and_cyclic() {
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;
assert_eq!((cyclic(5.0), cyclic(20.0), cyclic(35.0)), (0, 1, 2));
}
#[test]
fn trimshamsha_boundaries_are_half_open() {
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); }
#[test]
fn other_vargas() {
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);
assert_eq!(sign(30.0 + 1.0, 8), 8); let (s, degree) = varga_position(3.5, 9, Hora::Parashara).unwrap();
assert_eq!(s, 1);
assert!((degree - 1.5).abs() < 1e-9);
}
#[test]
fn unknown_vargas_are_rejected() {
assert!(varga_position(10.0, 13, Hora::Parashara).is_err());
assert!(varga_position(f64::NAN, 9, Hora::Parashara).is_err());
}
}