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