mod data;
mod day_flags;
mod predict;
mod range;
mod raw_date;
mod resolved;
#[cfg(any(feature = "time", feature = "chrono"))]
pub mod date;
mod official;
pub use day_flags::DayFlags;
pub use range::WorkWeek;
pub use resolved::Resolved;
#[cfg(any(feature = "time", feature = "chrono"))]
pub use date::CalendarDate;
pub const FIRST_FACT_YEAR: i32 = data::FACT_FIRST_YEAR;
pub const LAST_FACT_YEAR: i32 = data::FACT_LAST_YEAR;
pub const MIN_YEAR: i32 = 1900;
pub const MAX_YEAR: i32 = 2100;
use predict as predict_mod;
use raw_date::RawDate;
#[cfg(any(feature = "time", feature = "chrono"))]
mod generic {
use super::*;
use crate::date::CalendarDate;
#[inline]
pub fn flags<D: CalendarDate>(date: D) -> Resolved<DayFlags> {
let raw = RawDate::from_calendar_date(date);
super::flags_raw(raw)
}
#[inline]
pub fn is_day_off<D: CalendarDate>(date: D) -> Resolved<bool> {
flags(date).map(DayFlags::is_day_off)
}
#[inline]
pub fn is_working_day<D: CalendarDate>(date: D) -> Resolved<bool> {
flags(date).map(DayFlags::is_working_day)
}
#[inline]
pub fn is_holiday<D: CalendarDate>(date: D) -> Resolved<bool> {
flags(date).map(DayFlags::is_holiday)
}
#[inline]
pub fn is_short_day<D: CalendarDate>(date: D) -> Resolved<bool> {
flags(date).map(DayFlags::is_short_day)
}
#[inline]
pub fn is_weekend<D: CalendarDate>(date: D) -> Resolved<bool> {
flags(date).map(DayFlags::is_weekend)
}
#[inline]
pub fn is_transferred<D: CalendarDate>(date: D) -> Resolved<bool> {
flags(date).map(DayFlags::is_transferred)
}
#[inline]
pub fn non_working_days_between<D: CalendarDate>(start: D, end: D) -> Option<Resolved<u32>> {
let start = RawDate::from_calendar_date(start);
let end = RawDate::from_calendar_date(end);
range::non_working_days_between_raw(start, end)
}
#[inline]
pub fn working_minutes_between<D: CalendarDate>(
start: D,
end: D,
week: WorkWeek,
) -> Option<Resolved<u32>> {
let start = RawDate::from_calendar_date(start);
let end = RawDate::from_calendar_date(end);
range::working_minutes_between_raw(start, end, week)
}
#[inline]
pub fn working_hours_between<D: CalendarDate>(
start: D,
end: D,
week: WorkWeek,
) -> Option<Resolved<f64>> {
working_minutes_between(start, end, week).map(|r| r.map(|minutes| minutes as f64 / 60.0))
}
}
#[cfg(any(feature = "time", feature = "chrono"))]
pub use generic::{
flags, is_day_off, is_holiday, is_short_day, is_transferred, is_weekend, is_working_day,
non_working_days_between, working_hours_between, working_minutes_between,
};
#[inline]
pub fn flags_ymd(year: i32, month: u8, day: u8) -> Option<Resolved<DayFlags>> {
let raw = RawDate::from_ymd(year, month, day)?;
Some(flags_raw(raw))
}
#[inline]
pub fn is_day_off_ymd(year: i32, month: u8, day: u8) -> Option<Resolved<bool>> {
flags_ymd(year, month, day).map(|r| r.map(DayFlags::is_day_off))
}
#[inline]
pub fn is_working_day_ymd(year: i32, month: u8, day: u8) -> Option<Resolved<bool>> {
flags_ymd(year, month, day).map(|r| r.map(DayFlags::is_working_day))
}
#[inline]
pub fn is_holiday_ymd(year: i32, month: u8, day: u8) -> Option<Resolved<bool>> {
flags_ymd(year, month, day).map(|r| r.map(DayFlags::is_holiday))
}
#[inline]
pub fn is_short_day_ymd(year: i32, month: u8, day: u8) -> Option<Resolved<bool>> {
flags_ymd(year, month, day).map(|r| r.map(DayFlags::is_short_day))
}
#[inline]
pub fn is_weekend_ymd(year: i32, month: u8, day: u8) -> Option<Resolved<bool>> {
flags_ymd(year, month, day).map(|r| r.map(DayFlags::is_weekend))
}
#[inline]
pub fn is_transferred_ymd(year: i32, month: u8, day: u8) -> Option<Resolved<bool>> {
flags_ymd(year, month, day).map(|r| r.map(DayFlags::is_transferred))
}
#[inline]
pub fn non_working_days_between_ymd(
start_year: i32,
start_month: u8,
start_day: u8,
end_year: i32,
end_month: u8,
end_day: u8,
) -> Option<Resolved<u32>> {
let start = RawDate::from_ymd(start_year, start_month, start_day)?;
let end = range_end_raw_ymd(end_year, end_month, end_day)?;
range::non_working_days_between_raw(start, end)
}
#[inline]
pub fn working_minutes_between_ymd(
start_year: i32,
start_month: u8,
start_day: u8,
end_year: i32,
end_month: u8,
end_day: u8,
week: WorkWeek,
) -> Option<Resolved<u32>> {
let start = RawDate::from_ymd(start_year, start_month, start_day)?;
let end = range_end_raw_ymd(end_year, end_month, end_day)?;
range::working_minutes_between_raw(start, end, week)
}
#[inline]
pub fn working_hours_between_ymd(
start_year: i32,
start_month: u8,
start_day: u8,
end_year: i32,
end_month: u8,
end_day: u8,
week: WorkWeek,
) -> Option<Resolved<f64>> {
working_minutes_between_ymd(
start_year,
start_month,
start_day,
end_year,
end_month,
end_day,
week,
)
.map(|r| r.map(|minutes| minutes as f64 / 60.0))
}
#[inline]
fn range_end_raw_ymd(year: i32, month: u8, day: u8) -> Option<RawDate> {
RawDate::from_ymd(year, month, day).or_else(|| {
(year == MAX_YEAR + 1 && month == 1 && day == 1)
.then(|| RawDate::from_ymd_unchecked(year, month, day))
})
}
#[inline]
pub(crate) fn flags_raw(date: RawDate) -> Resolved<DayFlags> {
if let Some(flags) = official::flags(date) {
Resolved::Fact(flags)
} else {
Resolved::Predict(predict_mod::flags(date))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_flags_ymd_valid() {
let r = flags_ymd(2026, 1, 9).unwrap();
assert!(r.is_fact());
assert!(r.value().is_day_off());
}
#[test]
fn test_flags_ymd_invalid() {
assert!(flags_ymd(2026, 2, 31).is_none());
assert!(flags_ymd(2026, 13, 1).is_none());
assert!(flags_ymd(2026, 1, 0).is_none());
}
#[test]
fn test_flags_ymd_predict() {
let r = flags_ymd(2027, 1, 1).unwrap();
assert!(r.is_predict());
assert!(r.value().is_holiday());
}
#[test]
fn test_is_day_off_ymd() {
let r = is_day_off_ymd(2026, 1, 9).unwrap();
assert!(r.value());
}
#[test]
fn test_is_holiday_ymd() {
let r = is_holiday_ymd(2026, 1, 1).unwrap();
assert!(r.value());
let r = is_holiday_ymd(2026, 1, 9).unwrap();
assert!(!r.value());
}
#[test]
fn test_is_working_day_ymd() {
let r = is_working_day_ymd(2026, 1, 12).unwrap();
assert!(r.value());
}
#[test]
fn test_is_short_day_ymd() {
let r = is_short_day_ymd(2026, 11, 3).unwrap();
assert!(r.value());
}
#[test]
fn test_is_weekend_ymd() {
let r = is_weekend_ymd(2026, 1, 11).unwrap();
assert!(r.value());
}
#[test]
fn test_flags_ymd_year_range() {
assert!(flags_ymd(MIN_YEAR - 1, 1, 1).is_none());
assert!(flags_ymd(MIN_YEAR, 1, 1).is_some());
assert!(flags_ymd(MAX_YEAR, 12, 31).is_some());
assert!(flags_ymd(MAX_YEAR + 1, 1, 1).is_none());
}
#[test]
fn test_range_ymd_can_include_last_supported_day() {
assert!(non_working_days_between_ymd(MAX_YEAR, 12, 31, MAX_YEAR + 1, 1, 1).is_some());
assert!(
working_minutes_between_ymd(
MAX_YEAR,
12,
31,
MAX_YEAR + 1,
1,
1,
WorkWeek::FortyHours,
)
.is_some()
);
}
#[test]
fn test_fact_year_invariants() {
for year in FIRST_FACT_YEAR..=LAST_FACT_YEAR {
let mut date = RawDate::from_ymd(year, 1, 1).unwrap();
loop {
let flags = flags_raw(date).value();
assert_ne!(
flags.is_day_off(),
flags.is_working_day(),
"{year}-{:02}-{:02}: day cannot be both off and working",
date.month,
date.day,
);
assert!(
!flags.is_short_day() || flags.is_working_day(),
"{year}-{:02}-{:02}: short day must be working",
date.month,
date.day,
);
assert!(
!flags.is_holiday() || flags.is_day_off(),
"{year}-{:02}-{:02}: holiday must be day off",
date.month,
date.day,
);
if date.month == 12 && date.day == 31 {
break;
}
date = date.next_day();
}
}
}
#[cfg(feature = "chrono")]
mod chrono_tests {
use super::*;
use chrono::NaiveDate;
#[test]
fn test_flags_with_naive_date() {
let date = NaiveDate::from_ymd_opt(2026, 1, 9).unwrap();
let r = flags(date);
assert!(r.is_fact());
assert!(r.value().is_day_off());
}
#[test]
fn test_is_day_off_with_naive_date() {
let date = NaiveDate::from_ymd_opt(2026, 1, 9).unwrap();
assert!(is_day_off(date).value());
}
#[test]
fn test_predict_with_naive_date() {
let date = NaiveDate::from_ymd_opt(2027, 1, 1).unwrap();
let r = flags(date);
assert!(r.is_predict());
assert!(r.value().is_holiday());
}
#[test]
fn test_chrono_matches_ymd() {
for (year, month, day) in [
(2000, 1, 1),
(2010, 1, 6),
(2024, 4, 27),
(2026, 12, 31),
(2027, 1, 11),
] {
let date = NaiveDate::from_ymd_opt(year, month, day).unwrap();
assert_eq!(
flags(date),
flags_ymd(year, month as u8, day as u8).unwrap()
);
}
}
}
#[cfg(feature = "time")]
mod time_tests {
use super::*;
use time::Date;
use time::Month;
#[test]
fn test_flags_with_time_date() {
let date = Date::from_calendar_date(2026, Month::January, 9).unwrap();
let r = flags(date);
assert!(r.is_fact());
assert!(r.value().is_day_off());
}
#[test]
fn test_is_day_off_with_time_date() {
let date = Date::from_calendar_date(2026, Month::January, 9).unwrap();
assert!(is_day_off(date).value());
}
#[test]
fn test_predict_with_time_date() {
let date = Date::from_calendar_date(2027, Month::January, 1).unwrap();
let r = flags(date);
assert!(r.is_predict());
assert!(r.value().is_holiday());
}
#[test]
fn test_time_matches_ymd() {
for (year, month, day) in [
(2000, Month::January, 1),
(2010, Month::January, 6),
(2024, Month::April, 27),
(2026, Month::December, 31),
(2027, Month::January, 11),
] {
let date = Date::from_calendar_date(year, month, day).unwrap();
assert_eq!(flags(date), flags_ymd(year, month.into(), day).unwrap());
}
}
}
}