Functional Weave
Code in Python

fleet.service-due

Next vehicle service by months or miles, whichever comes first, projecting the mileage date, with an overdue flag.

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

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

What it does

When a vehicle's next service is due under a "every N months or N miles, whichever comes first" schedule, how far away each limit is, and whether the service is overdue.

## Whichever comes first

For example

  • service_due(2025-03-15, 30,000, 12, 10,000, 2025-09-15, 35,000) → due date 2026-03-15, due mileage 40,000, days remaining 181, miles remaining 5,000, projected mileage date 2026-03-18, next due date 2026-03-15, due by date, overdue false an average driver: the 12 months come three days before the 10,000 miles
  • service_due(2025-01-10, 20,000, 12, 10,000, 2025-04-10, 26,000) → due date 2026-01-10, due mileage 30,000, days remaining 275, miles remaining 4,000, projected mileage date 2025-06-09, next due date 2025-06-09, due by mileage, overdue false a high-mileage driver reaches 10,000 miles long before the year is up
  • service_due(2024-05-01, 10,000, 12, 12,000, 2025-05-20, 18,000) → due date 2025-05-01, due mileage 22,000, days remaining -19, miles remaining 4,000, projected mileage date 2025-11-28, next due date 2025-05-01, due by date, overdue true overdue by date though the mileage is not reached

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 service_due(last_service_date: str, last_service_mileage: int, interval_months: int, interval_miles: int, on_date: str, current_mileage: int) -> ServiceDue
last_service_datedatethe date of the last service
last_service_mileageintodometer reading at the last service
interval_monthsintmonths between services; 0 when the schedule is by mileage only
interval_milesintmiles between services; 0 when the schedule is by time only
on_datedatetoday, as the caller sees it
current_mileageintodometer reading on onDate
returnsServiceDue

The types it declares, generated into your project

ServiceTrigger = Literal["date", "mileage"]

@dataclass(frozen=True)
class ServiceDue:
    """Both limits, how far away each is, and which one comes first."""

    #: last service plus the interval in months, clamped to the month end; null without one
    due_date: Optional[str]
    #: last service mileage plus the interval in miles; null without one
    due_mileage: Optional[int]
    #: onDate to dueDate; negative once past
    days_remaining: Optional[int]
    #: dueMileage less currentMileage; negative once past
    miles_remaining: Optional[int]
    #: when dueMileage will be reached at the average daily mileage since the last service
    projected_mileage_date: Optional[str]
    #: the earlier of dueDate and the mileage date (onDate once the mileage is reached)
    next_due_date: Optional[str]
    #: which limit gives nextDueDate; date on a tie
    due_by: Optional[ServiceTrigger]
    #: past dueDate, or past dueMileage
    overdue: bool

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

from fune.fleet.service_due import service_due  # fleet.service-due@^1
impl/python.py · 75 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.

from typing import Optional

from .dates_add_days import add_days  ← from dates.add-days ^1.0.0 · built alongside by fune
from .dates_add_months import add_months  ← from dates.add-months ^1.0.0 · built alongside by fune
from .dates_days_between import days_between  ← from dates.days-between ^1.0.0 · built alongside by fune
from .fleet_service_due_types import ServiceDue, ServiceTrigger
from .math_round_div import round_div  ← from math.round-div ^1.0.0 · built alongside by fune


def _whole_number(name: str, value: int) -> None:
    if isinstance(value, bool) or not isinstance(value, int) or value < 0:
        raise ValueError("%s must be a whole number, 0 or more, received %r" % (name, value))


def service_due(
    last_service_date: str,
    last_service_mileage: int,
    interval_months: int,
    interval_miles: int,
    on_date: str,
    current_mileage: int,
) -> ServiceDue:
    """The next service, by time or mileage, whichever comes first.

    "Whichever comes first" needs a date for the mileage limit too, so it is
    projected from the average daily mileage since the last service, rounded up
    to a whole day: a service booked for the projected day is never late on
    that estimate.
    """
    _whole_number("lastServiceMileage", last_service_mileage)
    _whole_number("intervalMonths", interval_months)
    _whole_number("intervalMiles", interval_miles)
    _whole_number("currentMileage", current_mileage)
    if interval_months == 0 and interval_miles == 0:
        raise ValueError("at least one of intervalMonths and intervalMiles must be above zero")
    days_since = days_between(last_service_date, on_date)
    if days_since < 0:
        raise ValueError("onDate %s is before the last service on %s" % (on_date, last_service_date))
    miles_since = current_mileage - last_service_mileage
    if miles_since < 0:
        raise ValueError(
            "currentMileage %d is below the mileage at the last service, %d" % (current_mileage, last_service_mileage)
        )

    due_date = add_months(last_service_date, interval_months) if interval_months > 0 else None
    days_remaining = None if due_date is None else days_between(on_date, due_date)
    due_mileage = last_service_mileage + interval_miles if interval_miles > 0 else None
    miles_remaining = None if due_mileage is None else due_mileage - current_mileage

    projected: Optional[str] = None
    mileage_date: Optional[str] = None
    if miles_remaining is not None:
        if miles_remaining <= 0:
            mileage_date = on_date
        elif days_since > 0 and miles_since > 0:
            projected = add_days(on_date, round_div(miles_remaining * days_since, miles_since, "up"))
            mileage_date = projected

    next_due: Optional[str] = due_date
    due_by: Optional[ServiceTrigger] = None if due_date is None else "date"
    if mileage_date is not None and (next_due is None or mileage_date < next_due):
        next_due = mileage_date
        due_by = "mileage"

    return ServiceDue(
        due_date=due_date,
        due_mileage=due_mileage,
        days_remaining=days_remaining,
        miles_remaining=miles_remaining,
        projected_mileage_date=projected,
        next_due_date=next_due,
        due_by=due_by,
        overdue=(days_remaining is not None and days_remaining < 0)
        or (miles_remaining is not None and miles_remaining < 0),
    )

Install

fune build

With that line in your source, in a Python project (language python in fune.project), fune build resolves it and its 4 dependencies, 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 fleet.service-due
Download for Python fleet.service-due-1.0.0-python.fune · 14,159 bytes sha256 a714dd2c00c936f251533deb600b4ce51727c3b01c101c7414daf73ee5a03bdb

The manifest, vectors and README with only the Python implementation. Install it without the registry with fune add ./fleet.service-due-1.0.0-python.fune, or fetch it from a terminal with fune pull fleet.service-due@1.0.0:python.

The whole function, every language, is one file too: fleet.service-due-1.0.0.fune, 21,991 bytes, sha256 121547871626eacd7a5f48e89ec365542c118d12124d29b4defe9360ad0a5566. 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 fleet.service-due

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

# fune: after fleet.service-due

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 fleet.service-due
# fune: replace dates.add-months in fleet.service-due
# fune: replace dates.days-between in fleet.service-due
# fune: replace math.round-div in fleet.service-due

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 fleet.service-due --steps.

# fune: step fleet.service-due 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
an average driver: the 12 months come three days before the 10,000 miles 2025-03-15, 30,000, 12, 10,000, 2025-09-15, 35,000 → due date 2026-03-15, due mileage 40,000, days remaining 181, miles remaining 5,000, projected mileage date 2026-03-18, next due date 2026-03-15, due by date, overdue false
a high-mileage driver reaches 10,000 miles long before the year is up 2025-01-10, 20,000, 12, 10,000, 2025-04-10, 26,000 → due date 2026-01-10, due mileage 30,000, days remaining 275, miles remaining 4,000, projected mileage date 2025-06-09, next due date 2025-06-09, due by mileage, overdue false
overdue by date though the mileage is not reached 2024-05-01, 10,000, 12, 12,000, 2025-05-20, 18,000 → due date 2025-05-01, due mileage 22,000, days remaining -19, miles remaining 4,000, projected mileage date 2025-11-28, next due date 2025-05-01, due by date, overdue true
overdue by mileage: due from today, nothing to project 2025-01-01, 50,000, 24, 20,000, 2025-06-01, 70,500 → due date 2027-01-01, due mileage 70,000, days remaining 579, miles remaining -500, projected mileage date —, next due date 2025-06-01, due by mileage, overdue true
exactly at the due mileage is due, not overdue 2025-01-01, 0, 12, 10,000, 2025-07-01, 10,000 → due date 2026-01-01, due mileage 10,000, days remaining 184, miles remaining 0, projected mileage date —, next due date 2025-07-01, due by mileage, overdue false
time only: a service on 29 February is next due on 28 February 2024-02-29, 1,000, 12, 0, 2024-06-01, 3,000 → due date 2025-02-28, due mileage —, days remaining 272, miles remaining —, projected mileage date —, next due date 2025-02-28, due by date, overdue false
mileage only: projected from 100 miles a day 2025-01-01, 0, 0, 5,000, 2025-01-11, 1,000 → due date —, due mileage 5,000, days remaining —, miles remaining 4,000, projected mileage date 2025-02-20, next due date 2025-02-20, due by mileage, overdue false
serviced today: no average yet, so no projection 2025-03-01, 40,000, 12, 10,000, 2025-03-01, 40,000 → due date 2026-03-01, due mileage 50,000, days remaining 365, miles remaining 10,000, projected mileage date —, next due date 2026-03-01, due by date, overdue false
a vehicle that has not moved has no projection 2025-03-01, 40,000, 12, 10,000, 2025-06-01, 40,000 → due date 2026-03-01, due mileage 50,000, days remaining 273, miles remaining 10,000, projected mileage date —, next due date 2026-03-01, due by date, overdue false
the projection lands on the due date: a tie is by date 2025-01-01, 0, 1, 310, 2025-01-11, 100 → due date 2025-02-01, due mileage 310, days remaining 21, miles remaining 210, projected mileage date 2025-02-01, next due date 2025-02-01, due by date, overdue false
Show the other 8 tests
CaseArgumentsExpected
a fractional day of driving rounds the projection up: 56.6 days is 57 2025-01-01, 0, 12, 10,000, 2025-01-08, 1,100 → due date 2026-01-01, due mileage 10,000, days remaining 358, miles remaining 8,900, projected mileage date 2025-03-06, next due date 2025-03-06, due by mileage, overdue false
mileage only and not moving: nothing is due yet 2025-01-01, 0, 0, 5,000, 2025-02-01, 0 → due date —, due mileage 5,000, days remaining —, miles remaining 5,000, projected mileage date —, next due date —, due by —, overdue false
no interval at all is an error 2025-01-01, 0, 0, 0, 2025-02-01, 100 → error: at least one of intervalMonths and intervalMiles must be above zero
a negative interval is an error 2025-01-01, 0, -12, 10,000, 2025-02-01, 100 → error: intervalMonths must be a whole number, 0 or more
a fractional mileage is an error 2025-01-01, 0, 12, 10,000, 2025-02-01, 100.5 → error: currentMileage must be a whole number, 0 or more
a date before the last service is an error 2025-01-01, 0, 12, 10,000, 2024-12-31, 0 → error: is before the last service
a mileage below the last service is an error (clocked, or the wrong vehicle) 2025-01-01, 5,000, 12, 10,000, 2025-02-01, 4,000 → error: is below the mileage at the last service
a malformed date is an error 2025-1-1, 0, 12, 10,000, 2025-02-01, 100 → error: is not an ISO date

More from the author

The time limit is a date: the last service plus `intervalMonths`, clamped to the month end by `dates.add-months` (a service on 29 February 2024 with a 12-month interval is due on 28 February 2025). The mileage limit is a mileage, so to compare the two it is turned into a date: the average miles a day since the last service, projected forward from `onDate`, rounded **up** to a whole day. `nextDueDate` is the earlier of the two and `dueBy` says which; a tie counts as `date`.

There is no projection (and `projectedMileageDate` is null) when the service was today or the vehicle has not moved, since there is no average to project; `nextDueDate` then falls back to the date limit alone, or is null for a mileage-only schedule. Once the due mileage is reached the service is due from `onDate`.

## Overdue

`overdue` is true once `onDate` is after `dueDate` or `currentMileage` is above `dueMileage`. On the due date, or at exactly the due mileage, the service is due but not yet overdue; `daysRemaining` or `milesRemaining` is then 0, and both go negative once past.

## Inputs

`intervalMonths` or `intervalMiles` may be 0 to schedule by the other alone, but not both. `onDate` is passed in, never read from a clock. An `onDate` before the last service, or a mileage below the last service's (a clocked odometer, or the wrong vehicle), is an error rather than a guess.

Files

PathBytes
README.md1,590
impl/python.py3,160
impl/rust.rs4,525
impl/typescript.ts2,992
vectors.json5,011