#[cfg(not(feature = "std"))]
use crate::common::FloatExt;
use crate::common::{cos_deg, sin_deg};
pub(crate) const RD_EPOCH_JDN: i64 = 1_721_425;
const MEAN_SYNODIC_MONTH: f64 = 29.530_588_861;
const MEAN_TROPICAL_YEAR: f64 = 365.242_189;
const J2000: f64 = 730_120.5;
const WINTER_SOLSTICE: f64 = 270.0;
const CHINESE_EPOCH: f64 = -963_099.0;
const CHINESE_EPOCH_GREGORIAN_YEAR: i64 = -2636;
fn julian_centuries(t: f64) -> f64 {
(dynamical_from_universal(t) - J2000) / 36525.0
}
fn poly(x: f64, a: &[f64]) -> f64 {
a.iter().rev().fold(0.0, |acc, c| acc * x + c)
}
fn amod_f(x: f64, y: f64) -> f64 {
x - y * (x / y).floor()
}
fn amod(x: i64, y: i64) -> i64 {
let m = x.rem_euclid(y);
if m == 0 { y } else { m }
}
fn ephemeris_correction(t: f64) -> f64 {
let year = gregorian_year_from_rd(t.floor() as i64);
let y = year as f64;
let c = (rd_from_gregorian(year, 7, 1) - rd_from_gregorian(1900, 1, 1)) as f64 / 36525.0;
let seconds = if (2051..=2150).contains(&year) {
-20.0 + 32.0 * ((y - 1820.0) / 100.0).powi(2) - 0.5628 * (2150.0 - y)
} else if (2006..=2050).contains(&year) {
poly(y - 2000.0, &[62.92, 0.32217, 0.005589])
} else if (1987..=2005).contains(&year) {
poly(
y - 2000.0,
&[
63.86,
0.3345,
-0.060_374,
0.001_727_5,
0.000_651_814,
0.000_023_73,
],
)
} else if (1900..=1986).contains(&year) {
poly(
c,
&[
-0.00002, 0.000297, 0.025184, -0.181133, 0.553040, -0.861938, 0.677066, -0.212591,
],
) * 86400.0
} else if (1800..=1899).contains(&year) {
poly(
c,
&[
-0.000009, 0.003844, 0.083563, 0.865736, 4.867575, 15.845535, 31.332267, 38.291999,
28.316289, 11.636204, 2.043794,
],
) * 86400.0
} else if (1700..=1799).contains(&year) {
poly(
y - 1700.0,
&[8.118780842, -0.005092142, 0.003336121, -0.0000266484],
)
} else if (1600..=1699).contains(&year) {
poly(y - 1600.0, &[120.0, -0.9808, -0.01532, 0.000140272128])
} else if (500..=1599).contains(&year) {
poly(
(y - 1000.0) / 100.0,
&[
1574.2,
-556.01,
71.23472,
0.319781,
-0.8503463,
-0.005050998,
0.0083572073,
],
)
} else if (-499..500).contains(&year) {
poly(
y / 100.0,
&[
10583.6,
-1014.41,
33.78311,
-5.952053,
-0.1798452,
0.022174192,
0.0090316521,
],
)
} else {
-20.0 + 32.0 * ((y - 1820.0) / 100.0).powi(2)
};
seconds / 86400.0
}
fn dynamical_from_universal(t: f64) -> f64 {
t + ephemeris_correction(t)
}
fn universal_from_dynamical(t: f64) -> f64 {
t - ephemeris_correction(t)
}
#[rustfmt::skip]
const SOLAR_TERMS: [(f64, f64, f64); 49] = [
(403406.0, 270.54861, 0.9287892), (195207.0, 340.19128, 35999.1376),
(119433.0, 63.91854, 35999.4089), (112392.0, 331.2622, 35998.7155),
(3891.0, 317.843, 71998.20), (2819.0, 86.631, 71998.4380),
(1721.0, 240.052, 36000.35726), (660.0, 310.26, 71997.4812),
(350.0, 247.23, 32964.4678), (334.0, 260.87, -19.4410),
(314.0, 297.82, 445267.1117), (268.0, 343.14, 45036.8840),
(242.0, 166.79, 3.1008), (234.0, 81.53, 22518.4434),
(158.0, 3.50, -19.9739), (132.0, 132.75, 65928.9345),
(129.0, 182.95, 9038.0293), (114.0, 162.03, 3034.7684),
(99.0, 29.8, 33718.148), (93.0, 266.4, 3034.448),
(86.0, 249.2, -2280.773), (78.0, 157.6, 29929.992),
(72.0, 257.8, 31556.493), (68.0, 185.1, 149.588),
(64.0, 69.9, 9037.750), (46.0, 8.0, 107997.405),
(38.0, 197.1, -4444.176), (37.0, 250.4, 151.771),
(32.0, 65.3, 67555.316), (29.0, 162.7, 31556.080),
(28.0, 341.5, -4561.540), (27.0, 291.6, 107996.706),
(27.0, 98.5, 1221.655), (25.0, 146.7, 62894.167),
(24.0, 110.0, 31437.369), (21.0, 5.2, 14578.298),
(21.0, 342.6, -31931.757), (20.0, 230.9, 34777.243),
(18.0, 256.1, 1221.999), (17.0, 45.3, 62894.511),
(14.0, 242.9, -4442.039), (13.0, 115.2, 107997.909),
(13.0, 151.8, 119.066), (13.0, 285.3, 16859.071),
(12.0, 53.3, -4.578), (10.0, 126.6, 26895.292),
(10.0, 205.7, -39.127), (10.0, 85.9, 12297.536),
(10.0, 146.1, 90073.778),
];
fn solar_longitude(t: f64) -> f64 {
let c = julian_centuries(t);
let sum: f64 = SOLAR_TERMS
.iter()
.map(|&(x, y, z)| x * sin_deg(y + z * c))
.sum();
let longitude = 282.7771834 + 36_000.769_537_44 * c + 0.000_005_729_577_951_308_232 * sum;
amod_f(longitude + aberration(c) + nutation(c), 360.0)
}
fn nutation(c: f64) -> f64 {
let a = poly(c, &[124.90, -1934.134, 0.002063]);
let b = poly(c, &[201.11, 72001.5377, 0.00057]);
-0.004778 * sin_deg(a) - 0.0003667 * sin_deg(b)
}
fn aberration(c: f64) -> f64 {
0.0000974 * cos_deg(177.63 + 35999.01848 * c) - 0.005575
}
#[rustfmt::skip]
const NEW_MOON_TERMS: [(f64, i32, f64, f64, f64); 24] = [
(-0.40720, 0, 0.0, 1.0, 0.0), (0.17241, 1, 1.0, 0.0, 0.0),
(0.01608, 0, 0.0, 2.0, 0.0), (0.01039, 0, 0.0, 0.0, 2.0),
(0.00739, 1, -1.0, 1.0, 0.0), (-0.00514, 1, 1.0, 1.0, 0.0),
(0.00208, 2, 2.0, 0.0, 0.0), (-0.00111, 0, 0.0, 1.0, -2.0),
(-0.00057, 0, 0.0, 1.0, 2.0), (0.00056, 1, 1.0, 2.0, 0.0),
(-0.00042, 0, 0.0, 3.0, 0.0), (0.00042, 1, 1.0, 0.0, 2.0),
(0.00038, 1, 1.0, 0.0, -2.0), (-0.00024, 1, -1.0, 2.0, 0.0),
(-0.00017, 0, 0.0, 0.0, 1.0), (-0.00007, 0, 2.0, 1.0, 0.0),
(0.00004, 0, 0.0, 2.0, -2.0), (0.00004, 0, 3.0, 0.0, 0.0),
(0.00003, 0, 1.0, 1.0, -2.0), (0.00003, 0, 0.0, 2.0, 2.0),
(-0.00003, 0, 1.0, 1.0, 2.0), (0.00003, 0, -1.0, 1.0, 2.0),
(-0.00002, 0, -1.0, 1.0, -2.0), (0.00002, 0, 1.0, 3.0, 0.0),
];
#[rustfmt::skip]
const NEW_MOON_EXTRA: [(f64, f64, f64); 13] = [
(251.88, 0.016321, 0.000165), (251.83, 26.651886, 0.000164),
(349.42, 36.412478, 0.000126), (84.66, 18.206239, 0.000110),
(141.74, 53.303771, 0.000062), (207.14, 2.453732, 0.000060),
(154.84, 7.306860, 0.000056), (34.52, 27.261239, 0.000047),
(207.19, 0.121824, 0.000042), (291.34, 1.844379, 0.000040),
(161.72, 24.198154, 0.000037), (239.56, 25.513099, 0.000035),
(331.55, 3.592518, 0.000023),
];
fn nth_new_moon(n: i64) -> f64 {
let k = (n - 24724) as f64;
let c = k / 1236.85;
let approx = J2000
+ poly(
c,
&[
5.09766,
MEAN_SYNODIC_MONTH * 1236.85,
0.000_154_37,
-0.000_000_150,
0.000_000_000_73,
],
);
let e = poly(c, &[1.0, -0.002516, -0.0000074]);
let solar = poly(
c,
&[2.5534, 1236.85 * 29.105_356_70, -0.0000014, -0.00000011],
);
let lunar = poly(
c,
&[
201.5643,
385.816_935_28 * 1236.85,
0.0107582,
0.00001238,
-0.000_000_058,
],
);
let arg = poly(
c,
&[
160.7108,
390.670_502_84 * 1236.85,
-0.0016118,
-0.00000227,
0.000_000_011,
],
);
let omega = poly(
c,
&[124.7746, -1.563_755_88 * 1236.85, 0.0020672, 0.00000215],
);
let correction: f64 = -0.00017 * sin_deg(omega)
+ NEW_MOON_TERMS
.iter()
.map(|&(v, w, x, y, z)| v * e.powi(w) * sin_deg(x * solar + y * lunar + z * arg))
.sum::<f64>();
let extra = 0.000325 * sin_deg(poly(c, &[299.77, 132.8475848, -0.009173]));
let additional: f64 = NEW_MOON_EXTRA
.iter()
.map(|&(i, j, l)| l * sin_deg(i + j * k))
.sum();
universal_from_dynamical(approx + correction + extra + additional)
}
fn new_moon_index_before(t: f64) -> i64 {
let mut n = ((t - nth_new_moon(0)) / MEAN_SYNODIC_MONTH).round() as i64;
for _ in 0..8 {
let step = ((t - nth_new_moon(n)) / MEAN_SYNODIC_MONTH).round() as i64;
if step == 0 {
break;
}
n += step;
}
for _ in 0..8 {
if nth_new_moon(n) > t {
n -= 1;
} else if nth_new_moon(n + 1) <= t {
n += 1;
} else {
break;
}
}
n
}
fn new_moon_before(t: f64) -> f64 {
let n = new_moon_index_before(t);
let m = nth_new_moon(n);
if m < t { m } else { nth_new_moon(n - 1) }
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) enum Lunisolar {
Chinese,
Dangi,
}
fn zone_offset(cal: Lunisolar, t: f64) -> f64 {
let day = t.floor() as i64;
let hours = match cal {
Lunisolar::Chinese => {
if gregorian_year_from_rd(day) < 1929 {
1397.0 / 180.0
} else {
8.0
}
}
Lunisolar::Dangi => {
if day < rd_from_gregorian(1908, 4, 1) {
3809.0 / 450.0
} else if day < rd_from_gregorian(1912, 1, 1) {
8.5
} else if day < rd_from_gregorian(1954, 3, 21) {
9.0
} else if day < rd_from_gregorian(1961, 8, 10) {
8.5
} else {
9.0
}
}
};
hours / 24.0
}
fn midnight(cal: Lunisolar, day: i64) -> f64 {
day as f64 - zone_offset(cal, day as f64)
}
fn local_day(cal: Lunisolar, t: f64) -> i64 {
(t + zone_offset(cal, t)).floor() as i64
}
fn winter_solstice_on_or_before(cal: Lunisolar, day: i64) -> i64 {
let t = midnight(cal, day + 1);
let rate = MEAN_TROPICAL_YEAR / 360.0;
let mut tau = t - rate * amod_f(solar_longitude(t) - WINTER_SOLSTICE, 360.0);
for _ in 0..6 {
let delta = amod_f(solar_longitude(tau) - WINTER_SOLSTICE + 180.0, 360.0) - 180.0;
if delta.abs() < 1e-6 {
break;
}
tau -= rate * delta;
}
let past = |d: i64| solar_longitude(midnight(cal, d)) > WINTER_SOLSTICE;
let mut d = local_day(cal, tau);
for _ in 0..8 {
if past(d) {
d -= 1;
} else if !past(d + 1) {
d += 1;
} else {
break;
}
}
d.min(day)
}
fn local_day_of_moon(cal: Lunisolar, n: i64) -> i64 {
local_day(cal, nth_new_moon(n))
}
fn new_moon_day_before(cal: Lunisolar, day: i64) -> i64 {
local_day(cal, new_moon_before(midnight(cal, day)))
}
fn current_major_solar_term(cal: Lunisolar, day: i64) -> i64 {
let s = solar_longitude(midnight(cal, day));
amod(2 + (s / 30.0).floor() as i64, 12)
}
#[derive(Clone, Copy)]
struct SuiMonth {
start: i64,
end: i64,
number: i64,
leap: bool,
}
fn months_of_sui(cal: Lunisolar, day: i64) -> ([SuiMonth; 14], usize) {
if let Some(hit) = sui_cache::get(cal, day) {
return hit;
}
let (s1, s2, months, len) = months_of_sui_uncached(cal, day);
sui_cache::put(cal, s1, s2, months, len);
(months, len)
}
#[cfg(feature = "std")]
mod sui_cache {
use super::{Lunisolar, SuiMonth};
use std::cell::RefCell;
type Entry = (i64, i64, [SuiMonth; 14], usize);
std::thread_local! {
static SLOTS: RefCell<[Option<Entry>; 2]> = const { RefCell::new([None, None]) };
}
fn slot(cal: Lunisolar) -> usize {
usize::from(cal == Lunisolar::Dangi)
}
pub(super) fn get(cal: Lunisolar, day: i64) -> Option<([SuiMonth; 14], usize)> {
SLOTS.with(|s| {
let s = s.borrow();
let (s1, s2, months, len) = s[slot(cal)].as_ref()?;
(*s1..*s2).contains(&day).then_some((*months, *len))
})
}
pub(super) fn put(cal: Lunisolar, s1: i64, s2: i64, months: [SuiMonth; 14], len: usize) {
SLOTS.with(|s| s.borrow_mut()[slot(cal)] = Some((s1, s2, months, len)));
}
}
#[cfg(not(feature = "std"))]
mod sui_cache {
use super::{Lunisolar, SuiMonth};
pub(super) fn get(_: Lunisolar, _: i64) -> Option<([SuiMonth; 14], usize)> {
None
}
pub(super) fn put(_: Lunisolar, _: i64, _: i64, _: [SuiMonth; 14], _: usize) {}
}
fn months_of_sui_uncached(cal: Lunisolar, day: i64) -> (i64, i64, [SuiMonth; 14], usize) {
let s1 = winter_solstice_on_or_before(cal, day);
let s2 = winter_solstice_on_or_before(cal, s1 + 370);
let mut moon = new_moon_index_before(midnight(cal, s1 + 1)) + 1;
let m12 = local_day_of_moon(cal, moon);
let next_m11 = new_moon_day_before(cal, s2 + 1);
let leap_sui = ((next_m11 - m12) as f64 / MEAN_SYNODIC_MONTH).round() as i64 == 12;
let mut months = [SuiMonth {
start: 0,
end: 0,
number: 0,
leap: false,
}; 14];
let mut len = 0usize;
let mut start = m12;
let mut term = current_major_solar_term(cal, start);
let mut leap_seen = false;
let mut index = 0i64;
while len < 14 {
moon += 1;
let next = local_day_of_moon(cal, moon);
let next_term = current_major_solar_term(cal, next);
let no_term = leap_sui && term == next_term;
let is_leap = no_term && !leap_seen;
leap_seen |= no_term;
months[len] = SuiMonth {
start,
end: next,
number: amod(index - i64::from(leap_seen), 12),
leap: is_leap,
};
len += 1;
if start >= next_m11 {
break;
}
start = next;
term = next_term;
index += 1;
}
(s1, s2, months, len)
}
fn new_year_of(months: &[SuiMonth; 14], len: usize) -> i64 {
(0..len)
.find(|&i| months[i].number == 1 && !months[i].leap)
.map_or(months[0].start, |i| months[i].start)
}
fn sui_of_year_containing(cal: Lunisolar, day: i64) -> ([SuiMonth; 14], usize, i64) {
let (months, len) = months_of_sui(cal, day);
let ny = new_year_of(&months, len);
if day >= ny {
return (months, len, ny);
}
let (months, len) = months_of_sui(cal, day - 180);
let ny = new_year_of(&months, len);
(months, len, ny)
}
fn related_gregorian_year(day: i64, month: i64) -> i64 {
let elapsed_years = (1.5 - month as f64 / 12.0
+ (day as f64 - CHINESE_EPOCH) / MEAN_TROPICAL_YEAR)
.floor() as i64;
elapsed_years + CHINESE_EPOCH_GREGORIAN_YEAR - 1
}
pub(crate) struct LunisolarDate {
pub year: i64,
pub month: i64,
pub day: i64,
pub leap: bool,
}
fn month_containing(cal: Lunisolar, day: i64) -> SuiMonth {
let (months, len) = months_of_sui(cal, day);
if let Some(i) = (0..len).rev().find(|&i| months[i].start <= day) {
return months[i];
}
let (prev, prev_len) = months_of_sui(cal, months[0].start - 40);
let i = (0..prev_len)
.rev()
.find(|&i| prev[i].start <= day)
.unwrap_or(prev_len - 1);
prev[i]
}
fn from_rd(cal: Lunisolar, day: i64) -> LunisolarDate {
let m = month_containing(cal, day);
LunisolarDate {
year: related_gregorian_year(day, m.number),
month: m.number,
day: day - m.start + 1,
leap: m.leap,
}
}
fn year_start(cal: Lunisolar, year: i64) -> Option<([SuiMonth; 14], usize, i64)> {
if let Some(hit) = year_cache::get(cal, year) {
return Some(hit);
}
let computed = year_start_uncached(cal, year)?;
year_cache::put(cal, year, computed);
Some(computed)
}
#[cfg(feature = "std")]
mod year_cache {
use super::{Lunisolar, SuiMonth};
use alloc::vec;
use alloc::vec::Vec;
use std::cell::RefCell;
type Entry = (bool, i64, [SuiMonth; 14], usize, i64);
const SLOTS: usize = 256;
std::thread_local! {
static TABLE: RefCell<Vec<Option<Entry>>> = RefCell::new(vec![None; SLOTS]);
}
#[allow(clippy::cast_sign_loss)]
fn index(year: i64) -> usize {
(year.rem_euclid(SLOTS as i64)) as usize
}
pub(super) fn get(cal: Lunisolar, year: i64) -> Option<([SuiMonth; 14], usize, i64)> {
TABLE.with(|t| {
let t = t.borrow();
let (is_dangi, y, months, len, ny) = t[index(year)].as_ref()?;
(*is_dangi == (cal == Lunisolar::Dangi) && *y == year).then_some((*months, *len, *ny))
})
}
pub(super) fn put(cal: Lunisolar, year: i64, v: ([SuiMonth; 14], usize, i64)) {
TABLE.with(|t| {
t.borrow_mut()[index(year)] = Some((cal == Lunisolar::Dangi, year, v.0, v.1, v.2));
});
}
}
#[cfg(not(feature = "std"))]
mod year_cache {
use super::{Lunisolar, SuiMonth};
pub(super) fn get(_: Lunisolar, _: i64) -> Option<([SuiMonth; 14], usize, i64)> {
None
}
pub(super) fn put(_: Lunisolar, _: i64, _: ([SuiMonth; 14], usize, i64)) {}
}
fn year_start_uncached(cal: Lunisolar, year: i64) -> Option<([SuiMonth; 14], usize, i64)> {
let mut anchor = rd_from_gregorian(year, 12, 31);
let (mut months, mut len, mut new_year) = sui_of_year_containing(cal, anchor);
for _ in 0..4 {
let reported = related_gregorian_year(new_year, 1);
if reported == year {
return Some((months, len, new_year));
}
anchor += (year - reported) * 366;
(months, len, new_year) = sui_of_year_containing(cal, anchor);
}
Some((months, len, new_year))
}
fn to_rd(cal: Lunisolar, year: i64, month: i64, day: i64, leap: bool) -> Option<i64> {
if !(1..=12).contains(&month) || !(1..=30).contains(&day) {
return None;
}
let (months, len, new_year) = year_start(cal, year)?;
let (next_months, next_len) = months_of_sui(cal, new_year + 400);
for (ms, l) in [(&months, len), (&next_months, next_len)] {
for m in ms.iter().take(l) {
if m.number != month || m.leap != leap || m.start < new_year {
continue;
}
if related_gregorian_year(m.start, m.number) != year {
continue;
}
if day > m.end - m.start {
return None; }
return Some(m.start + day - 1);
}
}
None
}
#[must_use]
pub(crate) fn leap_month_of_year(cal: Lunisolar, year: i64) -> i64 {
if year.unsigned_abs() > 1 << 40 {
return 0;
}
let Some((months, len, new_year)) = year_start(cal, year) else {
return 0;
};
for (ms, l) in [(months, len), {
let (m, l) = months_of_sui(cal, new_year + 400);
(m, l)
}] {
for m in ms.iter().take(l) {
if m.leap && m.start >= new_year && related_gregorian_year(m.start, m.number) == year {
return m.number;
}
}
}
0
}
#[must_use]
pub(crate) fn from_jdn(cal: Lunisolar, jdn: i64) -> Option<LunisolarDate> {
let day = jdn.checked_sub(RD_EPOCH_JDN)?;
if day.unsigned_abs() > 1 << 52 {
return None;
}
Some(from_rd(cal, day))
}
#[must_use]
pub(crate) fn to_jdn(cal: Lunisolar, year: i64, month: i64, day: i64, leap: bool) -> Option<i64> {
if year.unsigned_abs() > 1 << 40 {
return None;
}
to_rd(cal, year, month, day, leap).map(|d| d + RD_EPOCH_JDN)
}
fn rd_from_gregorian(year: i64, month: i64, day: i64) -> i64 {
let a = (14 - month).div_euclid(12);
let y = year + 4800 - a;
let m = month + 12 * a - 3;
let jdn = day + (153 * m + 2).div_euclid(5) + 365 * y + y.div_euclid(4) - y.div_euclid(100)
+ y.div_euclid(400)
- 32045;
jdn - RD_EPOCH_JDN
}
fn gregorian_year_from_rd(day: i64) -> i64 {
let a = day + RD_EPOCH_JDN + 32044;
let b = (4 * a + 3).div_euclid(146097);
let c = a - (146097 * b).div_euclid(4);
let d = (4 * c + 3).div_euclid(1461);
let e = c - (1461 * d).div_euclid(4);
let m = (5 * e + 2).div_euclid(153);
100 * b + d - 4800 + m.div_euclid(10)
}
#[cfg(test)]
mod tests;