automapper_validation/eval/
werktag.rs1use chrono::{Datelike, Duration, NaiveDate, Weekday};
43
44pub fn compute_easter(year: i32) -> NaiveDate {
48 let a = year % 19;
49 let b = year / 100;
50 let c = year % 100;
51 let d = b / 4;
52 let e = b % 4;
53 let f = (b + 8) / 25;
54 let g = (b - f + 1) / 3;
55 let h = (19 * a + b - d - g + 15) % 30;
56 let i = c / 4;
57 let k = c % 4;
58 let l = (32 + 2 * e + 2 * i - h - k) % 7;
59 let m = (a + 11 * h + 22 * l) / 451;
60 let month = ((h + l - 7 * m + 114) / 31) as u32;
61 let day = (((h + l - 7 * m + 114) % 31) + 1) as u32;
62 NaiveDate::from_ymd_opt(year, month, day).expect("easter date is always valid")
63}
64
65pub fn buss_und_bettag(year: i32) -> NaiveDate {
67 let nov_23 = NaiveDate::from_ymd_opt(year, 11, 23).expect("23.11. is always valid");
68 let days_back = match nov_23.weekday() {
71 Weekday::Wed => 7,
72 Weekday::Thu => 1,
73 Weekday::Fri => 2,
74 Weekday::Sat => 3,
75 Weekday::Sun => 4,
76 Weekday::Mon => 5,
77 Weekday::Tue => 6,
78 };
79 nov_23 - Duration::days(days_back)
80}
81
82pub fn is_german_holiday(date: NaiveDate) -> bool {
84 let (year, month, day) = (date.year(), date.month(), date.day());
85 if matches!(
87 (month, day),
88 (1, 1) | (1, 6) | (3, 8) | (5, 1) | (8, 15) | (9, 20) | (10, 3) | (10, 31) | (11, 1) | (12, 24) | (12, 25) | (12, 26) | (12, 31) ) {
102 return true;
103 }
104 let easter = compute_easter(year);
106 if date == easter - Duration::days(2)
107 || date == easter + Duration::days(1)
108 || date == easter + Duration::days(39)
109 || date == easter + Duration::days(50)
110 || date == easter + Duration::days(60)
111 {
112 return true;
113 }
114 if month == 11 && date == buss_und_bettag(year) {
116 return true;
117 }
118 false
119}
120
121pub fn is_werktag(date: NaiveDate) -> bool {
123 !matches!(date.weekday(), Weekday::Sat | Weekday::Sun) && !is_german_holiday(date)
124}
125
126pub fn werktage_between(from: NaiveDate, to: NaiveDate) -> i32 {
135 if from == to {
136 return 0;
137 }
138 let (start, end, sign) = if from < to {
139 (from, to, 1)
140 } else {
141 (to, from, -1)
142 };
143 let mut count = 0;
144 let mut cur = start;
145 while cur < end {
146 cur = cur
147 .succ_opt()
148 .expect("succ_opt within validated date range");
149 if is_werktag(cur) {
150 count += 1;
151 }
152 }
153 count * sign
154}
155
156pub fn parse_ccyymmdd_prefix(value: &str) -> Option<NaiveDate> {
159 let s = value.trim();
160 if s.len() < 8 {
161 return None;
162 }
163 let year: i32 = s.get(0..4).and_then(|v| v.parse().ok())?;
164 let month: u32 = s.get(4..6).and_then(|v| v.parse().ok())?;
165 let day: u32 = s.get(6..8).and_then(|v| v.parse().ok())?;
166 NaiveDate::from_ymd_opt(year, month, day)
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172
173 #[test]
174 fn test_easter_2024() {
175 assert_eq!(
177 compute_easter(2024),
178 NaiveDate::from_ymd_opt(2024, 3, 31).unwrap()
179 );
180 }
181
182 #[test]
183 fn test_easter_2025() {
184 assert_eq!(
186 compute_easter(2025),
187 NaiveDate::from_ymd_opt(2025, 4, 20).unwrap()
188 );
189 }
190
191 #[test]
192 fn test_easter_2026() {
193 assert_eq!(
195 compute_easter(2026),
196 NaiveDate::from_ymd_opt(2026, 4, 5).unwrap()
197 );
198 }
199
200 #[test]
201 fn test_fixed_federal_holidays_2026() {
202 assert!(is_german_holiday(
203 NaiveDate::from_ymd_opt(2026, 1, 1).unwrap()
204 ));
205 assert!(is_german_holiday(
206 NaiveDate::from_ymd_opt(2026, 5, 1).unwrap()
207 ));
208 assert!(is_german_holiday(
209 NaiveDate::from_ymd_opt(2026, 10, 3).unwrap()
210 ));
211 assert!(is_german_holiday(
212 NaiveDate::from_ymd_opt(2026, 12, 25).unwrap()
213 ));
214 assert!(is_german_holiday(
215 NaiveDate::from_ymd_opt(2026, 12, 26).unwrap()
216 ));
217 }
218
219 #[test]
220 fn test_state_level_holidays_included_bundesweit_2026() {
221 assert!(is_german_holiday(
223 NaiveDate::from_ymd_opt(2026, 1, 6).unwrap()
224 )); assert!(is_german_holiday(
226 NaiveDate::from_ymd_opt(2026, 3, 8).unwrap()
227 )); assert!(is_german_holiday(
229 NaiveDate::from_ymd_opt(2026, 8, 15).unwrap()
230 )); assert!(is_german_holiday(
232 NaiveDate::from_ymd_opt(2026, 9, 20).unwrap()
233 )); assert!(is_german_holiday(
235 NaiveDate::from_ymd_opt(2026, 10, 31).unwrap()
236 )); assert!(is_german_holiday(
238 NaiveDate::from_ymd_opt(2026, 11, 1).unwrap()
239 )); }
241
242 #[test]
243 fn test_bdew_specific_holidays_2026() {
244 assert!(is_german_holiday(
246 NaiveDate::from_ymd_opt(2026, 12, 24).unwrap()
247 ));
248 assert!(is_german_holiday(
249 NaiveDate::from_ymd_opt(2026, 12, 31).unwrap()
250 ));
251 }
252
253 #[test]
254 fn test_fronleichnam_2026() {
255 assert!(is_german_holiday(
257 NaiveDate::from_ymd_opt(2026, 6, 4).unwrap()
258 ));
259 }
260
261 #[test]
262 fn test_buss_und_bettag() {
263 assert_eq!(
266 buss_und_bettag(2024),
267 NaiveDate::from_ymd_opt(2024, 11, 20).unwrap()
268 );
269 assert_eq!(
271 buss_und_bettag(2025),
272 NaiveDate::from_ymd_opt(2025, 11, 19).unwrap()
273 );
274 assert_eq!(
276 buss_und_bettag(2026),
277 NaiveDate::from_ymd_opt(2026, 11, 18).unwrap()
278 );
279 assert_eq!(
281 buss_und_bettag(2022),
282 NaiveDate::from_ymd_opt(2022, 11, 16).unwrap()
283 );
284 }
285
286 #[test]
287 fn test_buss_und_bettag_is_holiday() {
288 assert!(is_german_holiday(
289 NaiveDate::from_ymd_opt(2026, 11, 18).unwrap()
290 ));
291 assert!(!is_german_holiday(
293 NaiveDate::from_ymd_opt(2026, 11, 11).unwrap()
294 ));
295 assert!(!is_german_holiday(
296 NaiveDate::from_ymd_opt(2026, 11, 25).unwrap()
297 ));
298 }
299
300 #[test]
301 fn test_easter_holidays_2026() {
302 assert!(is_german_holiday(
308 NaiveDate::from_ymd_opt(2026, 4, 3).unwrap()
309 ));
310 assert!(is_german_holiday(
311 NaiveDate::from_ymd_opt(2026, 4, 6).unwrap()
312 ));
313 assert!(is_german_holiday(
314 NaiveDate::from_ymd_opt(2026, 5, 14).unwrap()
315 ));
316 assert!(is_german_holiday(
317 NaiveDate::from_ymd_opt(2026, 5, 25).unwrap()
318 ));
319 }
320
321 #[test]
322 fn test_non_holidays() {
323 assert!(!is_german_holiday(
324 NaiveDate::from_ymd_opt(2026, 1, 2).unwrap()
325 ));
326 assert!(!is_german_holiday(
327 NaiveDate::from_ymd_opt(2026, 7, 15).unwrap()
328 ));
329 }
330
331 #[test]
332 fn test_werktag_skips_weekend() {
333 assert!(!is_werktag(NaiveDate::from_ymd_opt(2026, 4, 4).unwrap()));
335 assert!(!is_werktag(NaiveDate::from_ymd_opt(2026, 4, 5).unwrap()));
336 }
337
338 #[test]
339 fn test_werktag_skips_holiday() {
340 assert!(!is_werktag(NaiveDate::from_ymd_opt(2026, 4, 3).unwrap()));
342 }
343
344 #[test]
345 fn test_werktage_between_simple() {
346 let from = NaiveDate::from_ymd_opt(2026, 1, 12).unwrap();
349 let to = NaiveDate::from_ymd_opt(2026, 1, 16).unwrap();
350 assert_eq!(werktage_between(from, to), 4);
351 }
352
353 #[test]
354 fn test_werktage_between_skips_hl_drei_koenige() {
355 let from = NaiveDate::from_ymd_opt(2026, 1, 5).unwrap();
358 let to = NaiveDate::from_ymd_opt(2026, 1, 9).unwrap();
359 assert_eq!(werktage_between(from, to), 3);
360 }
361
362 #[test]
363 fn test_werktage_between_across_weekend() {
364 let from = NaiveDate::from_ymd_opt(2026, 1, 9).unwrap();
366 let to = NaiveDate::from_ymd_opt(2026, 1, 12).unwrap();
367 assert_eq!(werktage_between(from, to), 1);
368 }
369
370 #[test]
371 fn test_werktage_between_skips_easter_holidays() {
372 let from = NaiveDate::from_ymd_opt(2026, 4, 1).unwrap();
376 let to = NaiveDate::from_ymd_opt(2026, 4, 7).unwrap();
377 assert_eq!(werktage_between(from, to), 2);
378 }
379
380 #[test]
381 fn test_werktage_between_10_wt_after_monday() {
382 let from = NaiveDate::from_ymd_opt(2026, 1, 12).unwrap();
384 let to = NaiveDate::from_ymd_opt(2026, 1, 26).unwrap();
385 assert_eq!(werktage_between(from, to), 10);
386 }
387
388 #[test]
389 fn test_werktage_between_negative() {
390 let from = NaiveDate::from_ymd_opt(2026, 1, 26).unwrap();
391 let to = NaiveDate::from_ymd_opt(2026, 1, 12).unwrap();
392 assert_eq!(werktage_between(from, to), -10);
393 }
394
395 #[test]
396 fn test_werktage_between_same_day() {
397 let d = NaiveDate::from_ymd_opt(2026, 1, 5).unwrap();
398 assert_eq!(werktage_between(d, d), 0);
399 }
400
401 #[test]
402 fn test_parse_ccyymmdd_prefix_from_format_102() {
403 assert_eq!(
404 parse_ccyymmdd_prefix("20260115"),
405 Some(NaiveDate::from_ymd_opt(2026, 1, 15).unwrap())
406 );
407 }
408
409 #[test]
410 fn test_parse_ccyymmdd_prefix_from_format_303() {
411 assert_eq!(
413 parse_ccyymmdd_prefix("202601151200+00"),
414 Some(NaiveDate::from_ymd_opt(2026, 1, 15).unwrap())
415 );
416 }
417
418 #[test]
419 fn test_parse_ccyymmdd_prefix_rejects_short() {
420 assert!(parse_ccyymmdd_prefix("2026").is_none());
421 assert!(parse_ccyymmdd_prefix("").is_none());
422 }
423
424 #[test]
425 fn test_parse_ccyymmdd_prefix_rejects_invalid() {
426 assert!(parse_ccyymmdd_prefix("20261315").is_none());
427 }
428}