use std::collections::HashSet; use super::funejson::Value; use super::dates_add_days::epoch_day_from_iso; use super::dates_day_of_week::day_of_week; /// Working days between two dates, excluding Saturdays, Sundays and any date in /// the holiday list. /// /// The interval is half-open: the start date counts, the end date does not. /// That is the convention that makes ranges compose - /// `business_days_between(a, b) + business_days_between(b, c)` equals /// `business_days_between(a, c)` - and it makes "how many working days until /// the deadline" come out at zero on the deadline itself rather than one. If /// you want the end date included, ask for the day after it. /// /// A start later than the end returns a negative count, with the same magnitude /// as the forward direction, so `business_days_between(a, b)` is exactly /// `-business_days_between(b, a)`. Returning zero or panicking would both hide /// a caller's argument-order bug. /// /// Holidays are an argument, not built in, because no library knows which days /// your company is closed. Weekend holidays are not double counted, duplicates /// in the list are harmless, and holidays outside the interval are ignored. /// /// # Panics /// Panics if either date, or any holiday, is malformed or impossible. pub fn business_days_between(start_iso: &str, end_iso: &str, holidays: &[String]) -> i64 { let start = epoch_day_from_iso(start_iso); let end = epoch_day_from_iso(end_iso); // Every holiday is validated even when it falls outside the interval: a typo // in a holiday calendar should fail loudly on the next run, not lie dormant // until the year the date is finally inside a query. let excluded: HashSet = holidays.iter().map(|h| epoch_day_from_iso(h)).collect(); if start > end { return -count_working_days(end, end_iso, start, &excluded); } count_working_days(start, start_iso, end, &excluded) } /// Working days in the half-open interval [from_day, to_day). fn count_working_days(from_day: i64, from_iso: &str, to_day: i64, excluded: &HashSet) -> i64 { // The weekday is carried forward rather than recomputed per day: one date // parse, then a seven-day cycle, which is the same loop in all three // languages and cannot drift between them. let mut weekday = day_of_week(from_iso); let mut count = 0i64; for day in from_day..to_day { if weekday <= 5 && !excluded.contains(&day) { count += 1; } weekday = if weekday == 7 { 1 } else { weekday + 1 }; } count } pub fn fune_vector(args: &[Value]) -> Value { let holidays: Vec = args[2].as_arr().iter().map(|v| v.as_str().to_string()).collect(); Value::Int(business_days_between( args[0].as_str(), args[1].as_str(), &holidays, )) }