mod north_indian;
mod south_indian;
mod western_wheel;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlanetPosition {
pub abbreviation: String,
pub house: usize,
pub longitude_deg: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChartData {
pub planet_positions: Vec<PlanetPosition>,
pub house_cusps_deg: [f64; 12],
pub ascendant_sign_index: usize,
pub ayanamsa_deg: f64,
}
const SIGN_ABBREV: [&str; 12] = [
"Ar", "Ta", "Ge", "Cn", "Le", "Vi", "Li", "Sc", "Sg", "Cp", "Aq", "Pi",
];
#[allow(dead_code)]
const SIGN_NAMES: [&str; 12] = [
"Aries",
"Taurus",
"Gemini",
"Cancer",
"Leo",
"Virgo",
"Libra",
"Scorpio",
"Sagittarius",
"Capricorn",
"Aquarius",
"Pisces",
];
pub fn render_north_indian(data: &ChartData) -> String {
north_indian::render(data)
}
pub fn render_south_indian(data: &ChartData) -> String {
south_indian::render(data)
}
pub fn render_western_wheel(data: &ChartData) -> String {
western_wheel::render(data)
}
fn svg_escape(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for ch in s.chars() {
match ch {
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'&' => out.push_str("&"),
'"' => out.push_str("""),
'\'' => out.push_str("'"),
_ => out.push(ch),
}
}
out
}
fn safe_longitude(deg: f64) -> f64 {
if deg.is_nan() || deg.is_infinite() {
0.0
} else {
deg
}
}
fn planets_in_house(data: &ChartData, house: usize) -> Vec<&str> {
data.planet_positions
.iter()
.filter(|p| p.house == house)
.map(|p| p.abbreviation.as_str())
.collect()
}
fn sign_for_house(asc_sign: usize, house: usize) -> &'static str {
let idx = (asc_sign + house - 1) % 12;
SIGN_ABBREV[idx]
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_chart() -> ChartData {
ChartData {
planet_positions: vec![
PlanetPosition {
abbreviation: "Su".into(),
house: 1,
longitude_deg: 15.0,
},
PlanetPosition {
abbreviation: "Mo".into(),
house: 4,
longitude_deg: 100.0,
},
PlanetPosition {
abbreviation: "Ma".into(),
house: 7,
longitude_deg: 195.0,
},
PlanetPosition {
abbreviation: "Me".into(),
house: 1,
longitude_deg: 20.0,
},
PlanetPosition {
abbreviation: "Ju".into(),
house: 10,
longitude_deg: 280.0,
},
PlanetPosition {
abbreviation: "Ve".into(),
house: 2,
longitude_deg: 45.0,
},
PlanetPosition {
abbreviation: "Sa".into(),
house: 5,
longitude_deg: 135.0,
},
PlanetPosition {
abbreviation: "Ra".into(),
house: 8,
longitude_deg: 220.0,
},
PlanetPosition {
abbreviation: "Ke".into(),
house: 2,
longitude_deg: 40.0,
},
],
house_cusps_deg: [
0.0, 30.0, 60.0, 90.0, 120.0, 150.0, 180.0, 210.0, 240.0, 270.0, 300.0, 330.0,
],
ascendant_sign_index: 0,
ayanamsa_deg: 24.17,
}
}
#[test]
fn north_indian_produces_valid_svg() {
let svg = render_north_indian(&sample_chart());
assert!(svg.starts_with("<svg"), "should start with <svg");
assert!(svg.ends_with("</svg>"), "should end with </svg>");
assert!(svg.contains("Su"), "should contain Sun");
assert!(svg.contains("Mo"), "should contain Moon");
assert!(svg.contains("Ar"), "should contain Aries sign label");
}
#[test]
fn south_indian_produces_valid_svg() {
let svg = render_south_indian(&sample_chart());
assert!(svg.starts_with("<svg"), "should start with <svg");
assert!(svg.ends_with("</svg>"), "should end with </svg>");
assert!(svg.contains("Su"), "should contain Sun");
assert!(svg.contains("Pi"), "Pisces sign should appear");
}
#[test]
fn western_wheel_produces_valid_svg() {
let svg = render_western_wheel(&sample_chart());
assert!(svg.starts_with("<svg"), "should start with <svg");
assert!(svg.ends_with("</svg>"), "should end with </svg>");
assert!(svg.contains("circle"), "should contain circle elements");
assert!(svg.contains("Su"), "should contain Sun");
}
#[test]
fn planets_in_house_grouping() {
let chart = sample_chart();
let h1 = planets_in_house(&chart, 1);
assert_eq!(h1.len(), 2);
assert!(h1.contains(&"Su"));
assert!(h1.contains(&"Me"));
}
#[test]
fn sign_for_house_wraps() {
assert_eq!(sign_for_house(0, 1), "Ar"); assert_eq!(sign_for_house(0, 12), "Pi"); assert_eq!(sign_for_house(3, 1), "Cn"); assert_eq!(sign_for_house(3, 10), "Ar"); }
#[test]
fn empty_chart_no_panic() {
let empty = ChartData {
planet_positions: vec![],
house_cusps_deg: [0.0; 12],
ascendant_sign_index: 0,
ayanamsa_deg: 0.0,
};
let svg = render_north_indian(&empty);
assert!(svg.starts_with("<svg"));
let svg2 = render_south_indian(&empty);
assert!(svg2.starts_with("<svg"));
let svg3 = render_western_wheel(&empty);
assert!(svg3.starts_with("<svg"));
}
#[test]
fn all_houses_populated() {
let mut positions = Vec::new();
for h in 1..=12 {
positions.push(PlanetPosition {
abbreviation: format!("P{h}"),
house: h,
longitude_deg: (h as f64 - 1.0) * 30.0 + 15.0,
});
}
let chart = ChartData {
planet_positions: positions,
house_cusps_deg: [
0.0, 30.0, 60.0, 90.0, 120.0, 150.0, 180.0, 210.0, 240.0, 270.0, 300.0, 330.0,
],
ascendant_sign_index: 6, ayanamsa_deg: 24.17,
};
let svg = render_north_indian(&chart);
for h in 1..=12 {
assert!(
svg.contains(&format!("P{h}")),
"house {h} planet missing from SVG"
);
}
}
}