1mod data;
53mod day_flags;
54mod predict;
55mod range;
56mod raw_date;
57mod resolved;
58
59#[cfg(any(feature = "time", feature = "chrono"))]
60pub mod date;
61
62mod official;
63
64pub use day_flags::DayFlags;
65pub use range::WorkWeek;
66pub use resolved::Resolved;
67
68#[cfg(any(feature = "time", feature = "chrono"))]
69pub use date::CalendarDate;
70
71pub const FIRST_FACT_YEAR: i32 = data::FACT_FIRST_YEAR;
73
74pub const LAST_FACT_YEAR: i32 = data::FACT_LAST_YEAR;
76
77pub const MIN_YEAR: i32 = 1900;
79
80pub const MAX_YEAR: i32 = 2100;
85
86use predict as predict_mod;
87use raw_date::RawDate;
88
89#[cfg(any(feature = "time", feature = "chrono"))]
94mod generic {
95 use super::*;
96 use crate::date::CalendarDate;
97
98 #[inline]
118 pub fn flags<D: CalendarDate>(date: D) -> Resolved<DayFlags> {
119 let raw = RawDate::from_calendar_date(date);
120 super::flags_raw(raw)
121 }
122
123 #[inline]
128 pub fn is_day_off<D: CalendarDate>(date: D) -> Resolved<bool> {
129 flags(date).map(DayFlags::is_day_off)
130 }
131
132 #[inline]
134 pub fn is_working_day<D: CalendarDate>(date: D) -> Resolved<bool> {
135 flags(date).map(DayFlags::is_working_day)
136 }
137
138 #[inline]
140 pub fn is_holiday<D: CalendarDate>(date: D) -> Resolved<bool> {
141 flags(date).map(DayFlags::is_holiday)
142 }
143
144 #[inline]
146 pub fn is_short_day<D: CalendarDate>(date: D) -> Resolved<bool> {
147 flags(date).map(DayFlags::is_short_day)
148 }
149
150 #[inline]
152 pub fn is_weekend<D: CalendarDate>(date: D) -> Resolved<bool> {
153 flags(date).map(DayFlags::is_weekend)
154 }
155
156 #[inline]
158 pub fn is_transferred<D: CalendarDate>(date: D) -> Resolved<bool> {
159 flags(date).map(DayFlags::is_transferred)
160 }
161
162 #[inline]
170 pub fn non_working_days_between<D: CalendarDate>(start: D, end: D) -> Option<Resolved<u32>> {
171 let start = RawDate::from_calendar_date(start);
172 let end = RawDate::from_calendar_date(end);
173
174 range::non_working_days_between_raw(start, end)
175 }
176
177 #[inline]
182 pub fn working_minutes_between<D: CalendarDate>(
183 start: D,
184 end: D,
185 week: WorkWeek,
186 ) -> Option<Resolved<u32>> {
187 let start = RawDate::from_calendar_date(start);
188 let end = RawDate::from_calendar_date(end);
189
190 range::working_minutes_between_raw(start, end, week)
191 }
192
193 #[inline]
197 pub fn working_hours_between<D: CalendarDate>(
198 start: D,
199 end: D,
200 week: WorkWeek,
201 ) -> Option<Resolved<f64>> {
202 working_minutes_between(start, end, week).map(|r| r.map(|minutes| minutes as f64 / 60.0))
203 }
204}
205
206#[cfg(any(feature = "time", feature = "chrono"))]
207pub use generic::{
208 flags, is_day_off, is_holiday, is_short_day, is_transferred, is_weekend, is_working_day,
209 non_working_days_between, working_hours_between, working_minutes_between,
210};
211
212#[inline]
231pub fn flags_ymd(year: i32, month: u8, day: u8) -> Option<Resolved<DayFlags>> {
232 let raw = RawDate::from_ymd(year, month, day)?;
233 Some(flags_raw(raw))
234}
235
236#[inline]
240pub fn is_day_off_ymd(year: i32, month: u8, day: u8) -> Option<Resolved<bool>> {
241 flags_ymd(year, month, day).map(|r| r.map(DayFlags::is_day_off))
242}
243
244#[inline]
248pub fn is_working_day_ymd(year: i32, month: u8, day: u8) -> Option<Resolved<bool>> {
249 flags_ymd(year, month, day).map(|r| r.map(DayFlags::is_working_day))
250}
251
252#[inline]
256pub fn is_holiday_ymd(year: i32, month: u8, day: u8) -> Option<Resolved<bool>> {
257 flags_ymd(year, month, day).map(|r| r.map(DayFlags::is_holiday))
258}
259
260#[inline]
264pub fn is_short_day_ymd(year: i32, month: u8, day: u8) -> Option<Resolved<bool>> {
265 flags_ymd(year, month, day).map(|r| r.map(DayFlags::is_short_day))
266}
267
268#[inline]
272pub fn is_weekend_ymd(year: i32, month: u8, day: u8) -> Option<Resolved<bool>> {
273 flags_ymd(year, month, day).map(|r| r.map(DayFlags::is_weekend))
274}
275
276#[inline]
280pub fn is_transferred_ymd(year: i32, month: u8, day: u8) -> Option<Resolved<bool>> {
281 flags_ymd(year, month, day).map(|r| r.map(DayFlags::is_transferred))
282}
283
284#[inline]
292pub fn non_working_days_between_ymd(
293 start_year: i32,
294 start_month: u8,
295 start_day: u8,
296 end_year: i32,
297 end_month: u8,
298 end_day: u8,
299) -> Option<Resolved<u32>> {
300 let start = RawDate::from_ymd(start_year, start_month, start_day)?;
301 let end = range_end_raw_ymd(end_year, end_month, end_day)?;
302
303 range::non_working_days_between_raw(start, end)
304}
305
306#[inline]
313pub fn working_minutes_between_ymd(
314 start_year: i32,
315 start_month: u8,
316 start_day: u8,
317 end_year: i32,
318 end_month: u8,
319 end_day: u8,
320 week: WorkWeek,
321) -> Option<Resolved<u32>> {
322 let start = RawDate::from_ymd(start_year, start_month, start_day)?;
323 let end = range_end_raw_ymd(end_year, end_month, end_day)?;
324
325 range::working_minutes_between_raw(start, end, week)
326}
327
328#[inline]
332pub fn working_hours_between_ymd(
333 start_year: i32,
334 start_month: u8,
335 start_day: u8,
336 end_year: i32,
337 end_month: u8,
338 end_day: u8,
339 week: WorkWeek,
340) -> Option<Resolved<f64>> {
341 working_minutes_between_ymd(
342 start_year,
343 start_month,
344 start_day,
345 end_year,
346 end_month,
347 end_day,
348 week,
349 )
350 .map(|r| r.map(|minutes| minutes as f64 / 60.0))
351}
352
353#[inline]
354fn range_end_raw_ymd(year: i32, month: u8, day: u8) -> Option<RawDate> {
355 RawDate::from_ymd(year, month, day).or_else(|| {
356 (year == MAX_YEAR + 1 && month == 1 && day == 1)
357 .then(|| RawDate::from_ymd_unchecked(year, month, day))
358 })
359}
360
361#[inline]
368pub(crate) fn flags_raw(date: RawDate) -> Resolved<DayFlags> {
369 if let Some(flags) = official::flags(date) {
370 Resolved::Fact(flags)
371 } else {
372 Resolved::Predict(predict_mod::flags(date))
373 }
374}
375
376#[cfg(test)]
377mod tests {
378 use super::*;
379
380 #[test]
385 fn test_flags_ymd_valid() {
386 let r = flags_ymd(2026, 1, 9).unwrap();
387 assert!(r.is_fact());
388 assert!(r.value().is_day_off());
389 }
390
391 #[test]
392 fn test_flags_ymd_invalid() {
393 assert!(flags_ymd(2026, 2, 31).is_none());
394 assert!(flags_ymd(2026, 13, 1).is_none());
395 assert!(flags_ymd(2026, 1, 0).is_none());
396 }
397
398 #[test]
399 fn test_flags_ymd_predict() {
400 let r = flags_ymd(2027, 1, 1).unwrap();
402 assert!(r.is_predict());
403 assert!(r.value().is_holiday());
404 }
405
406 #[test]
407 fn test_is_day_off_ymd() {
408 let r = is_day_off_ymd(2026, 1, 9).unwrap();
409 assert!(r.value());
410 }
411
412 #[test]
413 fn test_is_holiday_ymd() {
414 let r = is_holiday_ymd(2026, 1, 1).unwrap();
415 assert!(r.value());
416
417 let r = is_holiday_ymd(2026, 1, 9).unwrap();
418 assert!(!r.value());
419 }
420
421 #[test]
422 fn test_is_working_day_ymd() {
423 let r = is_working_day_ymd(2026, 1, 12).unwrap();
424 assert!(r.value());
426 }
427
428 #[test]
429 fn test_is_short_day_ymd() {
430 let r = is_short_day_ymd(2026, 11, 3).unwrap();
432 assert!(r.value());
433 }
434
435 #[test]
436 fn test_is_weekend_ymd() {
437 let r = is_weekend_ymd(2026, 1, 11).unwrap();
439 assert!(r.value());
440 }
441
442 #[test]
443 fn test_flags_ymd_year_range() {
444 assert!(flags_ymd(MIN_YEAR - 1, 1, 1).is_none());
445 assert!(flags_ymd(MIN_YEAR, 1, 1).is_some());
446 assert!(flags_ymd(MAX_YEAR, 12, 31).is_some());
447 assert!(flags_ymd(MAX_YEAR + 1, 1, 1).is_none());
448 }
449
450 #[test]
451 fn test_range_ymd_can_include_last_supported_day() {
452 assert!(non_working_days_between_ymd(MAX_YEAR, 12, 31, MAX_YEAR + 1, 1, 1).is_some());
453 assert!(
454 working_minutes_between_ymd(
455 MAX_YEAR,
456 12,
457 31,
458 MAX_YEAR + 1,
459 1,
460 1,
461 WorkWeek::FortyHours,
462 )
463 .is_some()
464 );
465 }
466
467 #[test]
468 fn test_fact_year_invariants() {
469 for year in FIRST_FACT_YEAR..=LAST_FACT_YEAR {
470 let mut date = RawDate::from_ymd(year, 1, 1).unwrap();
471
472 loop {
473 let flags = flags_raw(date).value();
474
475 assert_ne!(
476 flags.is_day_off(),
477 flags.is_working_day(),
478 "{year}-{:02}-{:02}: day cannot be both off and working",
479 date.month,
480 date.day,
481 );
482 assert!(
483 !flags.is_short_day() || flags.is_working_day(),
484 "{year}-{:02}-{:02}: short day must be working",
485 date.month,
486 date.day,
487 );
488 assert!(
489 !flags.is_holiday() || flags.is_day_off(),
490 "{year}-{:02}-{:02}: holiday must be day off",
491 date.month,
492 date.day,
493 );
494
495 if date.month == 12 && date.day == 31 {
496 break;
497 }
498
499 date = date.next_day();
500 }
501 }
502 }
503
504 #[cfg(feature = "chrono")]
509 mod chrono_tests {
510 use super::*;
511 use chrono::NaiveDate;
512
513 #[test]
514 fn test_flags_with_naive_date() {
515 let date = NaiveDate::from_ymd_opt(2026, 1, 9).unwrap();
516 let r = flags(date);
517 assert!(r.is_fact());
518 assert!(r.value().is_day_off());
519 }
520
521 #[test]
522 fn test_is_day_off_with_naive_date() {
523 let date = NaiveDate::from_ymd_opt(2026, 1, 9).unwrap();
524 assert!(is_day_off(date).value());
525 }
526
527 #[test]
528 fn test_predict_with_naive_date() {
529 let date = NaiveDate::from_ymd_opt(2027, 1, 1).unwrap();
530 let r = flags(date);
531 assert!(r.is_predict());
532 assert!(r.value().is_holiday());
533 }
534
535 #[test]
536 fn test_chrono_matches_ymd() {
537 for (year, month, day) in [
538 (2000, 1, 1),
539 (2010, 1, 6),
540 (2024, 4, 27),
541 (2026, 12, 31),
542 (2027, 1, 11),
543 ] {
544 let date = NaiveDate::from_ymd_opt(year, month, day).unwrap();
545 assert_eq!(
546 flags(date),
547 flags_ymd(year, month as u8, day as u8).unwrap()
548 );
549 }
550 }
551 }
552
553 #[cfg(feature = "time")]
558 mod time_tests {
559 use super::*;
560 use time::Date;
561 use time::Month;
562
563 #[test]
564 fn test_flags_with_time_date() {
565 let date = Date::from_calendar_date(2026, Month::January, 9).unwrap();
566 let r = flags(date);
567 assert!(r.is_fact());
568 assert!(r.value().is_day_off());
569 }
570
571 #[test]
572 fn test_is_day_off_with_time_date() {
573 let date = Date::from_calendar_date(2026, Month::January, 9).unwrap();
574 assert!(is_day_off(date).value());
575 }
576
577 #[test]
578 fn test_predict_with_time_date() {
579 let date = Date::from_calendar_date(2027, Month::January, 1).unwrap();
580 let r = flags(date);
581 assert!(r.is_predict());
582 assert!(r.value().is_holiday());
583 }
584
585 #[test]
586 fn test_time_matches_ymd() {
587 for (year, month, day) in [
588 (2000, Month::January, 1),
589 (2010, Month::January, 6),
590 (2024, Month::April, 27),
591 (2026, Month::December, 31),
592 (2027, Month::January, 11),
593 ] {
594 let date = Date::from_calendar_date(year, month, day).unwrap();
595 assert_eq!(flags(date), flags_ymd(year, month.into(), day).unwrap());
596 }
597 }
598 }
599}