1use serde::{Deserialize, Serialize};
14
15use crate::constants::TAU;
16use crate::ephemeris::observe::{apparent, ecliptic_latlon, observe};
17use crate::ephemeris::{KernelSet, SpkError};
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::pyfloat::wrap180;
24use crate::time::Time;
25
26const SUN: i32 = 10;
27const MOON: i32 = 301;
28const TROPICAL_YEAR: f64 = 365.242_19;
29const SYNODIC_MONTH: f64 = 29.530_589;
30const SUN_RATE: f64 = 360.0 / TROPICAL_YEAR;
32const ELONGATION_RATE: f64 = 360.0 / SYNODIC_MONTH;
34const EPSILON_DAYS: f64 = 1e-7;
36const UNIX_EPOCH_JD: f64 = 2_440_587.5;
38const UNIX_EPOCH_JDN: i64 = 2_440_588;
40const MICROS_PER_DAY: i64 = 86_400_000_000;
41const DATE_MARGIN: i64 = 3;
44
45const TERM_NAMES: [&str; 24] = [
47 "chunfen",
48 "qingming",
49 "guyu",
50 "lixia",
51 "xiaoman",
52 "mangzhong",
53 "xiazhi",
54 "xiaoshu",
55 "dashu",
56 "liqiu",
57 "chushu",
58 "bailu",
59 "qiufen",
60 "hanlu",
61 "shuangjiang",
62 "lidong",
63 "xiaoxue",
64 "daxue",
65 "dongzhi",
66 "xiaohan",
67 "dahan",
68 "lichun",
69 "yushui",
70 "jingzhe",
71];
72
73pub(crate) const LICHUN: u16 = 315;
75
76#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
78pub struct SolarTerm {
79 pub name: String,
81 pub longitude: u16,
83 pub jie: bool,
86 pub utc: String,
88}
89
90#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
92pub struct LunarMonth {
93 pub year: i32,
95 pub month: u8,
97 pub leap: bool,
99 pub start: String,
101 pub days: u8,
103}
104
105#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
108pub struct ChineseCalendar {
109 pub equation_of_time: f64,
111 pub solar_terms: Vec<SolarTerm>,
114 pub lunar_months: Vec<LunarMonth>,
116}
117
118impl SolarTerm {
119 pub(crate) fn instant(&self) -> Result<UtcInstant, EngineError> {
121 UtcInstant::parse(&self.utc).map_err(|e| EngineError::InvalidInput(e.to_string()))
122 }
123}
124
125impl LunarMonth {
126 pub(crate) fn start_day(&self) -> Result<i64, EngineError> {
128 let start = UtcInstant::parse(&format!("{}T00:00:00Z", self.start))
129 .map_err(|e| EngineError::InvalidInput(e.to_string()))?;
130 Ok(day_number(start))
131 }
132}
133
134pub(crate) fn day_number(instant: UtcInstant) -> i64 {
136 instant.micros().div_euclid(MICROS_PER_DAY) + UNIX_EPOCH_JDN
137}
138
139fn date_of(day: i64) -> String {
141 let (y, m, d, ..) = UtcInstant::from_micros((day - UNIX_EPOCH_JDN) * MICROS_PER_DAY).civil();
142 format!("{y:04}-{m:02}-{d:02}")
143}
144
145fn instant_of(jd: f64) -> UtcInstant {
147 let seconds = ((jd - UNIX_EPOCH_JD) * 86_400.0).round() as i64;
148 UtcInstant::from_micros(seconds * 1_000_000)
149}
150
151fn china_offset(jd: f64) -> f64 {
153 const CST_SINCE_JD: f64 = 2_425_612.5 - 8.0 / 24.0;
155 if jd < CST_SINCE_JD {
156 (116.0 + 25.0 / 60.0) / 360.0
157 } else {
158 8.0 / 24.0
159 }
160}
161
162fn china_day(jd: f64) -> i64 {
164 (jd + 0.5 + china_offset(jd)).floor() as i64
165}
166
167struct Sky<'a> {
169 kernels: &'a KernelSet,
170}
171
172impl Sky<'_> {
173 fn longitude(&self, target: i32, jd: f64) -> Result<f64, EngineError> {
175 let kernel = require_kernel(self.kernels, jd)?;
176 let t = Time::from_ut1(jd);
177 let orientation = Orientation::with_nutation(&t, iau2000b_radians(t.tt()));
179 let position = apparent(kernel, &observe(kernel, target, &t)?, &t)?;
180 Ok(ecliptic_latlon(&orientation, &position).1)
181 }
182
183 fn sun(&self, jd: f64) -> Result<f64, EngineError> {
184 self.longitude(SUN, jd)
185 }
186
187 fn elongation(&self, jd: f64) -> Result<f64, EngineError> {
188 Ok(self.longitude(MOON, jd)? - self.sun(jd)?)
189 }
190
191 fn sun_at(&self, longitude: f64, guess: f64) -> Result<f64, EngineError> {
193 crossing(|jd| self.sun(jd), longitude, guess, SUN_RATE)
194 }
195
196 fn new_moon_near(&self, guess: f64) -> Result<f64, EngineError> {
198 crossing(|jd| self.elongation(jd), 0.0, guess, ELONGATION_RATE)
199 }
200
201 fn sun_before(&self, longitude: f64, jd: f64) -> Result<f64, EngineError> {
203 let behind = (self.sun(jd)? - longitude).rem_euclid(360.0);
204 let found = self.sun_at(longitude, jd - behind / SUN_RATE)?;
205 if found > jd {
206 self.sun_at(longitude, found - TROPICAL_YEAR)
207 } else {
208 Ok(found)
209 }
210 }
211
212 fn new_moon_before(&self, jd: f64) -> Result<f64, EngineError> {
214 let behind = self.elongation(jd)?.rem_euclid(360.0);
215 let found = self.new_moon_near(jd - behind / ELONGATION_RATE)?;
216 if found > jd {
217 self.new_moon_near(found - SYNODIC_MONTH)
218 } else {
219 Ok(found)
220 }
221 }
222
223 fn month_eleven(&self, solstice: f64) -> Result<f64, EngineError> {
226 let solstice_day = china_day(solstice);
227 let moon = self.new_moon_before(solstice + 1.0)?;
228 if china_day(moon) > solstice_day {
229 self.new_moon_before(moon - 1.0)
230 } else {
231 Ok(moon)
232 }
233 }
234
235 fn sui(&self, solstice: f64) -> Result<(Vec<Month>, i64, f64), EngineError> {
238 let next_solstice = self.sun_at(270.0, solstice + TROPICAL_YEAR)?;
239 let end = china_day(self.month_eleven(next_solstice)?);
240
241 let mut moon = self.month_eleven(solstice)?;
242 let mut starts = vec![china_day(moon)];
243 loop {
244 moon = self.new_moon_near(moon + SYNODIC_MONTH)?;
245 let day = china_day(moon);
246 if day >= end {
247 break;
248 }
249 starts.push(day);
250 if starts.len() > 13 {
251 return Err(diverged("a year of more than 13 months"));
252 }
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 diverged(what: &str) -> EngineError {
322 EngineError::Ephemeris(SpkError::Format(format!("Chinese calendar search: {what}")))
323}
324
325fn crossing(
326 f: impl Fn(f64) -> Result<f64, EngineError>,
327 target: f64,
328 guess: f64,
329 rate: f64,
330) -> Result<f64, EngineError> {
331 let mut t0 = guess;
332 let mut d0 = wrap180(f(t0)? - target);
333 let mut t1 = t0 - d0 / rate;
334 for _ in 0..50 {
335 if (t1 - t0).abs() < EPSILON_DAYS {
336 break;
337 }
338 let d1 = wrap180(f(t1)? - target);
339 let slope = (d1 - d0) / (t1 - t0);
340 let slope = if slope.is_finite() && slope > rate / 4.0 {
343 slope
344 } else {
345 rate
346 };
347 (t0, d0) = (t1, d1);
348 t1 -= d1 / slope;
349 }
350 Ok(t1)
351}
352
353pub fn chinese_calendar(
356 kernels: &KernelSet,
357 instant: UtcInstant,
358) -> Result<Option<ChineseCalendar>, EngineError> {
359 match calendar(kernels, instant) {
360 Err(EngineError::OutOfRange { .. }) => Ok(None),
361 other => other.map(Some),
362 }
363}
364
365fn calendar(kernels: &KernelSet, instant: UtcInstant) -> Result<ChineseCalendar, EngineError> {
366 let sky = Sky { kernels };
367 let jd = instant.julian_day();
368
369 let mut solar_terms = Vec::new();
370 let mut term = sky.sun_before(f64::from(LICHUN), jd)?;
371 let mut longitude = LICHUN;
372 loop {
373 let jie = longitude % 30 == 15;
374 solar_terms.push(SolarTerm {
375 name: TERM_NAMES[usize::from(longitude / 15)].to_string(),
376 longitude,
377 jie,
378 utc: instant_of(term).isoformat(),
379 });
380 if jie && term > jd {
381 break;
382 }
383 if solar_terms.len() > 48 {
384 return Err(diverged("no solar term after the moment"));
385 }
386 longitude = (longitude + 15) % 360;
387 term = sky.sun_at(f64::from(longitude), term + 15.2)?;
388 }
389
390 let day = china_day(jd);
391 let solstice = sky.sun_before(270.0, jd)?;
392 let (mut months, mut end, mut next_solstice) = sky.sui(solstice)?;
393 if day - DATE_MARGIN < months[0].start {
394 let (mut earlier, ..) = sky.sui(sky.sun_at(270.0, solstice - TROPICAL_YEAR)?)?;
395 earlier.append(&mut months);
396 months = earlier;
397 }
398 while day + DATE_MARGIN >= end {
399 if months.len() > 40 {
400 return Err(diverged("no lunar year after the moment"));
401 }
402 let (mut later, later_end, later_solstice) = sky.sui(next_solstice)?;
403 months.append(&mut later);
404 (end, next_solstice) = (later_end, later_solstice);
405 }
406 let lunar_months = months
407 .into_iter()
408 .filter(|m| m.start <= day + DATE_MARGIN && m.end > day - DATE_MARGIN)
409 .map(|m| LunarMonth {
410 year: m.year,
411 month: m.number,
412 leap: m.leap,
413 start: date_of(m.start),
414 days: (m.end - m.start) as u8,
415 })
416 .collect();
417
418 Ok(ChineseCalendar {
419 equation_of_time: sky.equation_of_time(jd)?,
420 solar_terms,
421 lunar_months,
422 })
423}
424
425#[derive(Debug, Clone, PartialEq, Eq)]
427pub(crate) struct LunarYear {
428 pub year: i32,
430 pub start: i64,
432 pub days: Vec<u8>,
434 pub leap: u8,
436}
437
438#[cfg(test)]
440pub(crate) fn lunar_years(
441 kernels: &KernelSet,
442 first: i32,
443 last: i32,
444) -> Result<Vec<LunarYear>, EngineError> {
445 let sky = Sky { kernels };
446 let december = |year: i32| UtcInstant::from_civil(year, 12, 10, 0, 0, 0, 0).julian_day();
447 let mut solstice = sky.sun_at(270.0, december(first - 1))?;
448 let mut months = Vec::new();
449 loop {
450 let (mut sui, _, next) = sky.sui(solstice)?;
451 let done = sui.last().is_some_and(|m| m.year > last);
452 months.append(&mut sui);
453 if done {
454 break;
455 }
456 solstice = next;
457 }
458 Ok((first..=last)
459 .map(|year| {
460 let of_year: Vec<&Month> = months.iter().filter(|m| m.year == year).collect();
461 LunarYear {
462 year,
463 start: of_year[0].start,
464 days: of_year.iter().map(|m| (m.end - m.start) as u8).collect(),
465 leap: of_year.iter().find(|m| m.leap).map_or(0, |m| m.number),
466 }
467 })
468 .collect())
469}
470
471#[cfg(test)]
474pub(crate) fn jie_of_year(kernels: &KernelSet, year: i32) -> Result<[f64; 12], EngineError> {
475 let sky = Sky { kernels };
476 let january = UtcInstant::from_civil(year, 1, 6, 0, 0, 0, 0).julian_day();
477 let mut moments = [0.0; 12];
478 for (i, moment) in moments.iter_mut().enumerate() {
479 let longitude = f64::from((285 + 30 * i as u32) % 360);
480 *moment = sky.sun_at(longitude, january + 30.44 * i as f64)?;
481 }
482 Ok(moments)
483}