use crate::error::{JyotishError, Result};
use crate::planet::Planet;
use crate::zodiac::{Sign, tropical_sign};
use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum MotionState {
Direct,
Retrograde,
StationaryRetrograde,
StationaryDirect,
}
impl fmt::Display for MotionState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Direct => write!(f, "Direct"),
Self::Retrograde => write!(f, "Retrograde"),
Self::StationaryRetrograde => write!(f, "Stationary (Rx)"),
Self::StationaryDirect => write!(f, "Stationary (D)"),
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct Ingress {
pub planet: Planet,
pub sign: Sign,
pub jd: f64,
}
fn longitude_at(planet: Planet, jd: f64) -> Result<f64> {
match planet {
Planet::Sun => Ok(crate::sun::solar_longitude(jd)),
Planet::Moon => Ok(crate::moon::lunar_longitude(jd)),
_ => Ok(crate::planetary::compute_position(planet, jd)?.longitude_deg),
}
}
pub fn daily_motion(planet: Planet, jd: f64) -> Result<f64> {
let h = match planet {
Planet::Moon => 0.1,
_ => 1.0,
};
let lon_before = longitude_at(planet, jd - h)?;
let lon_after = longitude_at(planet, jd + h)?;
let mut diff = lon_after - lon_before;
if diff > 180.0 {
diff -= 360.0;
} else if diff < -180.0 {
diff += 360.0;
}
Ok(diff / (2.0 * h))
}
pub fn motion_state(planet: Planet, jd: f64) -> Result<MotionState> {
let motion = daily_motion(planet, jd)?;
if matches!(planet, Planet::Sun | Planet::Moon) {
return Ok(MotionState::Direct);
}
let station_threshold = 0.05;
if motion.abs() < station_threshold {
let motion_before = daily_motion(planet, jd - 5.0)?;
if motion_before > 0.0 {
Ok(MotionState::StationaryRetrograde)
} else {
Ok(MotionState::StationaryDirect)
}
} else if motion > 0.0 {
Ok(MotionState::Direct)
} else {
Ok(MotionState::Retrograde)
}
}
pub fn next_ingress(planet: Planet, jd: f64) -> Result<Ingress> {
let start_lon = longitude_at(planet, jd)?;
let start_sign = tropical_sign(start_lon).sign;
let step = match planet {
Planet::Moon => 0.5,
Planet::Sun | Planet::Mercury | Planet::Venus => 1.0,
_ => 2.0,
};
let mut jd_a = jd;
let mut jd_b = jd;
for _ in 0..2000 {
jd_b += step;
let lon_b = longitude_at(planet, jd_b)?;
let sign_b = tropical_sign(lon_b).sign;
if sign_b != start_sign {
break;
}
jd_a = jd_b;
}
if jd_b - jd > step * 2000.0 {
return Err(JyotishError::InvalidParameter(
"ingress search did not converge".into(),
));
}
for _ in 0..60 {
let jd_mid = (jd_a + jd_b) / 2.0;
let lon_mid = longitude_at(planet, jd_mid)?;
let sign_mid = tropical_sign(lon_mid).sign;
if sign_mid == start_sign {
jd_a = jd_mid;
} else {
jd_b = jd_mid;
}
if (jd_b - jd_a) < 1e-8 {
break;
}
}
let ingress_lon = longitude_at(planet, jd_b)?;
let ingress_sign = tropical_sign(ingress_lon).sign;
Ok(Ingress {
planet,
sign: ingress_sign,
jd: jd_b,
})
}
pub fn next_retrograde_station(planet: Planet, jd: f64) -> Result<f64> {
if matches!(planet, Planet::Sun | Planet::Moon) {
return Err(JyotishError::InvalidParameter(
"Sun and Moon do not go retrograde".into(),
));
}
let step = 5.0; let max_steps = 200;
let mut jd_a = jd;
let mut motion_a = daily_motion(planet, jd_a)?;
if motion_a < 0.0 {
for _ in 0..max_steps {
jd_a += step;
motion_a = daily_motion(planet, jd_a)?;
if motion_a > 0.0 {
break;
}
}
}
let mut jd_b = jd_a;
for _ in 0..max_steps {
jd_b += step;
let motion_b = daily_motion(planet, jd_b)?;
if motion_b < 0.0 {
let mut lo = jd_a;
let mut hi = jd_b;
for _ in 0..60 {
let mid = (lo + hi) / 2.0;
let m = daily_motion(planet, mid)?;
if m > 0.0 {
lo = mid;
} else {
hi = mid;
}
if (hi - lo) < 1e-6 {
break;
}
}
return Ok((lo + hi) / 2.0);
}
jd_a = jd_b;
}
Err(JyotishError::InvalidParameter(
"retrograde station search did not converge".into(),
))
}
#[cfg(test)]
mod tests {
use super::*;
const JD_J2000: f64 = 2_451_545.0;
#[test]
fn sun_daily_motion() {
let motion = daily_motion(Planet::Sun, JD_J2000).unwrap();
assert!(motion > 0.9 && motion < 1.1, "got {motion}");
}
#[test]
fn moon_daily_motion() {
let motion = daily_motion(Planet::Moon, JD_J2000).unwrap();
assert!(motion > 10.0 && motion < 16.0, "got {motion}");
}
#[test]
fn sun_always_direct() {
let state = motion_state(Planet::Sun, JD_J2000).unwrap();
assert_eq!(state, MotionState::Direct);
}
#[test]
fn moon_always_direct() {
let state = motion_state(Planet::Moon, JD_J2000).unwrap();
assert_eq!(state, MotionState::Direct);
}
#[test]
fn mars_motion_state() {
let state = motion_state(Planet::Mars, JD_J2000).unwrap();
assert!(matches!(
state,
MotionState::Direct
| MotionState::Retrograde
| MotionState::StationaryRetrograde
| MotionState::StationaryDirect
));
}
#[test]
fn sun_next_ingress() {
let ingress = next_ingress(Planet::Sun, JD_J2000).unwrap();
assert_eq!(ingress.planet, Planet::Sun);
assert!(ingress.jd > JD_J2000);
assert!(
ingress.jd - JD_J2000 < 32.0,
"took too long: {} days",
ingress.jd - JD_J2000
);
}
#[test]
fn moon_next_ingress() {
let ingress = next_ingress(Planet::Moon, JD_J2000).unwrap();
assert_eq!(ingress.planet, Planet::Moon);
assert!(
ingress.jd - JD_J2000 < 4.0,
"took too long: {} days",
ingress.jd - JD_J2000
);
}
#[test]
fn retrograde_station_sun_errors() {
assert!(next_retrograde_station(Planet::Sun, JD_J2000).is_err());
assert!(next_retrograde_station(Planet::Moon, JD_J2000).is_err());
}
#[test]
fn mars_retrograde_station() {
let station_jd = next_retrograde_station(Planet::Mars, JD_J2000).unwrap();
assert!(station_jd > JD_J2000);
assert!(station_jd - JD_J2000 < 900.0);
}
#[test]
fn motion_state_display() {
assert_eq!(MotionState::Direct.to_string(), "Direct");
assert_eq!(MotionState::Retrograde.to_string(), "Retrograde");
}
#[test]
fn motion_state_serde() {
let state = MotionState::Retrograde;
let json = serde_json::to_string(&state).unwrap();
let restored: MotionState = serde_json::from_str(&json).unwrap();
assert_eq!(restored, state);
}
}