Functional Weave
Code in Rust

dates.overlap

Days two date ranges have in common, with the end of each range stated as inclusive or exclusive.

1.0.0 · published 2026-10-03 by charlie · Anterra

Pinned by 15 tests, run in TypeScript, Python and Rust.

What it does

How many days two ranges share: the days of a policy that fall in a tax year, the nights of a booking inside a rate season, the part of a lease inside a service-charge year.

Whether a range's end date is one of its days is the question every overlap bug comes from, so it is an argument rather than a default. With `endInclusive` true, ranges are written the way a statement prints them: 1 to 10 January is ten days, and ranges that share only 10 January overlap by one day. With it false, ranges are half-open, the way a hotel stay or dates.business-days-between works: 1 to 10 January is nine days (or nights), and ranges that meet at 10 January do not overlap at all. Both ranges use the same convention.

For example

  • overlap_days(2026-01-01, 2026-01-10, 2026-01-05, 2026-01-20, true) → 6 inclusive: 1-10 Jan and 5-20 Jan share 5 to 10 January, six days
  • overlap_days(2026-01-01, 2026-01-10, 2026-01-05, 2026-01-20, false) → 5 exclusive: the same dates as half-open ranges share five days
  • overlap_days(2026-01-01, 2026-01-10, 2026-01-10, 2026-01-20, true) → 1 inclusive: ranges sharing only their boundary day overlap by one

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 overlap_days(a_start: &str, a_end: &str, b_start: &str, b_end: &str, end_inclusive: bool) -> i64
a_startdatefirst day of range A
a_enddateend of range A
b_startdatefirst day of range B
b_enddateend of range B
end_inclusivebooltrue: the end dates are the last day of each range (1-10 Jan is 10 days); false: the end is the first day after (1-10 Jan is 9 days)
returnsintdays in both ranges; 0 when they do not meet

Your code names it in one line, in the file that uses it

fune!(dates.overlap@^1);  // then call overlap_days(…)
impl/rust.rs · 35 lines · open · raw

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_days::epoch_day_from_iso;  ← from dates.add-days ^1.0.0 · built alongside by fune

/// Days two ranges have in common. `end_inclusive` says whether each end date
/// is the range's last day (a statement period) or the day after it (a
/// half-open range, a hotel stay); both ranges use the same convention.
///
/// # Panics
/// Panics on a malformed date or a range that ends before it starts.
pub fn overlap_days(a_start: &str, a_end: &str, b_start: &str, b_end: &str, end_inclusive: bool) -> i64 {
    let a0 = epoch_day_from_iso(a_start);
    let a1 = epoch_day_from_iso(a_end);
    let b0 = epoch_day_from_iso(b_start);
    let b1 = epoch_day_from_iso(b_end);
    if a1 < a0 {
        panic!("range A starts {}, after its end {}", a_start, a_end);
    }
    if b1 < b0 {
        panic!("range B starts {}, after its end {}", b_start, b_end);
    }
    // An inclusive end is the half-open end one day later.
    let extra = if end_inclusive { 1 } else { 0 };
    let days = a1.min(b1) + extra - a0.max(b0);
    days.max(0)
}

pub fn fune_vector(args: &[Value]) -> Value {
    Value::Int(overlap_days(
        args[0].as_str(),
        args[1].as_str(),
        args[2].as_str(),
        args[3].as_str(),
        args[4].as_bool(),
    ))
}

Install

fune build

With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and its 1 dependency, 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 dates.overlap
Download for Rust dates.overlap-1.0.0-rust.fune · 6,830 bytes sha256 ff3980595eb8490b5a49c9a33a45675b85c4671da7b0e5b30c16e7cafb02f512

The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./dates.overlap-1.0.0-rust.fune, or fetch it from a terminal with fune pull dates.overlap@1.0.0:rust.

The whole function, every language, is one file too: dates.overlap-1.0.0.fune, 8,742 bytes, sha256 08731d1504867ad1b280bdda86d5d7066507b82cdd9835266ada4d9e2c81b210. 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 dates.overlap

after — your function gets the result and the arguments, and returns the final result.

// fune: after dates.overlap

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-days in dates.overlap

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 dates.overlap --steps.

// fune: step dates.overlap 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.

CaseArgumentsExpected
inclusive: 1-10 Jan and 5-20 Jan share 5 to 10 January, six days 2026-01-01, 2026-01-10, 2026-01-05, 2026-01-20, true → 6
exclusive: the same dates as half-open ranges share five days 2026-01-01, 2026-01-10, 2026-01-05, 2026-01-20, false → 5
inclusive: ranges sharing only their boundary day overlap by one 2026-01-01, 2026-01-10, 2026-01-10, 2026-01-20, true → 1
exclusive: a stay ending on the day the next begins does not overlap 2026-01-01, 2026-01-10, 2026-01-10, 2026-01-20, false → 0
ranges that do not meet give zero, not a negative number 2026-01-01, 2026-01-10, 2026-03-01, 2026-03-10, true → 0
the order of the ranges does not matter 2026-01-05, 2026-01-20, 2026-01-01, 2026-01-10, true → 6
a range inside another overlaps by its own length: February 2026 2026-01-01, 2026-12-31, 2026-02-01, 2026-02-28, true → 28
a leap February counts its 29th 2024-02-01, 2024-03-31, 2024-02-15, 2024-02-29, true → 15
a policy year against the UK tax year from 6 April 2025-10-01, 2026-09-30, 2026-04-06, 2027-04-05, true → 178
inclusive: a one-day range inside another is one day 2026-09-23, 2026-09-23, 2026-09-01, 2026-09-30, true → 1
Show the other 5 tests
CaseArgumentsExpected
exclusive: a range whose start equals its end is empty 2026-09-23, 2026-09-23, 2026-09-01, 2026-09-30, false → 0
across a year end, inclusive 2026-12-20, 2027-01-05, 2026-12-25, 2027-01-01, true → 8
a range that ends before it starts is an error 2026-01-10, 2026-01-01, 2026-01-05, 2026-01-20, true → error: range A starts 2026-01-10, after its end 2026-01-01
the second range is checked too 2026-01-01, 2026-01-10, 2026-01-20, 2026-01-05, false → error: range B starts 2026-01-20, after its end 2026-01-05
an impossible date is an error 2026-02-30, 2026-03-10, 2026-03-01, 2026-03-05, true → error: is not a real calendar date

More from the author

Ranges that do not meet give 0, never a negative number. A range whose end is before its start is an error; an exclusive range whose start equals its end is empty (0 days) and allowed.

Dates are calendar days from the dates.add-days kernel, so leap days count and there are no time zones.

Files

PathBytes
README.md1,017
impl/python.py908
impl/rust.rs1,238
impl/typescript.ts917
vectors.json2,441