logistics.delivery-date
Estimated dispatch and delivery dates from order time, dispatch cut-off, service days, weekends and UK bank holidays.
1.0.0 · published 2026-10-03 by charlie · Anterra
Pinned by 26 tests, run in TypeScript, Python and Rust.
What it does
The "order in the next 2 hours for delivery on Thursday" calculation. An order goes out the same day when it is placed on a working day strictly before the cut-off time; otherwise it goes out on the next working day. Delivery is `serviceDays` working days after dispatch, so 1 is next-day delivery and 0 is same-day.
Working days are Monday to Friday, less the UK bank holidays of `region` (from `dates.bank-holidays`) and any `closures` the caller adds. The arithmetic is `dates.add-business-days`.
For example
delivery_date(2026-09-23, 10:00, 15:00, 1, england-and-wales, )→ dispatch date 2026-09-23, delivery date 2026-09-24, same day dispatch true a Wednesday morning order goes out today and arrives tomorrowdelivery_date(2026-09-23, 14:59, 15:00, 1, england-and-wales, )→ dispatch date 2026-09-23, delivery date 2026-09-24, same day dispatch true one minute before the cut-off still makes itdelivery_date(2026-09-23, 15:00, 15:00, 1, england-and-wales, )→ dispatch date 2026-09-24, delivery date 2026-09-25, same day dispatch false exactly at the cut-off misses it
The function
The same function in TypeScript, Python and Rust, pinned by the same tests. Pick your language; the choice follows you around the registry.
pub fn delivery_date(order_date: &str, order_time: &str, cut_off: &str, service_days: i64, region: Option<&str>, closures: &[String]) -> DeliveryEstimate
| order_date | date | the local date the order was placed, at the dispatching warehouse |
| order_time | string | local time the order was placed, 24-hour HH:MM |
| cut_off | string | orders placed strictly before this local time go out the same working day, HH:MM |
| service_days | int | working days in transit: 1 for next day, 0 for same-day delivery |
| region | UkRegion? | UK bank holidays to skip, as gov.uk divides them; null for none |
| closures | date[] | other non-working dates (warehouse or carrier closures, another country's holidays) |
| returns | DeliveryEstimate |
The type it declares, generated into your project
/// When the parcel leaves and when it should arrive.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DeliveryEstimate {
pub dispatch_date: String,
pub delivery_date: String,
/// true when the order made the day's cut-off
pub same_day_dispatch: bool,
}
Your code names it in one line, in the file that uses it
fune!(logistics.delivery-date@^1); // then call delivery_date(…)
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
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_business_days::add_business_days; ← from dates.add-business-days ^1.0.0 · built alongside by fune
use super::dates_bank_holidays::bank_holidays; ← from dates.bank-holidays ^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
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<String> = 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<String> = 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,
))
}Install
fune build
With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and its 3 dependencies, pins them in fune.lock, downloads only the Rust package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. A crate’s build.rs runs it before every compile. Or pin a range in fune.project and build in one step:
fune add logistics.delivery-date
The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./logistics.delivery-date-1.0.0-rust.fune, or fetch it from a terminal with fune pull logistics.delivery-date@1.0.0:rust.
The whole function, every language, is one file too: logistics.delivery-date-1.0.0.fune, 20,130 bytes, sha256 bd62d3c93df138ca8f79b8fc80b6870308c59a14b2e3ba36a8cf803ea728edc4. It installs into a project of any language.
Customise it in your app
The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.
before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.
// fune: before logistics.delivery-date
after — your function gets the result and the arguments, and returns the final result.
// fune: after logistics.delivery-date
replace — inside this capability’s code only, calls to a dependency go to your function, with the same signature. Other capabilities that use it are unaffected; write in * to replace it everywhere.
// fune: replace dates.add-business-days in logistics.delivery-date
// fune: replace dates.bank-holidays in logistics.delivery-date
// fune: replace dates.day-of-week in logistics.delivery-date
step — your function runs at a numbered point inside the function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show logistics.delivery-date --steps.
// fune: step logistics.delivery-date after <n|label>
Tests
A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.
| Case | Arguments | Expected | |
|---|---|---|---|
| a Wednesday morning order goes out today and arrives tomorrow | 2026-09-23, 10:00, 15:00, 1, england-and-wales, | → | dispatch date 2026-09-23, delivery date 2026-09-24, same day dispatch true |
| one minute before the cut-off still makes it | 2026-09-23, 14:59, 15:00, 1, england-and-wales, | → | dispatch date 2026-09-23, delivery date 2026-09-24, same day dispatch true |
| exactly at the cut-off misses it | 2026-09-23, 15:00, 15:00, 1, england-and-wales, | → | dispatch date 2026-09-24, delivery date 2026-09-25, same day dispatch false |
| Friday after the cut-off goes out on Monday | 2026-09-25, 16:00, 15:00, 1, england-and-wales, | → | dispatch date 2026-09-28, delivery date 2026-09-29, same day dispatch false |
| a Saturday order goes out on Monday even before the cut-off | 2026-09-26, 09:00, 15:00, 1, england-and-wales, | → | dispatch date 2026-09-28, delivery date 2026-09-29, same day dispatch false |
| Maundy Thursday after the cut-off skips Good Friday, the weekend and Easter Monday | 2026-04-02, 16:00, 15:00, 1, england-and-wales, | → | dispatch date 2026-04-07, delivery date 2026-04-08, same day dispatch false |
| Easter Monday is not a holiday in Scotland | 2026-04-06, 10:00, 15:00, 1, scotland, | → | dispatch date 2026-04-06, delivery date 2026-04-07, same day dispatch true |
| Easter Monday is a holiday in England and Wales | 2026-04-06, 10:00, 15:00, 1, england-and-wales, | → | dispatch date 2026-04-07, delivery date 2026-04-08, same day dispatch false |
| Christmas Eve, two days: skips Christmas, the weekend and the Boxing Day substitute | 2026-12-24, 12:00, 15:00, 2, england-and-wales, | → | dispatch date 2026-12-24, delivery date 2026-12-30, same day dispatch true |
| New Year's Eve: next year's New Year's Day is skipped too | 2026-12-31, 10:00, 15:00, 1, england-and-wales, | → | dispatch date 2026-12-31, delivery date 2027-01-04, same day dispatch true |
Show the other 16 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| New Year's Eve in Scotland also skips the 2nd January substitute | 2026-12-31, 10:00, 15:00, 1, scotland, | → | dispatch date 2026-12-31, delivery date 2027-01-05, same day dispatch true |
| five-day service over the August bank holiday | 2026-08-24, 09:00, 15:00, 5, england-and-wales, | → | dispatch date 2026-08-24, delivery date 2026-09-01, same day dispatch true |
| same-day service arrives on the dispatch day | 2026-09-23, 10:00, 15:00, 0, england-and-wales, | → | dispatch date 2026-09-23, delivery date 2026-09-23, same day dispatch true |
| same-day service ordered too late arrives the next working day | 2026-09-23, 15:30, 15:00, 0, england-and-wales, | → | dispatch date 2026-09-24, delivery date 2026-09-24, same day dispatch false |
| no region: Christmas Day is a working day | 2026-12-24, 16:00, 15:00, 1, —, | → | dispatch date 2026-12-25, delivery date 2026-12-28, same day dispatch false |
| a closure in transit is skipped | 2026-09-23, 10:00, 15:00, 2, —, 2026-09-24 | → | dispatch date 2026-09-23, delivery date 2026-09-28, same day dispatch true |
| a closure on the order day delays dispatch | 2026-09-23, 10:00, 15:00, 1, —, 2026-09-23 | → | dispatch date 2026-09-24, delivery date 2026-09-25, same day dispatch false |
| a midnight cut-off means nothing goes the same day | 2026-09-23, 00:00, 00:00, 1, england-and-wales, | → | dispatch date 2026-09-24, delivery date 2026-09-25, same day dispatch false |
| a cut-off of 23:59 | 2026-09-23, 23:58, 23:59, 1, england-and-wales, | → | dispatch date 2026-09-23, delivery date 2026-09-24, same day dispatch true |
| an estimate reaching a year gov.uk has not published | 2028-12-29, 10:00, 15:00, 1, england-and-wales, | → | error: no bank holiday data for england-and-wales in 2029 |
| a 12-hour time | 2026-09-23, 3pm, 15:00, 1, england-and-wales, | → | error: orderTime must be a 24-hour time HH:MM |
| 24:00 is not a time of day | 2026-09-23, 24:00, 15:00, 1, england-and-wales, | → | error: orderTime must be a 24-hour time HH:MM |
| a cut-off with 60 minutes | 2026-09-23, 10:00, 15:60, 1, england-and-wales, | → | error: cutOff must be a 24-hour time HH:MM |
| negative service days | 2026-09-23, 10:00, 15:00, -1, england-and-wales, | → | error: serviceDays must not be negative |
| an impossible order date | 2026-02-30, 10:00, 15:00, 1, england-and-wales, | → | error: 2026-02-30 |
| an unknown region | 2026-09-23, 10:00, 15:00, 1, wales, | → | error: unknown region "wales" |
More from the author
## Decisions
- **Before, not at.** An order at 15:00 exactly misses a 15:00 cut-off, matching "order before 3pm". Times are the warehouse's local clock, HH:MM in 24 hours; converting a customer's time zone is the caller's job. - **Weekend and holiday orders** are dispatched on the next working day whatever the time. - **Holidays in the right years.** Bank holidays are loaded for the order's year and then for each later year the estimate reaches, so an order on 31 December skips New Year's Day. An estimate that reaches a year gov.uk has not published yet is an error from `dates.bank-holidays` rather than a guess that treats unknown holidays as working days. - **One calendar for both ends.** The same working days apply to dispatch and delivery. A carrier that delivers on Saturdays, or a delivery into a region with different holidays, needs its own call or its own `closures`. - **An estimate, not a promise.** It knows nothing about carrier delays, remote-area surcharges, or the actual collection time.
## Sources
UK bank holidays: gov.uk, https://www.gov.uk/bank-holidays (via `dates.bank-holidays`).
Files
| Path | Bytes |
|---|---|
| README.md | 1,665 |
| impl/python.py | 2,798 |
| impl/rust.rs | 3,733 |
| impl/typescript.ts | 2,692 |
| vectors.json | 5,525 |