use crate::aspects::{AspectType, BodyPosition};
#[derive(Debug, Clone)]
pub struct SynastryAspect {
pub chart_a_body: usize,
pub chart_b_body: usize,
pub aspect_type: AspectType,
pub orb: f64,
pub strength: f64,
}
#[must_use]
pub fn find_synastry_aspects(
chart_a: &[BodyPosition],
chart_b: &[BodyPosition],
aspect_types: &[AspectType],
orb_factor: f64,
) -> Vec<SynastryAspect> {
let mut aspects = Vec::new();
for (i, pos_a) in chart_a.iter().enumerate() {
for (j, pos_b) in chart_b.iter().enumerate() {
let separation =
vedaksha_math::angle::angular_separation(pos_a.longitude, pos_b.longitude);
for &aspect_type in aspect_types {
let target = aspect_type.angle();
let max_orb = aspect_type.default_orb() * orb_factor;
let orb = (separation - target).abs();
if orb <= max_orb {
let strength = 1.0 - orb / max_orb;
aspects.push(SynastryAspect {
chart_a_body: i,
chart_b_body: j,
aspect_type,
orb,
strength,
});
}
}
}
}
aspects
}
#[cfg(test)]
mod tests {
use super::*;
const EPS: f64 = 1e-9;
fn pos(longitude: f64) -> BodyPosition {
BodyPosition {
longitude,
speed: 1.0,
}
}
#[test]
fn conjunction_detected_between_charts() {
let chart_a = [pos(30.0)];
let chart_b = [pos(32.0)];
let aspects = find_synastry_aspects(&chart_a, &chart_b, AspectType::MAJOR, 1.0);
assert!(
aspects
.iter()
.any(|a| a.aspect_type == AspectType::Conjunction),
"Expected a conjunction synastry aspect"
);
}
#[test]
fn trine_detected_between_charts() {
let chart_a = [pos(0.0)];
let chart_b = [pos(120.0)];
let aspects = find_synastry_aspects(&chart_a, &chart_b, AspectType::MAJOR, 1.0);
assert!(
aspects.iter().any(|a| a.aspect_type == AspectType::Trine),
"Expected a trine synastry aspect"
);
}
#[test]
fn no_aspects_when_far_apart() {
let chart_a = [pos(0.0)];
let chart_b = [pos(75.0)];
let aspects = find_synastry_aspects(&chart_a, &chart_b, AspectType::MAJOR, 1.0);
assert!(
aspects.is_empty(),
"Expected no aspects, found: {aspects:?}"
);
}
#[test]
fn multiple_aspects_detected_between_charts() {
let chart_a = [pos(0.0), pos(90.0)];
let chart_b = [pos(120.0), pos(0.5)];
let aspects = find_synastry_aspects(&chart_a, &chart_b, AspectType::MAJOR, 1.0);
assert!(
aspects.len() >= 2,
"Expected multiple synastry aspects, got {}",
aspects.len()
);
}
#[test]
fn exact_aspect_has_strength_one() {
let chart_a = [pos(0.0)];
let chart_b = [pos(0.0)];
let aspects = find_synastry_aspects(&chart_a, &chart_b, AspectType::MAJOR, 1.0);
let conj = aspects
.iter()
.find(|a| a.aspect_type == AspectType::Conjunction)
.expect("Expected a conjunction");
assert!(
(conj.strength - 1.0).abs() < EPS,
"Exact aspect should have strength 1.0, got {}",
conj.strength
);
}
#[test]
fn body_indices_are_correct() {
let chart_a = [pos(10.0), pos(120.0)];
let chart_b = [pos(0.0)];
let aspects = find_synastry_aspects(&chart_a, &chart_b, AspectType::MAJOR, 1.0);
let trine = aspects
.iter()
.find(|a| a.aspect_type == AspectType::Trine)
.expect("Expected a trine");
assert_eq!(trine.chart_a_body, 1);
assert_eq!(trine.chart_b_body, 0);
}
}