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 daysoverlap_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 daysoverlap_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.
def overlap_days(a_start: str, a_end: str, b_start: str, b_end: str, end_inclusive: bool) -> int
| a_start | date | first day of range A |
| a_end | date | end of range A |
| b_start | date | first day of range B |
| b_end | date | end of range B |
| end_inclusive | bool | true: 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) |
| returns | int | days in both ranges; 0 when they do not meet |
Your code names it in one line, in the file that uses it
from fune.dates.overlap import overlap_days # dates.overlap@^1
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
from .dates_add_days import epoch_day_from_iso ← from dates.add-days ^1.0.0 · built alongside by fune
def overlap_days(a_start: str, a_end: str, b_start: str, b_end: str, end_inclusive: bool) -> int:
"""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.
"""
a0 = epoch_day_from_iso(a_start)
a1 = epoch_day_from_iso(a_end)
b0 = epoch_day_from_iso(b_start)
b1 = epoch_day_from_iso(b_end)
if a1 < a0:
raise ValueError("range A starts %s, after its end %s" % (a_start, a_end))
if b1 < b0:
raise ValueError("range B starts %s, after its end %s" % (b_start, b_end))
# An inclusive end is the half-open end one day later.
extra = 1 if end_inclusive else 0
days = min(a1, b1) + extra - max(a0, b0)
return days if days > 0 else 0Install
fune build
With that line in your source, in a Python project (language python in fune.project), fune build resolves it and its 1 dependency, pins them in fune.lock, downloads only the Python 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. Or pin a range in fune.project and build in one step:
fune add dates.overlap
The manifest, vectors and README with only the Python implementation. Install it without the registry with fune add ./dates.overlap-1.0.0-python.fune, or fetch it from a terminal with fune pull dates.overlap@1.0.0:python.
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.
| Case | Arguments | Expected | |
|---|---|---|---|
| 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
| Case | Arguments | Expected | |
|---|---|---|---|
| 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
| Path | Bytes |
|---|---|
| README.md | 1,017 |
| impl/python.py | 908 |
| impl/rust.rs | 1,238 |
| impl/typescript.ts | 917 |
| vectors.json | 2,441 |