use crate::delta_t::delta_t_for_year;
use crate::julian::{jd_from_ymd, ymd_from_jd};
use crate::solar_ecliptic_state;
pub const SOLAR_TERM_LONGITUDES: [f64; 24] = [
285.0, 300.0, 315.0, 330.0, 345.0, 0.0, 15.0, 30.0, 45.0, 60.0, 75.0, 90.0, 105.0, 120.0,
135.0, 150.0, 165.0, 180.0, 195.0, 210.0, 225.0, 240.0, 255.0, 270.0,
];
#[rustfmt::skip]
pub const SOLAR_TERM_NAMES: [&str; 24] = [
"小寒", "大寒", "立春", "雨水", "驚蟄", "春分", "清明", "穀雨", "立夏", "小滿", "芒種", "夏至",
"小暑", "大暑", "立秋", "處暑", "白露", "秋分", "寒露", "霜降", "立冬", "小雪", "大雪", "冬至",
];
#[must_use]
pub fn solar_term_for_longitude(deg: f64) -> &'static str {
let idx = ((deg - SOLAR_TERM_LONGITUDES[0]).rem_euclid(360.0) / 15.0).floor() as usize % 24;
SOLAR_TERM_NAMES[idx]
}
fn normalize_degrees(d: f64) -> f64 {
d.rem_euclid(360.0)
}
fn crosses_target(lon1: f64, lon2: f64, target: f64) -> bool {
let lon1 = normalize_degrees(lon1);
let lon2 = normalize_degrees(lon2);
let target = normalize_degrees(target);
let forward = normalize_degrees(lon2 - lon1);
if forward > 180.0 {
return false;
}
let to_target = normalize_degrees(target - lon1);
to_target <= forward
}
fn apparent_longitude_ut(jd_ut: f64, dt: fn(f64) -> f64) -> f64 {
let (y, m, _) = ymd_from_jd(jd_ut);
let decimal_year = f64::from(y) + (f64::from(m) - 0.5) / 12.0;
let jd_tt = jd_ut + dt(decimal_year) / 86400.0;
solar_ecliptic_state(jd_tt).apparent_longitude_degrees
}
#[must_use]
pub(crate) fn find_sun_longitude_moment_dt(
target_longitude: f64,
start_jd_ut: f64,
search_days: u32,
dt: fn(f64) -> f64,
) -> Option<f64> {
let target = normalize_degrees(target_longitude);
let mut prev_lon = apparent_longitude_ut(start_jd_ut, dt);
let mut bracket: Option<(f64, f64)> = None;
for d in 1..=search_days {
let current = start_jd_ut + f64::from(d);
let current_lon = apparent_longitude_ut(current, dt);
if crosses_target(prev_lon, current_lon, target) {
bracket = Some((start_jd_ut + f64::from(d - 1), current));
break;
}
prev_lon = current_lon;
}
let (mut lo, mut hi) = bracket?;
let one_second = 1.0 / 86400.0;
while hi - lo > one_second {
let mid = lo + (hi - lo) / 2.0;
if crosses_target(
apparent_longitude_ut(lo, dt),
apparent_longitude_ut(mid, dt),
target,
) {
hi = mid;
} else {
lo = mid;
}
}
Some(lo + (hi - lo) / 2.0)
}
#[must_use]
pub fn find_solar_term_moment(target_longitude: f64, year: i32, start_month: u32) -> Option<f64> {
find_solar_term_moment_dt(target_longitude, year, start_month, delta_t_for_year)
}
pub(crate) fn find_solar_term_moment_dt(
target_longitude: f64,
year: i32,
start_month: u32,
dt: fn(f64) -> f64,
) -> Option<f64> {
let start = jd_from_ymd(year, start_month, 1);
find_sun_longitude_moment_dt(target_longitude, start, 120, dt)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::julian::ymd_from_jd;
fn beijing(jd_ut: f64) -> (i32, u32, u32) {
ymd_from_jd(jd_ut + 8.0 / 24.0)
}
#[test]
fn winter_solstice_and_spring_start() {
assert_eq!(
beijing(find_solar_term_moment(270.0, 2024, 12).unwrap()),
(2024, 12, 21)
);
assert_eq!(
beijing(find_solar_term_moment(270.0, 2023, 12).unwrap()),
(2023, 12, 22)
);
assert_eq!(
beijing(find_solar_term_moment(315.0, 2024, 1).unwrap()),
(2024, 2, 4)
);
}
#[test]
fn no_crossing_returns_none() {
assert!(
find_sun_longitude_moment_dt(270.0, jd_from_ymd(2024, 1, 1), 3, delta_t_for_year)
.is_none()
);
}
}