1use std::cell::RefCell;
7use std::collections::HashMap;
8
9use crate::delta_t::delta_t_for_year;
10use crate::julian::{jd_from_ymd, ymd_from_jd};
11use crate::new_moon::find_new_moons_in_range;
12use crate::solar_terms::{find_solar_term_moment_dt, SOLAR_TERM_LONGITUDES};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub struct CivilDate {
17 pub year: i32,
18 pub month: u32,
19 pub day: u32,
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub struct LunisolarDate {
25 pub year: i32,
26 pub month: u32,
27 pub day: u32,
28 pub is_leap_month: bool,
29 pub boundary_uncertain: bool,
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub struct LunarMonth {
40 pub month_number: u32,
41 pub is_leap_month: bool,
42 pub start: CivilDate,
43 pub days: u32,
44 pub boundary_uncertain: bool,
46}
47
48const ZHONGQI_INDICES: [usize; 12] = [1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23];
50const DONGZHI_INDEX: usize = 23;
52
53const SEQ: [u32; 11] = [12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
55
56fn jde_to_jd_ut(jde: f64, dt: fn(f64) -> f64) -> f64 {
57 let year = 2000.0 + (jde - 2451545.0) / 365.25;
58 jde - dt(year) / 86400.0
59}
60
61fn em_parabola(year: f64) -> f64 {
64 let u = (year - 1820.0) / 100.0;
65 -20.0 + 32.0 * u * u
66}
67
68fn start_is_uncertain(start_jd_ut: f64, dt: fn(f64) -> f64) -> bool {
73 let beijing = start_jd_ut + 8.0 / 24.0;
74 let year = ymd_from_jd(beijing).0;
75 if year <= 2050 {
76 return false;
77 }
78 let sigma_sec = (em_parabola(f64::from(year)) - dt(f64::from(year))).max(0.0);
79 let shifted = beijing + 0.5;
80 let frac = shifted - shifted.floor();
81 let dist_sec = frac.min(1.0 - frac) * 86400.0;
82 dist_sec < sigma_sec
83}
84
85fn beijing_civil(jd_ut: f64) -> CivilDate {
86 let (year, month, day) = ymd_from_jd(jd_ut + 8.0 / 24.0);
87 CivilDate { year, month, day }
88}
89
90fn civil_num(cd: CivilDate) -> i64 {
91 i64::from(cd.year) * 10000 + i64::from(cd.month) * 100 + i64::from(cd.day)
92}
93
94fn civil_jd(cd: CivilDate) -> f64 {
95 jd_from_ymd(cd.year, cd.month, cd.day)
96}
97
98fn days_between(a_jd_ut: f64, b_jd_ut: f64) -> u32 {
100 let a = civil_jd(beijing_civil(a_jd_ut));
101 let b = civil_jd(beijing_civil(b_jd_ut));
102 (b - a).round() as u32
103}
104
105fn zhongqi_falls_in_month(zq_jd: f64, month_start_jd: f64, next_month_jd: f64) -> bool {
106 let zq = civil_num(beijing_civil(zq_jd));
107 let ms = civil_num(beijing_civil(month_start_jd));
108 let nm = civil_num(beijing_civil(next_month_jd));
109 zq >= ms && zq < nm
110}
111
112fn new_moon_jd_uts(start_year: i32, end_year: i32, dt: fn(f64) -> f64) -> Vec<f64> {
114 let start_jd = jd_from_ymd(start_year, 1, 1) - 30.0;
115 let end_jd = jd_from_ymd(end_year + 1, 2, 28) + 30.0;
116 find_new_moons_in_range(start_jd, end_jd)
117 .into_iter()
118 .map(|jde| jde_to_jd_ut(jde, dt))
119 .collect()
120}
121
122fn zhongqi_moments(start_year: i32, end_year: i32, dt: fn(f64) -> f64) -> Vec<(usize, f64)> {
124 let mut moments = Vec::new();
125 for y in start_year..=end_year {
126 for &idx in &ZHONGQI_INDICES {
127 let longitude = SOLAR_TERM_LONGITUDES[idx];
128 let start_month = if idx < 2 { 1 } else { (idx / 2 + 1) as u32 };
129 if let Some(jd) = find_solar_term_moment_dt(longitude, y, start_month, dt) {
130 moments.push((idx, jd));
131 }
132 }
133 }
134 moments.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(core::cmp::Ordering::Equal));
135 moments
136}
137
138struct MonthSpan {
139 start_jd: f64,
140 days: u32,
141 has_zhongqi: bool,
142 has_dongzhi: bool,
143}
144
145struct NumberedMonth {
146 month_number: u32,
147 is_leap_month: bool,
148 start_jd: f64,
149 days: u32,
150}
151
152fn build_month_sequence(new_moons: &[f64], zhongqi: &[(usize, f64)]) -> Vec<NumberedMonth> {
156 let mut spans: Vec<MonthSpan> = Vec::new();
157 for w in new_moons.windows(2) {
158 let (start, end) = (w[0], w[1]);
159 let mut has_zhongqi = false;
160 let mut has_dongzhi = false;
161 for &(index, date) in zhongqi {
162 if zhongqi_falls_in_month(date, start, end) {
163 has_zhongqi = true;
164 if index == DONGZHI_INDEX {
165 has_dongzhi = true;
166 }
167 }
168 }
169 spans.push(MonthSpan {
170 start_jd: start,
171 days: days_between(start, end),
172 has_zhongqi,
173 has_dongzhi,
174 });
175 }
176
177 let dongzhi_indices: Vec<usize> = spans
178 .iter()
179 .enumerate()
180 .filter(|(_, s)| s.has_dongzhi)
181 .map(|(i, _)| i)
182 .collect();
183
184 let mut assignments: Vec<Option<(u32, bool)>> = vec![None; spans.len()];
185 for d in 0..dongzhi_indices.len() {
186 let m11 = dongzhi_indices[d];
187 assignments[m11] = Some((11, false));
188 if d + 1 >= dongzhi_indices.len() {
189 continue;
190 }
191 let next_m11 = dongzhi_indices[d + 1];
192 assignments[next_m11] = Some((11, false));
193
194 let intermediate = next_m11 - m11 - 1;
195 let needs_leap = intermediate > 11;
196
197 let mut seq_idx = 0usize;
198 let mut leap_inserted = false;
199 for i in (m11 + 1)..next_m11 {
200 if needs_leap && !leap_inserted && !spans[i].has_zhongqi {
201 let leap_num = if seq_idx == 0 { 11 } else { SEQ[seq_idx - 1] };
202 assignments[i] = Some((leap_num, true));
203 leap_inserted = true;
204 } else {
205 if seq_idx < SEQ.len() {
206 assignments[i] = Some((SEQ[seq_idx], false));
207 }
208 seq_idx += 1;
209 }
210 }
211 }
212
213 spans
214 .iter()
215 .zip(assignments)
216 .filter_map(|(span, a)| {
217 a.map(|(month_number, is_leap_month)| NumberedMonth {
218 month_number,
219 is_leap_month,
220 start_jd: span.start_jd,
221 days: span.days,
222 })
223 })
224 .collect()
225}
226
227thread_local! {
228 static MONTHS_CACHE: RefCell<HashMap<i32, Vec<LunarMonth>>> = RefCell::new(HashMap::new());
232}
233
234#[must_use]
241pub fn lunar_months_for_year(lunar_year: i32) -> Vec<LunarMonth> {
242 if let Some(v) = MONTHS_CACHE.with(|c| c.borrow().get(&lunar_year).cloned()) {
243 return v;
244 }
245 let result = compute_lunar_months_for_year(lunar_year, delta_t_for_year);
246 MONTHS_CACHE.with(|c| c.borrow_mut().insert(lunar_year, result.clone()));
247 result
248}
249
250fn compute_lunar_months_for_year(lunar_year: i32, dt: fn(f64) -> f64) -> Vec<LunarMonth> {
251 let new_moons = new_moon_jd_uts(lunar_year - 1, lunar_year + 1, dt);
252 let zhongqi = zhongqi_moments(lunar_year - 1, lunar_year + 1, dt);
253 let sequence = build_month_sequence(&new_moons, &zhongqi);
254
255 let month1_idx = sequence
256 .iter()
257 .position(|m| {
258 m.month_number == 1 && !m.is_leap_month && beijing_civil(m.start_jd).year == lunar_year
259 })
260 .unwrap_or_else(|| panic!("cannot find month 1 for lunar year {lunar_year}"));
261
262 let mut result = Vec::new();
263 for (i, m) in sequence.iter().enumerate().skip(month1_idx) {
264 if i > month1_idx && m.month_number == 1 && !m.is_leap_month {
265 break;
266 }
267 result.push(LunarMonth {
268 month_number: m.month_number,
269 is_leap_month: m.is_leap_month,
270 start: beijing_civil(m.start_jd),
271 days: m.days,
272 boundary_uncertain: start_is_uncertain(m.start_jd, dt),
273 });
274 }
275 result
276}
277
278#[must_use]
283pub fn lunar_new_year(gregorian_year: i32) -> CivilDate {
284 lunar_months_for_year(gregorian_year)
285 .into_iter()
286 .find(|m| m.month_number == 1 && !m.is_leap_month)
287 .unwrap_or_else(|| panic!("cannot find month 1 for lunar year {gregorian_year}"))
288 .start
289}
290
291#[must_use]
296pub fn gregorian_to_lunisolar(date: CivilDate) -> LunisolarDate {
297 convert(date, true, delta_t_for_year)
298}
299
300#[must_use]
307pub fn gregorian_to_lunisolar_with(date: CivilDate, dt: fn(f64) -> f64) -> LunisolarDate {
308 convert(date, false, dt)
309}
310
311fn convert(date: CivilDate, cached: bool, dt: fn(f64) -> f64) -> LunisolarDate {
312 let target_jd = civil_jd(date);
313 for lunar_year in [date.year, date.year - 1, date.year + 1] {
314 let months = if cached {
315 lunar_months_for_year(lunar_year)
316 } else {
317 compute_lunar_months_for_year(lunar_year, dt)
318 };
319 for m in months.iter().rev() {
320 let start_jd = civil_jd(m.start);
321 if target_jd >= start_jd {
322 let lunar_day = (target_jd - start_jd).round() as i64 + 1;
323 if lunar_day >= 1 && lunar_day <= i64::from(m.days) {
324 return LunisolarDate {
325 year: lunar_year,
326 month: m.month_number,
327 day: lunar_day as u32,
328 is_leap_month: m.is_leap_month,
329 boundary_uncertain: m.boundary_uncertain,
330 };
331 }
332 }
333 }
334 }
335 panic!(
336 "cannot convert {}-{}-{} to a lunisolar date",
337 date.year, date.month, date.day
338 )
339}