astroceleste_engine/chinese/
calendar.rs1use serde::{Deserialize, Serialize};
14
15use crate::constants::TAU;
16use crate::ephemeris::observe::{apparent, 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::search::{instant_of, Sky, SUN, SYNODIC_MONTH, TROPICAL_YEAR};
24use crate::time::Time;
25
26const UNIX_EPOCH_JDN: i64 = 2_440_588;
28const MICROS_PER_DAY: i64 = 86_400_000_000;
29const DATE_MARGIN: i64 = 3;
32
33const TERM_NAMES: [&str; 24] = [
35 "chunfen",
36 "qingming",
37 "guyu",
38 "lixia",
39 "xiaoman",
40 "mangzhong",
41 "xiazhi",
42 "xiaoshu",
43 "dashu",
44 "liqiu",
45 "chushu",
46 "bailu",
47 "qiufen",
48 "hanlu",
49 "shuangjiang",
50 "lidong",
51 "xiaoxue",
52 "daxue",
53 "dongzhi",
54 "xiaohan",
55 "dahan",
56 "lichun",
57 "yushui",
58 "jingzhe",
59];
60
61pub(crate) const LICHUN: u16 = 315;
63
64#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
66pub struct SolarTerm {
67 pub name: String,
69 pub longitude: u16,
71 pub jie: bool,
74 pub utc: String,
76}
77
78#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
80pub struct LunarMonth {
81 pub year: i32,
83 pub month: u8,
85 pub leap: bool,
87 pub start: String,
89 pub days: u8,
91}
92
93#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
96pub struct ChineseCalendar {
97 pub equation_of_time: f64,
99 pub solar_terms: Vec<SolarTerm>,
102 pub lunar_months: Vec<LunarMonth>,
104}
105
106impl SolarTerm {
107 pub(crate) fn instant(&self) -> Result<UtcInstant, EngineError> {
109 UtcInstant::parse(&self.utc).map_err(|e| EngineError::InvalidInput(e.to_string()))
110 }
111}
112
113impl LunarMonth {
114 pub(crate) fn start_day(&self) -> Result<i64, EngineError> {
116 let start = UtcInstant::parse(&format!("{}T00:00:00Z", self.start))
117 .map_err(|e| EngineError::InvalidInput(e.to_string()))?;
118 Ok(day_number(start))
119 }
120}
121
122pub(crate) fn day_number(instant: UtcInstant) -> i64 {
124 instant.micros().div_euclid(MICROS_PER_DAY) + UNIX_EPOCH_JDN
125}
126
127fn date_of(day: i64) -> String {
129 let (y, m, d, ..) = UtcInstant::from_micros((day - UNIX_EPOCH_JDN) * MICROS_PER_DAY).civil();
130 format!("{y:04}-{m:02}-{d:02}")
131}
132
133fn china_offset(jd: f64) -> f64 {
135 const CST_SINCE_JD: f64 = 2_425_612.5 - 8.0 / 24.0;
137 if jd < CST_SINCE_JD {
138 (116.0 + 25.0 / 60.0) / 360.0
139 } else {
140 8.0 / 24.0
141 }
142}
143
144fn china_day(jd: f64) -> i64 {
146 (jd + 0.5 + china_offset(jd)).floor() as i64
147}
148
149impl Sky<'_> {
150 fn month_eleven(&self, solstice: f64) -> Result<f64, EngineError> {
153 let solstice_day = china_day(solstice);
154 let moon = self.new_moon_before(solstice + 1.0)?;
155 if china_day(moon) > solstice_day {
156 self.new_moon_before(moon - 1.0)
157 } else {
158 Ok(moon)
159 }
160 }
161
162 fn sui(&self, solstice: f64) -> Result<(Vec<Month>, i64, f64), EngineError> {
165 let next_solstice = self.sun_at(270.0, solstice + TROPICAL_YEAR)?;
166 let end = china_day(self.month_eleven(next_solstice)?);
167
168 let mut moon = self.month_eleven(solstice)?;
169 let mut starts = vec![china_day(moon)];
170 loop {
171 moon = self.new_moon_near(moon + SYNODIC_MONTH)?;
172 let day = china_day(moon);
173 if day >= end {
174 break;
175 }
176 starts.push(day);
177 if starts.len() > 13 {
178 return Err(diverged("a year of more than 13 months"));
179 }
180 }
181
182 let mut major_days = vec![china_day(solstice)];
185 let mut term = solstice;
186 for k in 1..12 {
187 term = self.sun_at(f64::from((270 + 30 * k) % 360), term + 30.44)?;
188 major_days.push(china_day(term));
189 }
190
191 let (first_year, ..) =
192 UtcInstant::from_micros((starts[0] - UNIX_EPOCH_JDN) * MICROS_PER_DAY).civil();
193 let mut ends: Vec<i64> = starts[1..].to_vec();
194 ends.push(end);
195 let mut leap_pending = starts.len() == 13;
196 let mut number = 11u8;
197 let mut year = first_year;
198 let mut months = Vec::with_capacity(starts.len());
199 for (i, (&start, &month_end)) in starts.iter().zip(&ends).enumerate() {
200 let has_major = major_days.iter().any(|&d| start <= d && d < month_end);
201 let leap = i > 0 && leap_pending && !has_major;
202 if leap {
203 leap_pending = false;
204 } else if i > 0 {
205 number = number % 12 + 1;
206 if number == 1 {
207 year = first_year + 1;
208 }
209 }
210 months.push(Month {
211 start,
212 end: month_end,
213 number,
214 leap,
215 year,
216 });
217 }
218 Ok((months, end, next_solstice))
219 }
220
221 fn equation_of_time(&self, jd: f64) -> Result<f64, EngineError> {
223 let kernel = require_kernel(self.kernels, jd)?;
224 let t = Time::from_ut1(jd);
225 let orientation = Orientation::with_nutation(&t, iau2000b_radians(t.tt()));
226 let position = apparent(kernel, &observe(kernel, SUN, &t)?, &t)?;
227 let equatorial = mxv(&orientation.m, &position);
228 let ra_hours = equatorial[1].atan2(equatorial[0]).rem_euclid(TAU) * 24.0 / TAU;
229 let apparent_hours = orientation.gast_hours - ra_hours + 12.0;
230 let mean_hours = (jd + 0.5).rem_euclid(1.0) * 24.0;
231 Ok(((apparent_hours - mean_hours + 12.0).rem_euclid(24.0) - 12.0) * 60.0)
232 }
233}
234
235struct Month {
237 start: i64,
238 end: i64,
240 number: u8,
241 leap: bool,
242 year: i32,
243}
244
245fn diverged(what: &str) -> EngineError {
247 EngineError::Ephemeris(SpkError::Format(format!("Chinese calendar search: {what}")))
248}
249
250pub fn chinese_calendar(
253 kernels: &KernelSet,
254 instant: UtcInstant,
255) -> Result<Option<ChineseCalendar>, EngineError> {
256 match calendar(kernels, instant) {
257 Err(EngineError::OutOfRange { .. }) => Ok(None),
258 other => other.map(Some),
259 }
260}
261
262fn calendar(kernels: &KernelSet, instant: UtcInstant) -> Result<ChineseCalendar, EngineError> {
263 let sky = Sky { kernels };
264 let jd = instant.julian_day();
265
266 let mut solar_terms = Vec::new();
267 let mut term = sky.sun_before(f64::from(LICHUN), jd)?;
268 let mut longitude = LICHUN;
269 loop {
270 let jie = longitude % 30 == 15;
271 solar_terms.push(SolarTerm {
272 name: TERM_NAMES[usize::from(longitude / 15)].to_string(),
273 longitude,
274 jie,
275 utc: instant_of(term).isoformat(),
276 });
277 if jie && term > jd {
278 break;
279 }
280 if solar_terms.len() > 48 {
281 return Err(diverged("no solar term after the moment"));
282 }
283 longitude = (longitude + 15) % 360;
284 term = sky.sun_at(f64::from(longitude), term + 15.2)?;
285 }
286
287 let day = china_day(jd);
288 let solstice = sky.sun_before(270.0, jd)?;
289 let (mut months, mut end, mut next_solstice) = sky.sui(solstice)?;
290 if day - DATE_MARGIN < months[0].start {
291 let (mut earlier, ..) = sky.sui(sky.sun_at(270.0, solstice - TROPICAL_YEAR)?)?;
292 earlier.append(&mut months);
293 months = earlier;
294 }
295 while day + DATE_MARGIN >= end {
296 if months.len() > 40 {
297 return Err(diverged("no lunar year after the moment"));
298 }
299 let (mut later, later_end, later_solstice) = sky.sui(next_solstice)?;
300 months.append(&mut later);
301 (end, next_solstice) = (later_end, later_solstice);
302 }
303 let lunar_months = months
304 .into_iter()
305 .filter(|m| m.start <= day + DATE_MARGIN && m.end > day - DATE_MARGIN)
306 .map(|m| LunarMonth {
307 year: m.year,
308 month: m.number,
309 leap: m.leap,
310 start: date_of(m.start),
311 days: (m.end - m.start) as u8,
312 })
313 .collect();
314
315 Ok(ChineseCalendar {
316 equation_of_time: sky.equation_of_time(jd)?,
317 solar_terms,
318 lunar_months,
319 })
320}
321
322#[derive(Debug, Clone, PartialEq, Eq)]
324pub(crate) struct LunarYear {
325 pub year: i32,
327 pub start: i64,
329 pub days: Vec<u8>,
331 pub leap: u8,
333}
334
335#[cfg(test)]
337pub(crate) fn lunar_years(
338 kernels: &KernelSet,
339 first: i32,
340 last: i32,
341) -> Result<Vec<LunarYear>, EngineError> {
342 let sky = Sky { kernels };
343 let december = |year: i32| UtcInstant::from_civil(year, 12, 10, 0, 0, 0, 0).julian_day();
344 let mut solstice = sky.sun_at(270.0, december(first - 1))?;
345 let mut months = Vec::new();
346 loop {
347 let (mut sui, _, next) = sky.sui(solstice)?;
348 let done = sui.last().is_some_and(|m| m.year > last);
349 months.append(&mut sui);
350 if done {
351 break;
352 }
353 solstice = next;
354 }
355 Ok((first..=last)
356 .map(|year| {
357 let of_year: Vec<&Month> = months.iter().filter(|m| m.year == year).collect();
358 LunarYear {
359 year,
360 start: of_year[0].start,
361 days: of_year.iter().map(|m| (m.end - m.start) as u8).collect(),
362 leap: of_year.iter().find(|m| m.leap).map_or(0, |m| m.number),
363 }
364 })
365 .collect())
366}
367
368#[cfg(test)]
371pub(crate) fn jie_of_year(kernels: &KernelSet, year: i32) -> Result<[f64; 12], EngineError> {
372 let sky = Sky { kernels };
373 let january = UtcInstant::from_civil(year, 1, 6, 0, 0, 0, 0).julian_day();
374 let mut moments = [0.0; 12];
375 for (i, moment) in moments.iter_mut().enumerate() {
376 let longitude = f64::from((285 + 30 * i as u32) % 360);
377 *moment = sky.sun_at(longitude, january + 30.44 * i as f64)?;
378 }
379 Ok(moments)
380}