use super::funejson::Value; use super::dates_add_business_days::add_business_days; use super::dates_bank_holidays::bank_holidays; use super::dates_day_of_week::day_of_week; fn is_time(value: &str) -> bool { let b = value.as_bytes(); if b.len() != 5 || b[2] != b':' || !b.iter().enumerate().all(|(i, c)| i == 2 || c.is_ascii_digit()) { return false; } let hours = (b[0] - b'0') * 10 + (b[1] - b'0'); hours <= 23 && b[3] <= b'5' } fn estimate(order_date: &str, order_time: &str, cut_off: &str, service_days: i64, holidays: &[String]) -> DeliveryEstimate { let working = day_of_week(order_date) <= 5 && !holidays.iter().any(|h| h == order_date); // HH:MM compares correctly as text. Strictly before: 15:00 misses a 15:00 cut-off. let same_day = working && order_time < cut_off; let dispatch = if same_day { order_date.to_string() } else { add_business_days(order_date, 1, holidays) }; DeliveryEstimate { delivery_date: add_business_days(&dispatch, service_days, holidays), dispatch_date: dispatch, same_day_dispatch: same_day, } } /// When an order leaves the warehouse and when it should arrive: same /// working day if placed before the cut-off, then service_days working days /// in transit, skipping weekends, the region's bank holidays and the /// caller's closures. /// /// # Panics /// Panics on a malformed time or date, negative service days, an unknown /// region, or an estimate reaching a year with no published bank holidays. pub fn delivery_date( order_date: &str, order_time: &str, cut_off: &str, service_days: i64, region: Option<&str>, closures: &[String], ) -> DeliveryEstimate { if !is_time(order_time) { panic!("orderTime must be a 24-hour time HH:MM, received \"{}\"", order_time); } if !is_time(cut_off) { panic!("cutOff must be a 24-hour time HH:MM, received \"{}\"", cut_off); } if service_days < 0 { panic!("serviceDays must not be negative, received {}", service_days); } day_of_week(order_date); // validates the date before its year is used let mut holidays: Vec = closures.to_vec(); let mut last_year: i64 = order_date[0..4].parse().unwrap(); if let Some(r) = region { holidays.extend(bank_holidays(r, last_year).into_iter().map(|h| h.date)); } // Load each later year the estimate reaches: an order on 31 December must // skip next year's New Year's Day. More holidays only move the answer later, // so this settles. loop { let result = estimate(order_date, order_time, cut_off, service_days, &holidays); let reached: i64 = result.delivery_date[0..4].parse().unwrap(); let r = match region { Some(r) if reached > last_year => r, _ => return result, }; for year in (last_year + 1)..=reached { holidays.extend(bank_holidays(r, year).into_iter().map(|h| h.date)); } last_year = reached; } } pub fn delivery_estimate_to_value(e: &DeliveryEstimate) -> Value { Value::obj(vec![ ("dispatchDate", Value::str(&e.dispatch_date)), ("deliveryDate", Value::str(&e.delivery_date)), ("sameDayDispatch", Value::Bool(e.same_day_dispatch)), ]) } pub fn fune_vector(args: &[Value]) -> Value { let closures: Vec = args[5].as_arr().iter().map(|v| v.as_str().to_string()).collect(); let region = if args[4].is_null() { None } else { Some(args[4].as_str()) }; delivery_estimate_to_value(&delivery_date( args[0].as_str(), args[1].as_str(), args[2].as_str(), args[3].as_i64(), region, &closures, )) }