1use chrono::{Datelike, NaiveDate, Weekday};
37use std::collections::HashMap;
38
39#[derive(Debug, Clone)]
41pub struct HolidayInfo {
42 pub date: NaiveDate,
43 pub name: &'static str,
45 pub is_holiday: bool,
47 pub is_confirmed: bool,
49}
50
51pub fn get_china_holidays() -> HashMap<NaiveDate, HolidayInfo> {
77 let mut holidays: Vec<HolidayInfo> = Vec::new();
78
79 holidays.push(HolidayInfo {
84 date: NaiveDate::from_ymd_opt(2026, 1, 1).unwrap(),
85 name: "元旦", is_holiday: true, is_confirmed: true,
86 });
87
88 for d in 0..7 {
89 holidays.push(HolidayInfo {
90 date: NaiveDate::from_ymd_opt(2026, 2, 15).unwrap() + chrono::Duration::days(d),
91 name: "春节", is_holiday: true, is_confirmed: true,
92 });
93 }
94 holidays.push(HolidayInfo {
95 date: NaiveDate::from_ymd_opt(2026, 2, 14).unwrap(),
96 name: "春节调休", is_holiday: false, is_confirmed: true,
97 });
98 holidays.push(HolidayInfo {
99 date: NaiveDate::from_ymd_opt(2026, 2, 28).unwrap(),
100 name: "春节调休", is_holiday: false, is_confirmed: true,
101 });
102
103 holidays.push(HolidayInfo {
104 date: NaiveDate::from_ymd_opt(2026, 4, 5).unwrap(),
105 name: "清明节", is_holiday: true, is_confirmed: true,
106 });
107 holidays.push(HolidayInfo {
108 date: NaiveDate::from_ymd_opt(2026, 4, 6).unwrap(),
109 name: "清明节", is_holiday: true, is_confirmed: true,
110 });
111
112 for d in 0..5 {
113 holidays.push(HolidayInfo {
114 date: NaiveDate::from_ymd_opt(2026, 5, 1).unwrap() + chrono::Duration::days(d),
115 name: "劳动节", is_holiday: true, is_confirmed: true,
116 });
117 }
118 holidays.push(HolidayInfo {
119 date: NaiveDate::from_ymd_opt(2026, 5, 9).unwrap(),
120 name: "劳动节调休", is_holiday: false, is_confirmed: true,
121 });
122
123 for d in 0..3 {
124 holidays.push(HolidayInfo {
125 date: NaiveDate::from_ymd_opt(2026, 6, 19).unwrap() + chrono::Duration::days(d),
126 name: "端午节", is_holiday: true, is_confirmed: true,
127 });
128 }
129
130 for d in 0..3 {
131 holidays.push(HolidayInfo {
132 date: NaiveDate::from_ymd_opt(2026, 9, 25).unwrap() + chrono::Duration::days(d),
133 name: "中秋节", is_holiday: true, is_confirmed: true,
134 });
135 }
136
137 for d in 0..7 {
138 holidays.push(HolidayInfo {
139 date: NaiveDate::from_ymd_opt(2026, 10, 1).unwrap() + chrono::Duration::days(d),
140 name: "国庆节", is_holiday: true, is_confirmed: true,
141 });
142 }
143 holidays.push(HolidayInfo {
144 date: NaiveDate::from_ymd_opt(2026, 9, 27).unwrap(),
145 name: "国庆节调休", is_holiday: false, is_confirmed: true,
146 });
147 holidays.push(HolidayInfo {
148 date: NaiveDate::from_ymd_opt(2026, 10, 10).unwrap(),
149 name: "国庆节调休", is_holiday: false, is_confirmed: true,
150 });
151
152 for d in 0..3 {
157 holidays.push(HolidayInfo {
158 date: NaiveDate::from_ymd_opt(2027, 1, 1).unwrap() + chrono::Duration::days(d),
159 name: "元旦[待确认]", is_holiday: true, is_confirmed: false,
160 });
161 }
162
163 for d in 0..7 {
164 holidays.push(HolidayInfo {
165 date: NaiveDate::from_ymd_opt(2027, 2, 5).unwrap() + chrono::Duration::days(d),
166 name: "春节[待确认]", is_holiday: true, is_confirmed: false,
167 });
168 }
169 holidays.push(HolidayInfo {
170 date: NaiveDate::from_ymd_opt(2027, 1, 31).unwrap(),
171 name: "春节调休[待确认]", is_holiday: false, is_confirmed: false,
172 });
173 holidays.push(HolidayInfo {
174 date: NaiveDate::from_ymd_opt(2027, 2, 13).unwrap(),
175 name: "春节调休[待确认]", is_holiday: false, is_confirmed: false,
176 });
177
178 holidays.push(HolidayInfo {
179 date: NaiveDate::from_ymd_opt(2027, 4, 5).unwrap(),
180 name: "清明节[待确认]", is_holiday: true, is_confirmed: false,
181 });
182
183 for d in 0..5 {
184 holidays.push(HolidayInfo {
185 date: NaiveDate::from_ymd_opt(2027, 5, 1).unwrap() + chrono::Duration::days(d),
186 name: "劳动节[待确认]", is_holiday: true, is_confirmed: false,
187 });
188 }
189 holidays.push(HolidayInfo {
190 date: NaiveDate::from_ymd_opt(2027, 5, 8).unwrap(),
191 name: "劳动节调休[待确认]", is_holiday: false, is_confirmed: false,
192 });
193
194 holidays.into_iter().map(|h| (h.date, h)).collect()
195}
196
197pub fn is_weekend(date: NaiveDate) -> bool {
209 matches!(date.weekday(), Weekday::Sat | Weekday::Sun)
210}
211
212pub fn is_naturally_off(date: NaiveDate, holidays: &HashMap<NaiveDate, HolidayInfo>) -> bool {
230 if let Some(info) = holidays.get(&date) {
231 return info.is_holiday;
232 }
233 is_weekend(date)
234}
235
236#[cfg(test)]
237mod tests {
238 use super::*;
239
240 #[test]
241 fn all_2026_confirmed() {
242 let holidays = get_china_holidays();
243 let confirmed_2026: Vec<_> = holidays
244 .values()
245 .filter(|h| h.date.year() == 2026 && h.is_confirmed)
246 .collect();
247 assert!(confirmed_2026.len() >= 30);
248 }
249
250 #[test]
251 fn all_2027_unconfirmed() {
252 let holidays = get_china_holidays();
253 let unconfirmed_2027: Vec<_> = holidays.values().filter(|h| h.date.year() == 2027).collect();
254 assert!(!unconfirmed_2027.is_empty());
255 for h in unconfirmed_2027 {
256 assert!(!h.is_confirmed);
257 assert!(h.name.contains("[待确认]"));
258 }
259 }
260
261 #[test]
262 fn new_years_day_is_holiday() {
263 let holidays = get_china_holidays();
264 let d = NaiveDate::from_ymd_opt(2026, 1, 1).unwrap();
265 let info = holidays.get(&d).unwrap();
266 assert!(info.is_holiday);
267 assert_eq!(info.name, "元旦");
268 }
269
270 #[test]
271 fn spring_festival_adjustment_is_workday() {
272 let holidays = get_china_holidays();
273 let d = NaiveDate::from_ymd_opt(2026, 2, 14).unwrap();
274 let info = holidays.get(&d).unwrap();
275 assert!(!info.is_holiday);
276 }
277
278 #[test]
279 fn national_day_is_seven_days() {
280 let holidays = get_china_holidays();
281 let count = holidays.values().filter(|h| h.date.year() == 2026 && h.name == "国庆节").count();
282 assert_eq!(count, 7);
283 }
284
285 #[test]
286 fn labour_day_is_five_days() {
287 let holidays = get_china_holidays();
288 let count = holidays.values().filter(|h| h.date.year() == 2026 && h.name == "劳动节").count();
289 assert_eq!(count, 5);
290 }
291
292 #[test]
293 fn is_weekend_saturday_and_sunday() {
294 assert!(is_weekend(NaiveDate::from_ymd_opt(2026, 5, 23).unwrap()));
295 assert!(is_weekend(NaiveDate::from_ymd_opt(2026, 5, 24).unwrap()));
296 assert!(!is_weekend(NaiveDate::from_ymd_opt(2026, 5, 22).unwrap()));
297 }
298
299 #[test]
300 fn is_naturally_off_respects_adjusted_workday() {
301 let holidays = get_china_holidays();
302 let d = NaiveDate::from_ymd_opt(2026, 2, 14).unwrap();
303 assert!(!is_naturally_off(d, &holidays));
304 }
305
306 #[test]
307 fn is_naturally_off_for_regular_holiday() {
308 let holidays = get_china_holidays();
309 let d = NaiveDate::from_ymd_opt(2026, 1, 1).unwrap();
310 assert!(is_naturally_off(d, &holidays));
311 }
312
313 #[test]
314 fn holiday_data_contains_no_duplicate_dates() {
315 let holidays = get_china_holidays();
316 let mut dates: Vec<_> = holidays.keys().copied().collect();
317 let before = dates.len();
318 dates.sort();
319 dates.dedup();
320 assert_eq!(before, dates.len(), "Duplicate dates found in holiday data");
321 }
322
323 #[test]
324 fn dragon_boat_festival_is_three_days() {
325 let holidays = get_china_holidays();
326 let count = holidays.values().filter(|h| h.date.year() == 2026 && h.name == "端午节").count();
327 assert_eq!(count, 3);
328 }
329
330 #[test]
331 fn mid_autumn_festival_is_two_days_effective() {
332 let holidays = get_china_holidays();
333 let count = holidays.values().filter(|h| h.date.year() == 2026 && h.name == "中秋节").count();
334 assert_eq!(count, 2);
335 }
336}