1use crate::sun::sun_longitude;
2use std::f64::consts::PI;
16
17#[derive(Debug, Clone, PartialEq)]
19pub struct SolarTerm {
20 pub index: usize,
21 pub name: String,
22 pub description: String,
23 pub longitude: i32,
24 pub current_longitude: f64,
25 pub season: String,
26}
27
28#[derive(Debug, Clone)]
30pub struct SolarTermDef {
31 pub name: &'static str,
32 pub description: &'static str,
33 pub longitude: i32,
34}
35
36pub const TIET_KHI: [SolarTermDef; 24] = [
38 SolarTermDef {
39 name: "Xuân Phân",
40 description: "Spring Equinox (Xuân Phân)",
41 longitude: 0,
42 },
43 SolarTermDef {
44 name: "Thanh Minh",
45 description: "Pure Brightness (Thanh Minh)",
46 longitude: 15,
47 },
48 SolarTermDef {
49 name: "Cốc Vũ",
50 description: "Grain Rain (Cốc Vũ)",
51 longitude: 30,
52 },
53 SolarTermDef {
54 name: "Lập Hạ",
55 description: "Start of Summer (Lập Hạ)",
56 longitude: 45,
57 },
58 SolarTermDef {
59 name: "Tiểu Mãn",
60 description: "Grain Buds (Tiểu Mãn)",
61 longitude: 60,
62 },
63 SolarTermDef {
64 name: "Mang Chủng",
65 description: "Grain in Ear (Mang Chủng)",
66 longitude: 75,
67 },
68 SolarTermDef {
69 name: "Hạ Chí",
70 description: "Summer Solstice (Hạ Chí)",
71 longitude: 90,
72 },
73 SolarTermDef {
74 name: "Tiểu Thử",
75 description: "Slight Heat (Tiểu Thử)",
76 longitude: 105,
77 },
78 SolarTermDef {
79 name: "Đại Thử",
80 description: "Great Heat (Đại Thử)",
81 longitude: 120,
82 },
83 SolarTermDef {
84 name: "Lập Thu",
85 description: "Start of Autumn (Lập Thu)",
86 longitude: 135,
87 },
88 SolarTermDef {
89 name: "Xử Thử",
90 description: "End of Heat (Xử Thử)",
91 longitude: 150,
92 },
93 SolarTermDef {
94 name: "Bạch Lộ",
95 description: "White Dew (Bạch Lộ)",
96 longitude: 165,
97 },
98 SolarTermDef {
99 name: "Thu Phân",
100 description: "Autumn Equinox (Thu Phân)",
101 longitude: 180,
102 },
103 SolarTermDef {
104 name: "Hàn Lộ",
105 description: "Cold Dew (Hàn Lộ)",
106 longitude: 195,
107 },
108 SolarTermDef {
109 name: "Sương Giáng",
110 description: "Frost Descent (Sương Giáng)",
111 longitude: 210,
112 },
113 SolarTermDef {
114 name: "Lập Đông",
115 description: "Start of Winter (Lập Đông)",
116 longitude: 225,
117 },
118 SolarTermDef {
119 name: "Tiểu Tuyết",
120 description: "Slight Snow (Tiểu Tuyết)",
121 longitude: 240,
122 },
123 SolarTermDef {
124 name: "Đại Tuyết",
125 description: "Great Snow (Đại Tuyết)",
126 longitude: 255,
127 },
128 SolarTermDef {
129 name: "Đông Chí",
130 description: "Winter Solstice (Đông Chí)",
131 longitude: 270,
132 },
133 SolarTermDef {
134 name: "Tiểu Hàn",
135 description: "Slight Cold (Tiểu Hàn)",
136 longitude: 285,
137 },
138 SolarTermDef {
139 name: "Đại Hàn",
140 description: "Great Cold (Đại Hàn)",
141 longitude: 300,
142 },
143 SolarTermDef {
144 name: "Lập Xuân",
145 description: "Start of Spring (Lập Xuân)",
146 longitude: 315,
147 },
148 SolarTermDef {
149 name: "Vũ Thủy",
150 description: "Rain Water (Vũ Thủy)",
151 longitude: 330,
152 },
153 SolarTermDef {
154 name: "Kinh Trập",
155 description: "Awakening of Insects (Kinh Trập)",
156 longitude: 345,
157 },
158];
159
160pub fn get_season(term_index: usize) -> &'static str {
168 match term_index {
169 0..=5 => "Xuân (Spring)",
170 6..=11 => "Hạ (Summer)",
171 12..=17 => "Thu (Autumn)",
172 _ => "Đông (Winter)",
173 }
174}
175
176pub fn get_tiet_khi(jd: i32, time_zone: f64) -> SolarTerm {
185 let sun_long_rad = sun_longitude(jd as f64 - 0.5 - time_zone / 24.0);
187
188 let sun_long_deg = (sun_long_rad * 180.0 / PI) % 360.0;
190
191 let term_index = (sun_long_deg / 15.0).floor() as usize;
193
194 let term = &TIET_KHI[term_index];
195
196 SolarTerm {
197 index: term_index,
198 name: term.name.to_string(),
199 description: term.description.to_string(),
200 longitude: term.longitude,
201 current_longitude: (sun_long_deg * 100.0).round() / 100.0, season: get_season(term_index).to_string(),
203 }
204}
205
206#[derive(Debug, Clone)]
208pub struct SolarTermWithDate {
209 pub jd: i32,
210 pub index: usize,
211 pub name: String,
212 pub description: String,
213 pub longitude: i32,
214 pub current_longitude: f64,
215 pub season: String,
216}
217
218pub fn get_all_tiet_khi_for_year(year: i32, time_zone: f64) -> Vec<SolarTermWithDate> {
228 use crate::julian::jd_from_date;
229
230 let mut terms = Vec::new();
231 let start_jd = jd_from_date(1, 1, year);
232 let end_jd = jd_from_date(31, 12, year);
233
234 let mut prev_term_index: Option<usize> = None;
235
236 for jd in start_jd..=end_jd {
237 let tiet_khi = get_tiet_khi(jd, time_zone);
238
239 if Some(tiet_khi.index) != prev_term_index {
241 terms.push(SolarTermWithDate {
242 jd,
243 index: tiet_khi.index,
244 name: tiet_khi.name.clone(),
245 description: tiet_khi.description.clone(),
246 longitude: tiet_khi.longitude,
247 current_longitude: tiet_khi.current_longitude,
248 season: tiet_khi.season.clone(),
249 });
250 prev_term_index = Some(tiet_khi.index);
251 }
252 }
253
254 terms
255}
256
257#[cfg(test)]
258mod tests {
259 use super::*;
260 use crate::julian::jd_from_date;
261
262 #[test]
263 fn test_tiet_khi_constants() {
264 assert_eq!(TIET_KHI.len(), 24);
265 assert_eq!(TIET_KHI[0].name, "Xuân Phân");
266 assert_eq!(TIET_KHI[0].longitude, 0);
267 assert_eq!(TIET_KHI[12].name, "Thu Phân");
268 assert_eq!(TIET_KHI[12].longitude, 180);
269 }
270
271 #[test]
272 fn test_get_season() {
273 assert_eq!(get_season(0), "Xuân (Spring)");
274 assert_eq!(get_season(5), "Xuân (Spring)");
275 assert_eq!(get_season(6), "Hạ (Summer)");
276 assert_eq!(get_season(11), "Hạ (Summer)");
277 assert_eq!(get_season(12), "Thu (Autumn)");
278 assert_eq!(get_season(17), "Thu (Autumn)");
279 assert_eq!(get_season(18), "Đông (Winter)");
280 assert_eq!(get_season(23), "Đông (Winter)");
281 }
282
283 #[test]
284 fn test_get_tiet_khi_basic() {
285 let jd = jd_from_date(10, 2, 2024);
287 let tiet_khi = get_tiet_khi(jd, 7.0);
288
289 assert!(tiet_khi.index < 24);
291 assert!(!tiet_khi.name.is_empty());
292 assert!(tiet_khi.current_longitude >= 0.0 && tiet_khi.current_longitude < 360.0);
293 }
294
295 #[test]
296 fn test_get_tiet_khi_march_equinox() {
297 let jd = jd_from_date(21, 3, 2024);
299 let tiet_khi = get_tiet_khi(jd, 7.0);
300
301 assert!(
303 tiet_khi.index == 0 || tiet_khi.index == 1 || tiet_khi.index == 23,
304 "Expected index 0, 1, or 23 for March 21, got {}",
305 tiet_khi.index
306 );
307 }
308
309 #[test]
310 fn test_get_tiet_khi_summer_solstice() {
311 let jd = jd_from_date(21, 6, 2024);
313 let tiet_khi = get_tiet_khi(jd, 7.0);
314
315 assert!(
317 tiet_khi.index >= 5 && tiet_khi.index <= 7,
318 "Expected index 6±1, got {}",
319 tiet_khi.index
320 );
321 }
322
323 #[test]
324 fn test_get_tiet_khi_winter_solstice() {
325 let jd = jd_from_date(21, 12, 2024);
327 let tiet_khi = get_tiet_khi(jd, 7.0);
328
329 assert!(
331 tiet_khi.index >= 17 && tiet_khi.index <= 19,
332 "Expected index 18±1, got {}",
333 tiet_khi.index
334 );
335 }
336
337 #[test]
338 fn test_get_all_tiet_khi_for_year() {
339 let terms = get_all_tiet_khi_for_year(2024, 7.0);
340
341 assert!(
343 terms.len() >= 23 && terms.len() <= 25,
344 "Expected 23-25 terms, got {}",
345 terms.len()
346 );
347
348 for i in 1..terms.len() {
350 assert!(
351 terms[i].jd > terms[i - 1].jd,
352 "Terms not in chronological order"
353 );
354 }
355 }
356
357 #[test]
358 fn test_solar_term_progression() {
359 let jd_spring = jd_from_date(1, 4, 2024);
361 let jd_summer = jd_from_date(1, 7, 2024);
362 let jd_autumn = jd_from_date(1, 10, 2024);
363 let jd_winter = jd_from_date(1, 1, 2024);
364
365 let term_spring = get_tiet_khi(jd_spring, 7.0);
366 let term_summer = get_tiet_khi(jd_summer, 7.0);
367 let term_autumn = get_tiet_khi(jd_autumn, 7.0);
368 let term_winter = get_tiet_khi(jd_winter, 7.0);
369
370 assert!(term_spring.season.contains("Spring") || term_spring.season.contains("Summer"));
372 assert!(term_summer.season.contains("Summer"));
373 assert!(term_autumn.season.contains("Autumn"));
374 assert!(term_winter.season.contains("Winter") || term_winter.season.contains("Spring"));
375 }
376}