use crate::calendar::julian_centuries;
use crate::coords::normalize_degrees;
use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Sign {
Aries,
Taurus,
Gemini,
Cancer,
Leo,
Virgo,
Libra,
Scorpio,
Sagittarius,
Capricorn,
Aquarius,
Pisces,
}
pub const SIGNS: [Sign; 12] = [
Sign::Aries,
Sign::Taurus,
Sign::Gemini,
Sign::Cancer,
Sign::Leo,
Sign::Virgo,
Sign::Libra,
Sign::Scorpio,
Sign::Sagittarius,
Sign::Capricorn,
Sign::Aquarius,
Sign::Pisces,
];
impl Sign {
pub fn index(self) -> usize {
match self {
Self::Aries => 0,
Self::Taurus => 1,
Self::Gemini => 2,
Self::Cancer => 3,
Self::Leo => 4,
Self::Virgo => 5,
Self::Libra => 6,
Self::Scorpio => 7,
Self::Sagittarius => 8,
Self::Capricorn => 9,
Self::Aquarius => 10,
Self::Pisces => 11,
}
}
pub fn cusp_longitude(self) -> f64 {
self.index() as f64 * 30.0
}
pub fn element(self) -> Element {
match self.index() % 4 {
0 => Element::Fire,
1 => Element::Earth,
2 => Element::Air,
_ => Element::Water,
}
}
pub fn modality(self) -> Modality {
match self.index() % 3 {
0 => Modality::Cardinal,
1 => Modality::Fixed,
_ => Modality::Mutable,
}
}
}
impl fmt::Display for Sign {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let name = match self {
Self::Aries => "Aries",
Self::Taurus => "Taurus",
Self::Gemini => "Gemini",
Self::Cancer => "Cancer",
Self::Leo => "Leo",
Self::Virgo => "Virgo",
Self::Libra => "Libra",
Self::Scorpio => "Scorpio",
Self::Sagittarius => "Sagittarius",
Self::Capricorn => "Capricorn",
Self::Aquarius => "Aquarius",
Self::Pisces => "Pisces",
};
write!(f, "{name}")
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Element {
Fire,
Earth,
Air,
Water,
}
impl fmt::Display for Element {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Fire => write!(f, "Fire"),
Self::Earth => write!(f, "Earth"),
Self::Air => write!(f, "Air"),
Self::Water => write!(f, "Water"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Modality {
Cardinal,
Fixed,
Mutable,
}
impl fmt::Display for Modality {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Cardinal => write!(f, "Cardinal"),
Self::Fixed => write!(f, "Fixed"),
Self::Mutable => write!(f, "Mutable"),
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct SignPosition {
pub sign: Sign,
pub degrees_in_sign: f64,
}
pub fn tropical_sign(longitude_deg: f64) -> SignPosition {
let lon = normalize_degrees(longitude_deg);
let index = (lon / 30.0) as usize;
let index = index.min(11); SignPosition {
sign: SIGNS[index],
degrees_in_sign: lon - index as f64 * 30.0,
}
}
pub fn sidereal_sign(longitude_deg: f64, jd: f64) -> SignPosition {
let sidereal_lon = normalize_degrees(longitude_deg - lahiri_ayanamsa(jd));
tropical_sign(sidereal_lon)
}
pub fn lahiri_ayanamsa(jd: f64) -> f64 {
let t = julian_centuries(jd);
23.85 + 1.396_971 * t + 0.000_308_6 * t * t
}
pub fn tropical_to_sidereal(longitude_deg: f64, jd: f64) -> f64 {
normalize_degrees(longitude_deg - lahiri_ayanamsa(jd))
}
pub fn sidereal_to_tropical(longitude_deg: f64, jd: f64) -> f64 {
normalize_degrees(longitude_deg + lahiri_ayanamsa(jd))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tropical_sign_boundaries() {
for (i, &sign) in SIGNS.iter().enumerate() {
let lon = i as f64 * 30.0 + 0.001;
let pos = tropical_sign(lon);
assert_eq!(pos.sign, sign, "wrong sign at {lon}°");
}
}
#[test]
fn tropical_sign_degrees_in_sign() {
let pos = tropical_sign(45.0);
assert_eq!(pos.sign, Sign::Taurus);
assert!((pos.degrees_in_sign - 15.0).abs() < 1e-10);
}
#[test]
fn tropical_sign_zero() {
let pos = tropical_sign(0.0);
assert_eq!(pos.sign, Sign::Aries);
assert!(pos.degrees_in_sign.abs() < 1e-10);
}
#[test]
fn tropical_sign_near_360() {
let pos = tropical_sign(359.99);
assert_eq!(pos.sign, Sign::Pisces);
}
#[test]
fn sidereal_sign_j2000() {
let pos = sidereal_sign(45.0, 2_451_545.0);
assert_eq!(pos.sign, Sign::Aries);
}
#[test]
fn lahiri_ayanamsa_j2000() {
let aya = lahiri_ayanamsa(2_451_545.0);
assert!((aya - 23.85).abs() < 0.1, "got {aya}");
}
#[test]
fn lahiri_ayanamsa_increases() {
let aya_2000 = lahiri_ayanamsa(2_451_545.0);
let aya_2100 = lahiri_ayanamsa(2_451_545.0 + 36525.0);
assert!(aya_2100 > aya_2000, "ayanamsa should increase over time");
}
#[test]
fn tropical_sidereal_roundtrip() {
let jd = 2_451_545.0;
let lon = 123.456;
let sid = tropical_to_sidereal(lon, jd);
let restored = sidereal_to_tropical(sid, jd);
assert!((restored - lon).abs() < 1e-10);
}
#[test]
fn sign_properties() {
assert_eq!(Sign::Aries.element(), Element::Fire);
assert_eq!(Sign::Aries.modality(), Modality::Cardinal);
assert_eq!(Sign::Taurus.element(), Element::Earth);
assert_eq!(Sign::Taurus.modality(), Modality::Fixed);
assert_eq!(Sign::Gemini.element(), Element::Air);
assert_eq!(Sign::Gemini.modality(), Modality::Mutable);
assert_eq!(Sign::Cancer.element(), Element::Water);
assert_eq!(Sign::Cancer.modality(), Modality::Cardinal);
}
#[test]
fn sign_display() {
assert_eq!(Sign::Sagittarius.to_string(), "Sagittarius");
assert_eq!(Sign::Pisces.to_string(), "Pisces");
}
#[test]
fn sign_cusp_longitudes() {
assert!((Sign::Aries.cusp_longitude() - 0.0).abs() < 1e-10);
assert!((Sign::Cancer.cusp_longitude() - 90.0).abs() < 1e-10);
assert!((Sign::Libra.cusp_longitude() - 180.0).abs() < 1e-10);
assert!((Sign::Capricorn.cusp_longitude() - 270.0).abs() < 1e-10);
}
#[test]
fn sign_serde_roundtrip() {
let sign = Sign::Scorpio;
let json = serde_json::to_string(&sign).unwrap();
let restored: Sign = serde_json::from_str(&json).unwrap();
assert_eq!(restored, sign);
}
}