Functional Weave
Code in TypeScript

dates.business-days-between@1.0.0

impl/rust.rs

2,833 bytes · the Rust implementation · view raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

use std::collections::HashSet;

use super::funejson::Value;  ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::dates_add_days::epoch_day_from_iso;  ← from dates.add-days ^1.0.0 · built alongside by fune
use super::dates_day_of_week::day_of_week;  ← from dates.day-of-week ^1.0.0 · built alongside by fune

/// 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<i64> = 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>) -> 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<String> = 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,
    ))
}