1use crate::holiday_data::{lunar_festivals, solar_holidays};
2use crate::julian::{jd_from_date, jd_to_date};
9use crate::lunar::{convert_lunar_to_solar, LunarDate};
10use crate::tietkhi::get_all_tiet_khi_for_year;
11use crate::types::VIETNAM_TIMEZONE;
12
13#[derive(Debug, Clone)]
15pub struct Holiday {
16 pub name: String,
17 pub description: String,
18 pub lunar_date: Option<LunarDate>,
19 pub solar_day: i32,
20 pub solar_month: i32,
21 pub solar_year: i32,
22 pub is_solar: bool,
23 pub category: String,
24 pub is_major: bool,
25}
26
27#[derive(Debug, Clone)]
28pub struct UpcomingEvent {
29 pub name: String,
30 pub jd: i32,
31 pub days_left: i32,
32 pub is_lunar: bool,
33}
34
35pub fn get_upcoming_events(
36 current_jd: i32,
37 current_solar_year: i32,
38 limit_days: i32,
39) -> Vec<UpcomingEvent> {
40 let mut all_holidays = get_vietnamese_holidays(current_solar_year);
41 all_holidays.extend(get_vietnamese_holidays(current_solar_year + 1));
43
44 let mut upcoming = Vec::new();
45
46 let mut seen_jds = std::collections::HashSet::new();
48
49 all_holidays.sort_by(|a, b| {
52 let jd_a = jd_from_date(a.solar_day, a.solar_month, a.solar_year);
53 let jd_b = jd_from_date(b.solar_day, b.solar_month, b.solar_year);
54 jd_a.cmp(&jd_b).then_with(|| b.is_major.cmp(&a.is_major))
55 });
56
57 for h in all_holidays {
58 let jd = jd_from_date(h.solar_day, h.solar_month, h.solar_year);
59 let days_left = jd - current_jd;
60 if days_left > 0 && days_left <= limit_days {
61 if !seen_jds.contains(&jd) {
62 seen_jds.insert(jd);
63 upcoming.push(UpcomingEvent {
64 name: h.name.clone(),
65 jd,
66 days_left,
67 is_lunar: !h.is_solar,
68 });
69 }
70 }
71 }
72
73 upcoming.sort_by_key(|e| e.jd);
75 upcoming
76}
77
78struct LunarHolidayInput<'a> {
79 name: &'a str,
80 lunar_day: i32,
81 lunar_month: i32,
82 lunar_year: i32,
83 description: &'a str,
84 category: &'a str,
85 is_major: bool,
86}
87
88fn create_lunar_holiday(input: LunarHolidayInput<'_>, time_zone: f64) -> Option<Holiday> {
90 let solar = convert_lunar_to_solar(
91 input.lunar_day,
92 input.lunar_month,
93 input.lunar_year,
94 false,
95 time_zone,
96 );
97 if solar.0 > 0 {
98 Some(Holiday {
99 name: input.name.to_string(),
100 description: input.description.to_string(),
101 lunar_date: Some(LunarDate {
102 day: input.lunar_day,
103 month: input.lunar_month,
104 year: input.lunar_year,
105 is_leap: false,
106 }),
107 solar_day: solar.0,
108 solar_month: solar.1,
109 solar_year: solar.2,
110 is_solar: false,
111 category: input.category.to_string(),
112 is_major: input.is_major,
113 })
114 } else {
115 None
116 }
117}
118
119fn nth_weekday_of_month(year: i32, month: i32, weekday: usize, nth: i32) -> (i32, i32, i32) {
120 let first_jd = jd_from_date(1, month, year);
121 let first_weekday = (first_jd + 1) % 7;
122 let target_weekday = weekday as i32;
123 let offset = (7 + target_weekday - first_weekday) % 7;
124 let day = 1 + offset + 7 * (nth - 1);
125 (day, month, year)
126}
127
128pub fn get_vietnamese_holidays(solar_year: i32) -> Vec<Holiday> {
136 let time_zone = VIETNAM_TIMEZONE;
137 let mut holidays = Vec::new();
138
139 for festival in lunar_festivals() {
141 if festival.is_solar {
143 continue;
144 }
145
146 let name = &festival.names.vi[0];
147 let description = &festival.names.en[0];
148 let lunar_year = solar_year + festival.year_offset;
149
150 if let Some(h) = create_lunar_holiday(
151 LunarHolidayInput {
152 name,
153 lunar_day: festival.lunar_day,
154 lunar_month: festival.lunar_month,
155 lunar_year,
156 description,
157 category: &festival.category,
158 is_major: festival.is_major,
159 },
160 time_zone,
161 ) {
162 holidays.push(h);
163 }
164 }
165
166 let thanh_minh_date = get_all_tiet_khi_for_year(solar_year, time_zone)
168 .into_iter()
169 .find(|t| t.name == "Thanh Minh")
170 .map(|t| jd_to_date(t.jd))
171 .unwrap_or((5, 4, solar_year));
172
173 holidays.push(Holiday {
174 name: "Tết Thanh Minh".to_string(),
175 description: "Tomb Sweeping Day (Solar calendar)".to_string(),
176 lunar_date: None,
177 solar_day: thanh_minh_date.0,
178 solar_month: thanh_minh_date.1,
179 solar_year: thanh_minh_date.2,
180 is_solar: true,
181 category: "festival".to_string(),
182 is_major: true,
183 });
184
185 for holiday_data in solar_holidays() {
187 let name = &holiday_data.names.vi[0];
188 let description = &holiday_data.names.en[0];
189
190 holidays.push(Holiday {
191 name: name.clone(),
192 description: description.clone(),
193 lunar_date: None,
194 solar_day: holiday_data.solar_day,
195 solar_month: holiday_data.solar_month,
196 solar_year,
197 is_solar: true,
198 category: holiday_data.category.clone(),
199 is_major: holiday_data.is_major,
200 });
201 }
202
203 let mothers_day = nth_weekday_of_month(solar_year, 5, 0, 2);
204 holidays.push(Holiday {
205 name: "Ngày của Mẹ".to_string(),
206 description: "Mother's Day (2nd Sunday of May)".to_string(),
207 lunar_date: None,
208 solar_day: mothers_day.0,
209 solar_month: mothers_day.1,
210 solar_year: mothers_day.2,
211 is_solar: true,
212 category: "social".to_string(),
213 is_major: true,
214 });
215
216 let fathers_day = nth_weekday_of_month(solar_year, 6, 0, 3);
217 holidays.push(Holiday {
218 name: "Ngày của Cha".to_string(),
219 description: "Father's Day (3rd Sunday of June)".to_string(),
220 lunar_date: None,
221 solar_day: fathers_day.0,
222 solar_month: fathers_day.1,
223 solar_year: fathers_day.2,
224 is_solar: true,
225 category: "social".to_string(),
226 is_major: true,
227 });
228
229 for month in 1..=12 {
231 let mung_mot_name = format!("Mùng 1 tháng {}", month);
232 if let Some(h) = create_lunar_holiday(
233 LunarHolidayInput {
234 name: &mung_mot_name,
235 lunar_day: 1,
236 lunar_month: month,
237 lunar_year: solar_year,
238 description: "First day of lunar month",
239 category: "lunar-cycle",
240 is_major: false,
241 },
242 time_zone,
243 ) {
244 holidays.push(h);
245 }
246
247 let ram_name = format!("Rằm tháng {}", month);
248 if let Some(h) = create_lunar_holiday(
249 LunarHolidayInput {
250 name: &ram_name,
251 lunar_day: 15,
252 lunar_month: month,
253 lunar_year: solar_year,
254 description: "Full moon day",
255 category: "lunar-cycle",
256 is_major: false,
257 },
258 time_zone,
259 ) {
260 holidays.push(h);
261 }
262 }
263
264 holidays.sort_by(|a, b| {
266 let date_a = (a.solar_year, a.solar_month, a.solar_day);
267 let date_b = (b.solar_year, b.solar_month, b.solar_day);
268 date_a.cmp(&date_b)
269 });
270
271 holidays
272}
273
274pub fn get_major_holidays(solar_year: i32) -> Vec<Holiday> {
282 get_vietnamese_holidays(solar_year)
283 .into_iter()
284 .filter(|h| h.is_major)
285 .collect()
286}
287
288#[cfg(test)]
289mod tests {
290 use super::*;
291
292 #[test]
293 fn test_get_vietnamese_holidays_2024() {
294 let holidays = get_vietnamese_holidays(2024);
295
296 assert!(holidays.len() > 20, "Should have many holidays");
298
299 for i in 1..holidays.len() {
301 let date_prev = (
302 holidays[i - 1].solar_year,
303 holidays[i - 1].solar_month,
304 holidays[i - 1].solar_day,
305 );
306 let date_curr = (
307 holidays[i].solar_year,
308 holidays[i].solar_month,
309 holidays[i].solar_day,
310 );
311 assert!(date_curr >= date_prev, "Holidays should be sorted by date");
312 }
313 }
314
315 #[test]
316 fn test_tet_nguyen_dan_present() {
317 let holidays = get_vietnamese_holidays(2024);
318
319 let tet = holidays.iter().find(|h| h.name.contains("Tết Nguyên Đán"));
321 assert!(tet.is_some(), "Should have Tết Nguyên Đán");
322
323 let tet = tet.unwrap();
324 assert_eq!(tet.solar_day, 10);
326 assert_eq!(tet.solar_month, 2);
327 assert_eq!(tet.solar_year, 2024);
328 }
329
330 #[test]
331 fn test_thanh_minh_is_solar() {
332 let holidays = get_vietnamese_holidays(2024);
333
334 let thanh_minh = holidays.iter().find(|h| h.name.contains("Thanh Minh"));
335 assert!(thanh_minh.is_some(), "Should have Thanh Minh");
336
337 let thanh_minh = thanh_minh.unwrap();
338 assert!(thanh_minh.is_solar, "Thanh Minh should be solar-based");
339 assert_eq!(thanh_minh.solar_month, 4);
340 assert_eq!(thanh_minh.solar_day, 5);
341 }
342
343 #[test]
344 fn test_trung_thu_present() {
345 let holidays = get_vietnamese_holidays(2024);
346
347 let trung_thu = holidays.iter().find(|h| h.name.contains("Trung Thu"));
348 assert!(trung_thu.is_some(), "Should have Tết Trung Thu");
349
350 let trung_thu = trung_thu.unwrap();
352 assert!(trung_thu.lunar_date.is_some());
353 let lunar = trung_thu.lunar_date.as_ref().unwrap();
354 assert_eq!(lunar.day, 15);
355 assert_eq!(lunar.month, 8);
356 }
357
358 #[test]
359 fn test_get_major_holidays() {
360 let all = get_vietnamese_holidays(2024);
361 let major = get_major_holidays(2024);
362
363 assert!(major.len() < all.len(), "Major holidays should be a subset");
365
366 assert!(major.iter().any(|h| h.name.contains("Tết Nguyên Đán")));
368
369 assert!(major.iter().any(|h| h.name.contains("Trung Thu")));
371 }
372
373 #[test]
374 fn test_lunar_dates_populated() {
375 let holidays = get_vietnamese_holidays(2024);
376
377 let with_lunar = holidays.iter().filter(|h| h.lunar_date.is_some()).count();
379 assert!(with_lunar > 20, "Most holidays should have lunar dates");
380 }
381
382 #[test]
383 fn test_floating_mother_father_days() {
384 let holidays = get_vietnamese_holidays(2024);
385
386 let mother_day = holidays.iter().find(|h| h.name == "Ngày của Mẹ");
387 assert!(mother_day.is_some(), "Should have Mother's Day");
388 let mother_day = mother_day.unwrap();
389 assert_eq!(mother_day.solar_day, 12);
390 assert_eq!(mother_day.solar_month, 5);
391
392 let father_day = holidays.iter().find(|h| h.name == "Ngày của Cha");
393 assert!(father_day.is_some(), "Should have Father's Day");
394 let father_day = father_day.unwrap();
395 assert_eq!(father_day.solar_day, 16);
396 assert_eq!(father_day.solar_month, 6);
397 }
398}