use crate::kundli::astro::HouseSystem;
use crate::kundli::derive::sign::normalize_longitude;
use crate::kundli::error::DeriveError;
use crate::kundli::model::HouseNumber;
const DEGREES_PER_SIGN: f64 = 30.0;
const NUM_HOUSES: usize = 12;
pub(crate) fn derive_house(
planet_longitude: f64,
ascendant_longitude: f64,
house_cusps: &[f64],
house_system: HouseSystem,
) -> Result<HouseNumber, DeriveError> {
let planet_longitude = normalize_longitude(planet_longitude)?;
let ascendant_longitude = normalize_longitude(ascendant_longitude)?;
match house_system {
HouseSystem::WholeSign => derive_house_whole_sign(planet_longitude, ascendant_longitude),
_ => derive_house_from_cusps(planet_longitude, house_cusps),
}
}
fn derive_house_whole_sign(
planet_longitude: f64,
ascendant_longitude: f64,
) -> Result<HouseNumber, DeriveError> {
let planet_sign = longitude_to_sign_index(planet_longitude)?;
let ascendant_sign = longitude_to_sign_index(ascendant_longitude)?;
let house_number =
((planet_sign as i32 - ascendant_sign as i32 + NUM_HOUSES as i32) % NUM_HOUSES as i32) + 1;
debug_assert!((1..=12).contains(&house_number));
HouseNumber::new(house_number as u8).ok_or(DeriveError::InvalidHouseNumber(house_number as u8))
}
fn derive_house_from_cusps(
planet_longitude: f64,
house_cusps: &[f64],
) -> Result<HouseNumber, DeriveError> {
if house_cusps.len() != NUM_HOUSES {
return Err(DeriveError::InvalidHouseCusps(house_cusps.len()));
}
for &cusp in house_cusps {
if !cusp.is_finite() {
return Err(DeriveError::InvalidLongitude(cusp));
}
}
let planet_lon = normalize_longitude(planet_longitude)?;
for i in 0..NUM_HOUSES {
let cusp_start = normalize_longitude(house_cusps[i])?;
let cusp_end = normalize_longitude(house_cusps[(i + 1) % NUM_HOUSES])?;
if is_in_house(planet_lon, cusp_start, cusp_end) {
return HouseNumber::new((i + 1) as u8)
.ok_or(DeriveError::InvalidHouseNumber((i + 1) as u8));
}
}
Err(DeriveError::InvalidHouseCusps(house_cusps.len()))
}
fn longitude_to_sign_index(longitude: f64) -> Result<usize, DeriveError> {
let normalized = normalize_longitude(longitude)?;
Ok((normalized / DEGREES_PER_SIGN).floor() as usize % NUM_HOUSES)
}
fn is_in_house(planet_lon: f64, cusp_start: f64, cusp_end: f64) -> bool {
if cusp_start <= cusp_end {
planet_lon >= cusp_start && planet_lon < cusp_end
} else {
planet_lon >= cusp_start || planet_lon < cusp_end
}
}
const _: () = {
let _ = DEGREES_PER_SIGN;
let _ = NUM_HOUSES;
let _ = derive_house as fn(f64, f64, &[f64], HouseSystem) -> Result<HouseNumber, DeriveError>;
let _ = derive_house_whole_sign as fn(f64, f64) -> Result<HouseNumber, DeriveError>;
let _ = derive_house_from_cusps as fn(f64, &[f64]) -> Result<HouseNumber, DeriveError>;
let _ = longitude_to_sign_index as fn(f64) -> Result<usize, DeriveError>;
let _ = is_in_house as fn(f64, f64, f64) -> bool;
};
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn checked_constructor_rejects_invalid_house_number() {
assert_eq!(HouseNumber::new(0), None);
assert_eq!(HouseNumber::new(13), None);
}
fn house(number: u8) -> HouseNumber {
HouseNumber::new(number).unwrap()
}
#[test]
fn whole_sign_same_sign_as_ascendant_is_house_1() {
let house_number = derive_house(20.0, 15.0, &[], HouseSystem::WholeSign);
assert_eq!(house_number.unwrap(), house(1));
}
#[test]
fn whole_sign_next_sign_is_house_2() {
let house_number = derive_house(40.0, 15.0, &[], HouseSystem::WholeSign);
assert_eq!(house_number.unwrap(), house(2));
}
#[test]
fn whole_sign_previous_sign_is_house_12() {
let house_number = derive_house(350.0, 15.0, &[], HouseSystem::WholeSign);
assert_eq!(house_number.unwrap(), house(12));
}
#[test]
fn whole_sign_multiple_houses_away() {
let house_number = derive_house(95.0, 5.0, &[], HouseSystem::WholeSign);
assert_eq!(house_number.unwrap(), house(4));
}
#[test]
fn whole_sign_at_sign_boundary() {
let house_number = derive_house(30.0, 0.0, &[], HouseSystem::WholeSign);
assert_eq!(house_number.unwrap(), house(2));
}
#[test]
fn whole_sign_with_negative_longitude() {
let house_number = derive_house(-10.0, 15.0, &[], HouseSystem::WholeSign);
assert_eq!(house_number.unwrap(), house(12));
}
#[test]
fn cusp_based_planet_in_first_house() {
let cusps: Vec<f64> = (0..12).map(|i| (i * 30) as f64).collect();
let house_number = derive_house(15.0, 0.0, &cusps, HouseSystem::Equal);
assert_eq!(house_number.unwrap(), house(1));
}
#[test]
fn cusp_based_planet_in_middle_house() {
let cusps: Vec<f64> = (0..12).map(|i| (i * 30) as f64).collect();
let house_number = derive_house(105.0, 0.0, &cusps, HouseSystem::Equal);
assert_eq!(house_number.unwrap(), house(4));
}
#[test]
fn cusp_based_planet_at_cusp_boundary() {
let cusps: Vec<f64> = (0..12).map(|i| (i * 30) as f64).collect();
let house_number = derive_house(60.0, 0.0, &cusps, HouseSystem::Equal);
assert_eq!(house_number.unwrap(), house(3));
}
#[test]
fn cusp_based_wrap_around_last_house() {
let cusps: Vec<f64> = (0..12).map(|i| (i * 30) as f64).collect();
let house_number = derive_house(345.0, 0.0, &cusps, HouseSystem::Equal);
assert_eq!(house_number.unwrap(), house(12));
}
#[test]
fn cusp_based_wrap_around_from_360_to_first_cusp() {
let cusps: Vec<f64> = vec![
300.0, 330.0, 0.0, 30.0, 60.0, 90.0, 120.0, 150.0, 180.0, 210.0, 240.0, 270.0,
];
let house_number = derive_house(350.0, 300.0, &cusps, HouseSystem::Placidus);
assert_eq!(house_number.unwrap(), house(2));
}
#[test]
fn non_whole_sign_validates_ascendant_longitude() {
let cusps: Vec<f64> = (0..12).map(|i| (i * 30) as f64).collect();
let house_number = derive_house(15.0, f64::NAN, &cusps, HouseSystem::Equal);
assert!(
matches!(house_number, Err(DeriveError::InvalidLongitude(value)) if value.is_nan())
);
}
#[test]
fn cusp_based_planet_in_wrap_around_house() {
let cusps: Vec<f64> = vec![
300.0, 330.0, 0.0, 30.0, 60.0, 90.0, 120.0, 150.0, 180.0, 210.0, 240.0, 270.0,
];
let house_number = derive_house(5.0, 300.0, &cusps, HouseSystem::Placidus);
assert_eq!(house_number.unwrap(), house(3));
}
#[test]
fn invalid_longitude_returns_error() {
let house_number = derive_house(f64::NAN, 15.0, &[], HouseSystem::WholeSign);
assert!(matches!(
house_number,
Err(DeriveError::InvalidLongitude(_))
));
}
#[test]
fn infinite_longitude_returns_error() {
let house_number = derive_house(f64::INFINITY, 15.0, &[], HouseSystem::WholeSign);
assert!(matches!(
house_number,
Err(DeriveError::InvalidLongitude(_))
));
}
#[test]
fn wrong_number_of_cusps_returns_error() {
let cusps = vec![0.0, 30.0, 60.0]; let house_number = derive_house(45.0, 0.0, &cusps, HouseSystem::Equal);
assert!(matches!(
house_number,
Err(DeriveError::InvalidHouseCusps(3))
));
}
#[test]
fn empty_cusps_returns_error() {
let house_number = derive_house(45.0, 0.0, &[], HouseSystem::Equal);
assert!(matches!(
house_number,
Err(DeriveError::InvalidHouseCusps(0))
));
}
#[test]
fn cusp_with_nan_returns_error() {
let cusps = vec![
0.0,
30.0,
60.0,
90.0,
120.0,
150.0,
f64::NAN,
210.0,
240.0,
270.0,
300.0,
330.0,
];
let house_number = derive_house(45.0, 0.0, &cusps, HouseSystem::Equal);
assert!(matches!(
house_number,
Err(DeriveError::InvalidLongitude(_))
));
}
#[test]
fn is_in_house_normal_case() {
assert!(is_in_house(45.0, 30.0, 60.0));
assert!(!is_in_house(25.0, 30.0, 60.0));
assert!(!is_in_house(65.0, 30.0, 60.0));
}
#[test]
fn is_in_house_wrap_around_case() {
assert!(is_in_house(355.0, 350.0, 10.0));
assert!(is_in_house(5.0, 350.0, 10.0));
assert!(!is_in_house(20.0, 350.0, 10.0));
assert!(!is_in_house(340.0, 350.0, 10.0));
}
#[test]
fn whole_sign_with_large_longitude() {
let house_number = derive_house(725.0, 15.0, &[], HouseSystem::WholeSign);
assert_eq!(house_number.unwrap(), house(1));
}
#[test]
fn all_whole_sign_houses() {
for house_num in 1..=12 {
let planet_sign = house_num - 1; let planet_lon = (planet_sign as f64) * 30.0 + 15.0; let house_number = derive_house(planet_lon, 0.0, &[], HouseSystem::WholeSign);
assert_eq!(
house_number.unwrap(),
house(house_num),
"Failed for house {}",
house_num
);
}
}
}