1use serde::{Deserialize, Serialize};
14
15use crate::constants::TAU;
16use crate::ephemeris::observe::{apparent, ecliptic_latlon, observe};
17use crate::ephemeris::KernelSet;
18use crate::error::EngineError;
19use crate::frames::nutation::iau2000b_radians;
20use crate::frames::{mxv, Orientation};
21use crate::instant::UtcInstant;
22use crate::planets::require_kernel;
23use crate::time::Time;
24
25const SUN: i32 = 10;
26const MOON: i32 = 301;
27const TROPICAL_YEAR: f64 = 365.242_19;
28const SYNODIC_MONTH: f64 = 29.530_589;
29const SUN_RATE: f64 = 360.0 / TROPICAL_YEAR;
31const ELONGATION_RATE: f64 = 360.0 / SYNODIC_MONTH;
33const EPSILON_DAYS: f64 = 1e-7;
35const UNIX_EPOCH_JD: f64 = 2_440_587.5;
37const UNIX_EPOCH_JDN: i64 = 2_440_588;
39const MICROS_PER_DAY: i64 = 86_400_000_000;
40const DATE_MARGIN: i64 = 3;
43
44const TERM_NAMES: [&str; 24] = [
46 "chunfen",
47 "qingming",
48 "guyu",
49 "lixia",
50 "xiaoman",
51 "mangzhong",
52 "xiazhi",
53 "xiaoshu",
54 "dashu",
55 "liqiu",
56 "chushu",
57 "bailu",
58 "qiufen",
59 "hanlu",
60 "shuangjiang",
61 "lidong",
62 "xiaoxue",
63 "daxue",
64 "dongzhi",
65 "xiaohan",
66 "dahan",
67 "lichun",
68 "yushui",
69 "jingzhe",
70];
71
72pub(crate) const LICHUN: u16 = 315;
74
75#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
77pub struct SolarTerm {
78 pub name: String,
80 pub longitude: u16,
82 pub jie: bool,
85 pub utc: String,
87}
88
89#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
91pub struct LunarMonth {
92 pub year: i32,
94 pub month: u8,
96 pub leap: bool,
98 pub start: String,
100 pub days: u8,
102}
103
104#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
107pub struct ChineseCalendar {
108 pub equation_of_time: f64,
110 pub solar_terms: Vec<SolarTerm>,
113 pub lunar_months: Vec<LunarMonth>,
115}
116
117impl SolarTerm {
118 pub(crate) fn instant(&self) -> Result<UtcInstant, EngineError> {
120 UtcInstant::parse(&self.utc).map_err(|e| EngineError::InvalidInput(e.to_string()))
121 }
122}
123
124impl LunarMonth {
125 pub(crate) fn start_day(&self) -> Result<i64, EngineError> {
127 let start = UtcInstant::parse(&format!("{}T00:00:00Z", self.start))
128 .map_err(|e| EngineError::InvalidInput(e.to_string()))?;
129 Ok(day_number(start))
130 }
131}
132
133pub(crate) fn day_number(instant: UtcInstant) -> i64 {
135 instant.micros().div_euclid(MICROS_PER_DAY) + UNIX_EPOCH_JDN
136}
137
138fn date_of(day: i64) -> String {
140 let (y, m, d, ..) = UtcInstant::from_micros((day - UNIX_EPOCH_JDN) * MICROS_PER_DAY).civil();
141 format!("{y:04}-{m:02}-{d:02}")
142}
143
144fn instant_of(jd: f64) -> UtcInstant {
146 let seconds = ((jd - UNIX_EPOCH_JD) * 86_400.0).round() as i64;
147 UtcInstant::from_micros(seconds * 1_000_000)
148}
149
150fn wrap180(degrees: f64) -> f64 {
151 (degrees + 180.0).rem_euclid(360.0) - 180.0
152}
153
154fn china_offset(jd: f64) -> f64 {
156 const CST_SINCE_JD: f64 = 2_425_612.5 - 8.0 / 24.0;
158 if jd < CST_SINCE_JD {
159 (116.0 + 25.0 / 60.0) / 360.0
160 } else {
161 8.0 / 24.0
162 }
163}
164
165fn china_day(jd: f64) -> i64 {
167 (jd + 0.5 + china_offset(jd)).floor() as i64
168}
169
170struct Sky<'a> {
172 kernels: &'a KernelSet,
173}
174
175impl Sky<'_> {
176 fn longitude(&self, target: i32, jd: f64) -> Result<f64, EngineError> {
178 let kernel = require_kernel(self.kernels, jd)?;
179 let t = Time::from_ut1(jd);
180 let orientation = Orientation::with_nutation(&t, iau2000b_radians(t.tt()));
182 let position = apparent(kernel, &observe(kernel, target, &t)?, &t)?;
183 Ok(ecliptic_latlon(&orientation, &position).1)
184 }
185
186 fn sun(&self, jd: f64) -> Result<f64, EngineError> {
187 self.longitude(SUN, jd)
188 }
189
190 fn elongation(&self, jd: f64) -> Result<f64, EngineError> {
191 Ok(self.longitude(MOON, jd)? - self.sun(jd)?)
192 }
193
194 fn sun_at(&self, longitude: f64, guess: f64) -> Result<f64, EngineError> {
196 crossing(|jd| self.sun(jd), longitude, guess, SUN_RATE)
197 }
198
199 fn new_moon_near(&self, guess: f64) -> Result<f64, EngineError> {
201 crossing(|jd| self.elongation(jd), 0.0, guess, ELONGATION_RATE)
202 }
203
204 fn sun_before(&self, longitude: f64, jd: f64) -> Result<f64, EngineError> {
206 let behind = (self.sun(jd)? - longitude).rem_euclid(360.0);
207 let found = self.sun_at(longitude, jd - behind / SUN_RATE)?;
208 if found > jd {
209 self.sun_at(longitude, found - TROPICAL_YEAR)
210 } else {
211 Ok(found)
212 }
213 }
214
215 fn new_moon_before(&self, jd: f64) -> Result<f64, EngineError> {
217 let behind = self.elongation(jd)?.rem_euclid(360.0);
218 let found = self.new_moon_near(jd - behind / ELONGATION_RATE)?;
219 if found > jd {
220 self.new_moon_near(found - SYNODIC_MONTH)
221 } else {
222 Ok(found)
223 }
224 }
225
226 fn month_eleven(&self, solstice: f64) -> Result<f64, EngineError> {
229 let solstice_day = china_day(solstice);
230 let moon = self.new_moon_before(solstice + 1.0)?;
231 if china_day(moon) > solstice_day {
232 self.new_moon_before(moon - 1.0)
233 } else {
234 Ok(moon)
235 }
236 }
237
238 fn sui(&self, solstice: f64) -> Result<(Vec<Month>, i64, f64), EngineError> {
241 let next_solstice = self.sun_at(270.0, solstice + TROPICAL_YEAR)?;
242 let end = china_day(self.month_eleven(next_solstice)?);
243
244 let mut moon = self.month_eleven(solstice)?;
245 let mut starts = vec![china_day(moon)];
246 loop {
247 moon = self.new_moon_near(moon + SYNODIC_MONTH)?;
248 let day = china_day(moon);
249 if day >= end {
250 break;
251 }
252 starts.push(day);
253 }
254
255 let mut major_days = vec![china_day(solstice)];
258 let mut term = solstice;
259 for k in 1..12 {
260 term = self.sun_at(f64::from((270 + 30 * k) % 360), term + 30.44)?;
261 major_days.push(china_day(term));
262 }
263
264 let (first_year, ..) =
265 UtcInstant::from_micros((starts[0] - UNIX_EPOCH_JDN) * MICROS_PER_DAY).civil();
266 let mut ends: Vec<i64> = starts[1..].to_vec();
267 ends.push(end);
268 let mut leap_pending = starts.len() == 13;
269 let mut number = 11u8;
270 let mut year = first_year;
271 let mut months = Vec::with_capacity(starts.len());
272 for (i, (&start, &month_end)) in starts.iter().zip(&ends).enumerate() {
273 let has_major = major_days.iter().any(|&d| start <= d && d < month_end);
274 let leap = i > 0 && leap_pending && !has_major;
275 if leap {
276 leap_pending = false;
277 } else if i > 0 {
278 number = number % 12 + 1;
279 if number == 1 {
280 year = first_year + 1;
281 }
282 }
283 months.push(Month {
284 start,
285 end: month_end,
286 number,
287 leap,
288 year,
289 });
290 }
291 Ok((months, end, next_solstice))
292 }
293
294 fn equation_of_time(&self, jd: f64) -> Result<f64, EngineError> {
296 let kernel = require_kernel(self.kernels, jd)?;
297 let t = Time::from_ut1(jd);
298 let orientation = Orientation::with_nutation(&t, iau2000b_radians(t.tt()));
299 let position = apparent(kernel, &observe(kernel, SUN, &t)?, &t)?;
300 let equatorial = mxv(&orientation.m, &position);
301 let ra_hours = equatorial[1].atan2(equatorial[0]).rem_euclid(TAU) * 24.0 / TAU;
302 let apparent_hours = orientation.gast_hours - ra_hours + 12.0;
303 let mean_hours = (jd + 0.5).rem_euclid(1.0) * 24.0;
304 Ok(((apparent_hours - mean_hours + 12.0).rem_euclid(24.0) - 12.0) * 60.0)
305 }
306}
307
308struct Month {
310 start: i64,
311 end: i64,
313 number: u8,
314 leap: bool,
315 year: i32,
316}
317
318fn crossing(
321 f: impl Fn(f64) -> Result<f64, EngineError>,
322 target: f64,
323 guess: f64,
324 rate: f64,
325) -> Result<f64, EngineError> {
326 let mut t0 = guess;
327 let mut d0 = wrap180(f(t0)? - target);
328 let mut t1 = t0 - d0 / rate;
329 for _ in 0..50 {
330 if (t1 - t0).abs() < EPSILON_DAYS {
331 break;
332 }
333 let d1 = wrap180(f(t1)? - target);
334 let slope = (d1 - d0) / (t1 - t0);
335 let slope = if slope.is_finite() && slope > rate / 4.0 {
338 slope
339 } else {
340 rate
341 };
342 (t0, d0) = (t1, d1);
343 t1 -= d1 / slope;
344 }
345 Ok(t1)
346}
347
348pub fn chinese_calendar(
351 kernels: &KernelSet,
352 instant: UtcInstant,
353) -> Result<Option<ChineseCalendar>, EngineError> {
354 match calendar(kernels, instant) {
355 Err(EngineError::OutOfRange { .. }) => Ok(None),
356 other => other.map(Some),
357 }
358}
359
360fn calendar(kernels: &KernelSet, instant: UtcInstant) -> Result<ChineseCalendar, EngineError> {
361 let sky = Sky { kernels };
362 let jd = instant.julian_day();
363
364 let mut solar_terms = Vec::new();
365 let mut term = sky.sun_before(f64::from(LICHUN), jd)?;
366 let mut longitude = LICHUN;
367 loop {
368 let jie = longitude % 30 == 15;
369 solar_terms.push(SolarTerm {
370 name: TERM_NAMES[usize::from(longitude / 15)].to_string(),
371 longitude,
372 jie,
373 utc: instant_of(term).isoformat(),
374 });
375 if jie && term > jd {
376 break;
377 }
378 longitude = (longitude + 15) % 360;
379 term = sky.sun_at(f64::from(longitude), term + 15.2)?;
380 }
381
382 let day = china_day(jd);
383 let solstice = sky.sun_before(270.0, jd)?;
384 let (mut months, mut end, mut next_solstice) = sky.sui(solstice)?;
385 if day - DATE_MARGIN < months[0].start {
386 let (mut earlier, ..) = sky.sui(sky.sun_at(270.0, solstice - TROPICAL_YEAR)?)?;
387 earlier.append(&mut months);
388 months = earlier;
389 }
390 while day + DATE_MARGIN >= end {
391 let (mut later, later_end, later_solstice) = sky.sui(next_solstice)?;
392 months.append(&mut later);
393 (end, next_solstice) = (later_end, later_solstice);
394 }
395 let lunar_months = months
396 .into_iter()
397 .filter(|m| m.start <= day + DATE_MARGIN && m.end > day - DATE_MARGIN)
398 .map(|m| LunarMonth {
399 year: m.year,
400 month: m.number,
401 leap: m.leap,
402 start: date_of(m.start),
403 days: (m.end - m.start) as u8,
404 })
405 .collect();
406
407 Ok(ChineseCalendar {
408 equation_of_time: sky.equation_of_time(jd)?,
409 solar_terms,
410 lunar_months,
411 })
412}
413
414#[derive(Debug, Clone, PartialEq, Eq)]
416pub(crate) struct LunarYear {
417 pub year: i32,
419 pub start: i64,
421 pub days: Vec<u8>,
423 pub leap: u8,
425}
426
427#[cfg(test)]
429pub(crate) fn lunar_years(
430 kernels: &KernelSet,
431 first: i32,
432 last: i32,
433) -> Result<Vec<LunarYear>, EngineError> {
434 let sky = Sky { kernels };
435 let december = |year: i32| UtcInstant::from_civil(year, 12, 10, 0, 0, 0, 0).julian_day();
436 let mut solstice = sky.sun_at(270.0, december(first - 1))?;
437 let mut months = Vec::new();
438 loop {
439 let (mut sui, _, next) = sky.sui(solstice)?;
440 let done = sui.last().is_some_and(|m| m.year > last);
441 months.append(&mut sui);
442 if done {
443 break;
444 }
445 solstice = next;
446 }
447 Ok((first..=last)
448 .map(|year| {
449 let of_year: Vec<&Month> = months.iter().filter(|m| m.year == year).collect();
450 LunarYear {
451 year,
452 start: of_year[0].start,
453 days: of_year.iter().map(|m| (m.end - m.start) as u8).collect(),
454 leap: of_year.iter().find(|m| m.leap).map_or(0, |m| m.number),
455 }
456 })
457 .collect())
458}
459
460#[cfg(test)]
463pub(crate) fn jie_of_year(kernels: &KernelSet, year: i32) -> Result<[f64; 12], EngineError> {
464 let sky = Sky { kernels };
465 let january = UtcInstant::from_civil(year, 1, 6, 0, 0, 0, 0).julian_day();
466 let mut moments = [0.0; 12];
467 for (i, moment) in moments.iter_mut().enumerate() {
468 let longitude = f64::from((285 + 30 * i as u32) % 360);
469 *moment = sky.sun_at(longitude, january + 30.44 * i as f64)?;
470 }
471 Ok(moments)
472}