Business days
Working days for due dates, delivery estimates and SLAs: add them, count them between dates, skip weekends and holidays, with US, UK, French and German public holidays built in.
Version 1.0.0 · 5 functions · 1 crate
Functions
What use business_days.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use business_days_add, business_days_between from business_days (Quillo 0.6).
business_days_add(date: str, days: int, holidays: list) → stror an errordate moved by days business days (Monday to Friday, not in holidays, a list of "YYYY-MM-DD"): Friday plus 1 is Monday; negative goes back. 0 gives date itself, even on a weekend. Up to 100,000 days.
business_days_between(start: str, end: str, holidays: list) → intor an errorHow many business days lie from start up to end, start counted and end not: Monday to the next Monday is 5. Negative when end is before start. Up to 300 years apart.
business_day_is(date: str, holidays: list) → boolor an errorWhether date is a business day: Monday to Friday and not in holidays.
business_day_next(date: str, holidays: list) → stror an errordate itself when it is a business day, else the next one: when a payment made on a Saturday is booked.
business_holidays(country: str, year: int) → listor an errorThe public holidays of country in year as "YYYY-MM-DD", in order, for holidays: "US" (federal, as observed), "GB" (England and Wales bank holidays, as observed), "FR" (national) or "DE" (national: states add their own). Other countries: give your own list.
Get it
$ quillo install business_days # business_days.qwer into this folder$ quillo install business_days --example # and its example, business_days.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/business_days.qwer$ curl -O https://quillolang.org/plugins/files/business_days.qweThen, in your program, all of it or only what you call:
use business_days.qweruse business_days_add, business_days_between from business_days // Quillo 0.6: only theseIt uses this crate, which cargo fetches the first time the plugin is built:
chrono = { version = "0.4", default-features = false, features = ["std"] }Example
// Delivery estimates and invoice due dates in working days, holidays included.//// quillo run business_days.qwe then open http://localhost:8080/delivery?ordered=2026-12-23use "business_days.qwer"let HOLIDAYS = business_holidays("GB", 2026)? + business_holidays("GB", 2027)?route GET /delivery ? { ordered: str, days: int = 3 } = { ordered, ships: business_day_next(ordered, HOLIDAYS)?, arrives: business_days_add(ordered, days, HOLIDAYS)?,}route GET /due ? { from: str = "2026-12-21", to: str = "2027-01-04" } = { working_days: business_days_between(from, to, HOLIDAYS)?, plain_weeks: business_days_between(from, to, [])? }route GET /holidays ? { country: str = "US", year: int = 2026 } = business_holidays(country, year)?$ quillo run business_days.qwe$ curl 'localhost:8080/delivery?ordered=2026-12-23'$ curl 'localhost:8080/due'$ curl 'localhost:8080/holidays'Source
//! Working days for due dates, delivery estimates and SLAs: add business days, count them between dates, skip weekends and your holidays, with the public holidays of the US, UK, France and Germany built in.//! version: 1.0.0//!//! [dependencies]//! chrono = { version = "0.4", default-features = false, features = ["std"] }use chrono::{Datelike, Duration, NaiveDate, Weekday};use std::collections::HashSet;fn date(s: &str) -> Result<NaiveDate, String> { let t = s.trim(); NaiveDate::parse_from_str(t.get(..10).unwrap_or(t), "%Y-%m-%d").map_err(|_| format!("{:?} is not a date (YYYY-MM-DD)", s))}fn days_off(holidays: &[String]) -> Result<HashSet<NaiveDate>, String> { if holidays.len() > 10_000 { return Err("up to 10,000 holidays".into()); } holidays.iter().map(|h| date(h)).collect()}fn is_business(d: NaiveDate, off: &HashSet<NaiveDate>) -> bool { !matches!(d.weekday(), Weekday::Sat | Weekday::Sun) && !off.contains(&d)}/// `date` moved by `days` business days (Monday to Friday, not in `holidays`, a list of "YYYY-MM-DD"): Friday plus 1 is Monday; negative goes back. 0 gives `date` itself, even on a weekend. Up to 100,000 days.pub fn business_days_add(date: &str, days: i64, holidays: Vec<String>) -> Result<String, String> { let off = days_off(&holidays)?; if days.abs() > 100_000 { return Err("up to 100,000 days".into()); } let mut d = self::date(date)?; let step = if days >= 0 { 1 } else { -1 }; let mut left = days.abs(); while left > 0 { d = d.checked_add_signed(Duration::days(step)).ok_or("date out of range")?; if is_business(d, &off) { left -= 1; } } Ok(d.format("%Y-%m-%d").to_string())}/// How many business days lie from `start` up to `end`, `start` counted and `end` not: Monday to the next Monday is 5. Negative when `end` is before `start`. Up to 300 years apart.pub fn business_days_between(start: &str, end: &str, holidays: Vec<String>) -> Result<i64, String> { let off = days_off(&holidays)?; let (a, b) = (date(start)?, date(end)?); let (lo, hi, sign) = if a <= b { (a, b, 1) } else { (b, a, -1) }; if (hi - lo).num_days() > 110_000 { return Err("dates up to 300 years apart".into()); } let mut n = 0; let mut d = lo; while d < hi { if is_business(d, &off) { n += 1; } d += Duration::days(1); } Ok(n * sign)}/// Whether `date` is a business day: Monday to Friday and not in `holidays`.pub fn business_day_is(date: &str, holidays: Vec<String>) -> Result<bool, String> { Ok(is_business(self::date(date)?, &days_off(&holidays)?))}/// `date` itself when it is a business day, else the next one: when a payment made on a Saturday is booked.pub fn business_day_next(date: &str, holidays: Vec<String>) -> Result<String, String> { let off = days_off(&holidays)?; let mut d = self::date(date)?; for _ in 0..1000 { if is_business(d, &off) { return Ok(d.format("%Y-%m-%d").to_string()); } d += Duration::days(1); } Err("no business day within 1,000 days".into())}fn easter(y: i32) -> NaiveDate { // The anonymous Gregorian algorithm. let (a, b, c) = (y % 19, y / 100, y % 100); let (d, e) = (b / 4, b % 4); let f = (b + 8) / 25; let g = (b - f + 1) / 3; let h = (19 * a + b - d - g + 15) % 30; let (i, k) = (c / 4, c % 4); let l = (32 + 2 * e + 2 * i - h - k) % 7; let m = (a + 11 * h + 22 * l) / 451; let month = (h + l - 7 * m + 114) / 31; let day = (h + l - 7 * m + 114) % 31 + 1; NaiveDate::from_ymd_opt(y, month as u32, day as u32).unwrap_or_default()}fn nth(y: i32, m: u32, wd: Weekday, n: u8) -> NaiveDate { NaiveDate::from_weekday_of_month_opt(y, m, wd, n).unwrap_or_default()}fn last(y: i32, m: u32, wd: Weekday) -> NaiveDate { (1..=5).rev().find_map(|n| NaiveDate::from_weekday_of_month_opt(y, m, wd, n)).unwrap_or_default()}/// A holiday on a weekend is kept on the nearest weekday (US) or the next Monday (UK).fn observed_us(d: NaiveDate) -> NaiveDate { match d.weekday() { Weekday::Sat => d - Duration::days(1), Weekday::Sun => d + Duration::days(1), _ => d }}/// The public holidays of `country` in `year` as "YYYY-MM-DD", in order, for `holidays`: "US" (federal, as observed), "GB" (England and Wales bank holidays, as observed), "FR" (national) or "DE" (national: states add their own). Other countries: give your own list.pub fn business_holidays(country: &str, year: i64) -> Result<Vec<String>, String> { if !(1900..=2200).contains(&year) { return Err("years from 1900 to 2200".into()); } let y = year as i32; let ymd = |m: u32, d: u32| NaiveDate::from_ymd_opt(y, m, d).unwrap_or_default(); let e = easter(y); let mut v: Vec<NaiveDate> = match country.trim().to_uppercase().as_str() { "US" => vec![ observed_us(ymd(1, 1)), nth(y, 1, Weekday::Mon, 3), nth(y, 2, Weekday::Mon, 3), last(y, 5, Weekday::Mon), observed_us(ymd(6, 19)), observed_us(ymd(7, 4)), nth(y, 9, Weekday::Mon, 1), nth(y, 10, Weekday::Mon, 2), observed_us(ymd(11, 11)), nth(y, 11, Weekday::Thu, 4), observed_us(ymd(12, 25)), ], "GB" | "UK" => { let mut v = vec![e - Duration::days(2), e + Duration::days(1), nth(y, 5, Weekday::Mon, 1), last(y, 5, Weekday::Mon), last(y, 8, Weekday::Mon)]; // New Year, Christmas and Boxing Day move to the next weekdays free. let mut fixed = vec![ymd(1, 1), ymd(12, 25), ymd(12, 26)]; for f in fixed.iter_mut() { while matches!(f.weekday(), Weekday::Sat | Weekday::Sun) || v.contains(f) { *f += Duration::days(1); } v.push(*f); } v } "FR" => vec![ymd(1, 1), e + Duration::days(1), ymd(5, 1), ymd(5, 8), e + Duration::days(39), e + Duration::days(50), ymd(7, 14), ymd(8, 15), ymd(11, 1), ymd(11, 11), ymd(12, 25)], "DE" => vec![ymd(1, 1), e - Duration::days(2), e + Duration::days(1), ymd(5, 1), e + Duration::days(39), e + Duration::days(50), ymd(10, 3), ymd(12, 25), ymd(12, 26)], o => return Err(format!("no holidays built in for {:?}: US, GB, FR or DE", o)), }; v.sort(); v.dedup(); Ok(v.into_iter().map(|d| d.format("%Y-%m-%d").to_string()).collect())}Related
dates_english.qwer · 1.0.0Dates typed in English read as times, in the user's time zone and US or UK style: next friday at 3pm, tomorrow 9am, in 2 weeks, 3 days ago.8 views · 10 downloadsISO durations iso_durations.qwer · 1.0.0ISO 8601 durations such as P1DT2H and PT30M read, written, turned into seconds and added to dates and times, months moving the calendar.7 views · 9 downloadsEDIFACT edifact.qwer · 1.0.0UN/EDIFACT for European retail and logistics: interchanges parsed and checked, ORDERS, INVOIC and DESADV read into parties, lines and totals, interchanges built with escapes and counts, and CONTRL acknowledgments.8 views · 9 downloadsX12 EDI x12.qwer · 1.0.0ANSI X12 EDI for trading with retailers and carriers: interchanges parsed and checked, 850 orders, 810 invoices and 856 ship notices read into plain fields, interchanges built with their envelopes, and 997 acknowledgments.8 views · 6 downloadsChinese calendar chinese_calendar.qwer · 1.0.0The Chinese lunisolar calendar: a date's lunar year, month and day in Chinese, leap months included, the year's stem and branch, zodiac animal and element, Chinese New Year, lunar dates back to Gregorian, and the 24 solar terms.8 views · 7 downloadsCron lines cron.qwer · 1.0.0The next times a cron line runs, and whether it is valid: show a schedule, or run your own.7 views · 7 downloadsThread 0 posts
On Business days: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.