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, 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}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub struct LunarMonth {
35 pub month_number: u32,
36 pub is_leap_month: bool,
37 pub start: CivilDate,
38 pub days: u32,
39}
40
41const ZHONGQI_INDICES: [usize; 12] = [1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23];
43const DONGZHI_INDEX: usize = 23;
45
46const SEQ: [u32; 11] = [12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
48
49fn jde_to_jd_ut(jde: f64) -> f64 {
50 let year = 2000.0 + (jde - 2451545.0) / 365.25;
51 jde - delta_t_for_year(year) / 86400.0
52}
53
54fn beijing_civil(jd_ut: f64) -> CivilDate {
55 let (year, month, day) = ymd_from_jd(jd_ut + 8.0 / 24.0);
56 CivilDate { year, month, day }
57}
58
59fn civil_num(cd: CivilDate) -> i64 {
60 i64::from(cd.year) * 10000 + i64::from(cd.month) * 100 + i64::from(cd.day)
61}
62
63fn civil_jd(cd: CivilDate) -> f64 {
64 jd_from_ymd(cd.year, cd.month, cd.day)
65}
66
67fn days_between(a_jd_ut: f64, b_jd_ut: f64) -> u32 {
69 let a = civil_jd(beijing_civil(a_jd_ut));
70 let b = civil_jd(beijing_civil(b_jd_ut));
71 (b - a).round() as u32
72}
73
74fn zhongqi_falls_in_month(zq_jd: f64, month_start_jd: f64, next_month_jd: f64) -> bool {
75 let zq = civil_num(beijing_civil(zq_jd));
76 let ms = civil_num(beijing_civil(month_start_jd));
77 let nm = civil_num(beijing_civil(next_month_jd));
78 zq >= ms && zq < nm
79}
80
81fn new_moon_jd_uts(start_year: i32, end_year: i32) -> Vec<f64> {
83 let start_jd = jd_from_ymd(start_year, 1, 1) - 30.0;
84 let end_jd = jd_from_ymd(end_year + 1, 2, 28) + 30.0;
85 find_new_moons_in_range(start_jd, end_jd)
86 .into_iter()
87 .map(jde_to_jd_ut)
88 .collect()
89}
90
91fn zhongqi_moments(start_year: i32, end_year: i32) -> Vec<(usize, f64)> {
93 let mut moments = Vec::new();
94 for y in start_year..=end_year {
95 for &idx in &ZHONGQI_INDICES {
96 let longitude = SOLAR_TERM_LONGITUDES[idx];
97 let start_month = if idx < 2 { 1 } else { (idx / 2 + 1) as u32 };
98 if let Some(jd) = find_solar_term_moment(longitude, y, start_month) {
99 moments.push((idx, jd));
100 }
101 }
102 }
103 moments.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(core::cmp::Ordering::Equal));
104 moments
105}
106
107struct MonthSpan {
108 start_jd: f64,
109 days: u32,
110 has_zhongqi: bool,
111 has_dongzhi: bool,
112}
113
114struct NumberedMonth {
115 month_number: u32,
116 is_leap_month: bool,
117 start_jd: f64,
118 days: u32,
119}
120
121fn build_month_sequence(new_moons: &[f64], zhongqi: &[(usize, f64)]) -> Vec<NumberedMonth> {
125 let mut spans: Vec<MonthSpan> = Vec::new();
126 for w in new_moons.windows(2) {
127 let (start, end) = (w[0], w[1]);
128 let mut has_zhongqi = false;
129 let mut has_dongzhi = false;
130 for &(index, date) in zhongqi {
131 if zhongqi_falls_in_month(date, start, end) {
132 has_zhongqi = true;
133 if index == DONGZHI_INDEX {
134 has_dongzhi = true;
135 }
136 }
137 }
138 spans.push(MonthSpan {
139 start_jd: start,
140 days: days_between(start, end),
141 has_zhongqi,
142 has_dongzhi,
143 });
144 }
145
146 let dongzhi_indices: Vec<usize> = spans
147 .iter()
148 .enumerate()
149 .filter(|(_, s)| s.has_dongzhi)
150 .map(|(i, _)| i)
151 .collect();
152
153 let mut assignments: Vec<Option<(u32, bool)>> = vec![None; spans.len()];
154 for d in 0..dongzhi_indices.len() {
155 let m11 = dongzhi_indices[d];
156 assignments[m11] = Some((11, false));
157 if d + 1 >= dongzhi_indices.len() {
158 continue;
159 }
160 let next_m11 = dongzhi_indices[d + 1];
161 assignments[next_m11] = Some((11, false));
162
163 let intermediate = next_m11 - m11 - 1;
164 let needs_leap = intermediate > 11;
165
166 let mut seq_idx = 0usize;
167 let mut leap_inserted = false;
168 for i in (m11 + 1)..next_m11 {
169 if needs_leap && !leap_inserted && !spans[i].has_zhongqi {
170 let leap_num = if seq_idx == 0 { 11 } else { SEQ[seq_idx - 1] };
171 assignments[i] = Some((leap_num, true));
172 leap_inserted = true;
173 } else {
174 if seq_idx < SEQ.len() {
175 assignments[i] = Some((SEQ[seq_idx], false));
176 }
177 seq_idx += 1;
178 }
179 }
180 }
181
182 spans
183 .iter()
184 .zip(assignments)
185 .filter_map(|(span, a)| {
186 a.map(|(month_number, is_leap_month)| NumberedMonth {
187 month_number,
188 is_leap_month,
189 start_jd: span.start_jd,
190 days: span.days,
191 })
192 })
193 .collect()
194}
195
196thread_local! {
197 static MONTHS_CACHE: RefCell<HashMap<i32, Vec<LunarMonth>>> = RefCell::new(HashMap::new());
201}
202
203#[must_use]
210pub fn lunar_months_for_year(lunar_year: i32) -> Vec<LunarMonth> {
211 if let Some(v) = MONTHS_CACHE.with(|c| c.borrow().get(&lunar_year).cloned()) {
212 return v;
213 }
214 let result = compute_lunar_months_for_year(lunar_year);
215 MONTHS_CACHE.with(|c| c.borrow_mut().insert(lunar_year, result.clone()));
216 result
217}
218
219fn compute_lunar_months_for_year(lunar_year: i32) -> Vec<LunarMonth> {
220 let new_moons = new_moon_jd_uts(lunar_year - 1, lunar_year + 1);
221 let zhongqi = zhongqi_moments(lunar_year - 1, lunar_year + 1);
222 let sequence = build_month_sequence(&new_moons, &zhongqi);
223
224 let month1_idx = sequence
225 .iter()
226 .position(|m| {
227 m.month_number == 1 && !m.is_leap_month && beijing_civil(m.start_jd).year == lunar_year
228 })
229 .unwrap_or_else(|| panic!("cannot find month 1 for lunar year {lunar_year}"));
230
231 let mut result = Vec::new();
232 for (i, m) in sequence.iter().enumerate().skip(month1_idx) {
233 if i > month1_idx && m.month_number == 1 && !m.is_leap_month {
234 break;
235 }
236 result.push(LunarMonth {
237 month_number: m.month_number,
238 is_leap_month: m.is_leap_month,
239 start: beijing_civil(m.start_jd),
240 days: m.days,
241 });
242 }
243 result
244}
245
246#[must_use]
251pub fn lunar_new_year(gregorian_year: i32) -> CivilDate {
252 lunar_months_for_year(gregorian_year)
253 .into_iter()
254 .find(|m| m.month_number == 1 && !m.is_leap_month)
255 .unwrap_or_else(|| panic!("cannot find month 1 for lunar year {gregorian_year}"))
256 .start
257}
258
259#[must_use]
264pub fn gregorian_to_lunisolar(date: CivilDate) -> LunisolarDate {
265 let target_jd = civil_jd(date);
266 for lunar_year in [date.year, date.year - 1, date.year + 1] {
267 let months = lunar_months_for_year(lunar_year);
268 for m in months.iter().rev() {
269 let start_jd = civil_jd(m.start);
270 if target_jd >= start_jd {
271 let lunar_day = (target_jd - start_jd).round() as i64 + 1;
272 if lunar_day >= 1 && lunar_day <= i64::from(m.days) {
273 return LunisolarDate {
274 year: lunar_year,
275 month: m.month_number,
276 day: lunar_day as u32,
277 is_leap_month: m.is_leap_month,
278 };
279 }
280 }
281 }
282 }
283 panic!(
284 "cannot convert {}-{}-{} to a lunisolar date",
285 date.year, date.month, date.day
286 )
287}