Functional Weave
Code in Rust

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.

pub fn service_due(last_service_date: &str, last_service_mileage: i64, interval_months: i64, interval_miles: i64, on_date: &str, current_mileage: i64) -> 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 is a string in Rust, one of: "date", "mileage".
// Parameters take it as &str and results hold it as String.

/// Both limits, how far away each is, and which one comes first.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ServiceDue {
    /// last service plus the interval in months, clamped to the month end; null without one
    pub due_date: Option<String>,
    /// last service mileage plus the interval in miles; null without one
    pub due_mileage: Option<i64>,
    /// onDate to dueDate; negative once past
    pub days_remaining: Option<i64>,
    /// dueMileage less currentMileage; negative once past
    pub miles_remaining: Option<i64>,
    /// when dueMileage will be reached at the average daily mileage since the last service
    pub projected_mileage_date: Option<String>,
    /// the earlier of dueDate and the mileage date (onDate once the mileage is reached)
    pub next_due_date: Option<String>,
    /// which limit gives nextDueDate; date on a tie
    pub due_by: Option<String>,
    /// past dueDate, or past dueMileage
    pub overdue: bool,
}

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

fune!(fleet.service-due@^1);  // then call service_due(…)
impl/rust.rs · 129 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::add_days;  ← from dates.add-days ^1.0.0 · built alongside by fune
use super::dates_add_months::add_months;  ← from dates.add-months ^1.0.0 · built alongside by fune
use super::dates_days_between::days_between;  ← from dates.days-between ^1.0.0 · built alongside by fune
use super::math_round_div::round_div;  ← from math.round-div ^1.0.0 · built alongside by fune

fn whole_number(name: &str, value: i64) {
    if value < 0 {
        panic!("{} must be a whole number, 0 or more, received {}", name, value);
    }
}

/// 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.
///
/// # Panics
/// Panics on a negative number, no interval at all, a malformed date, or an
/// onDate or mileage before the last service.
pub fn service_due(
    last_service_date: &str,
    last_service_mileage: i64,
    interval_months: i64,
    interval_miles: i64,
    on_date: &str,
    current_mileage: i64,
) -> ServiceDue {
    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 && interval_miles == 0 {
        panic!("at least one of intervalMonths and intervalMiles must be above zero");
    }
    let days_since = days_between(last_service_date, on_date);
    if days_since < 0 {
        panic!("onDate {} is before the last service on {}", on_date, last_service_date);
    }
    let miles_since = current_mileage - last_service_mileage;
    if miles_since < 0 {
        panic!(
            "currentMileage {} is below the mileage at the last service, {}",
            current_mileage, last_service_mileage
        );
    }

    let due_date = if interval_months > 0 { Some(add_months(last_service_date, interval_months)) } else { None };
    let days_remaining = due_date.as_ref().map(|d| days_between(on_date, d));
    let due_mileage = if interval_miles > 0 { Some(last_service_mileage + interval_miles) } else { None };
    let miles_remaining = due_mileage.map(|m| m - current_mileage);

    let mut projected: Option<String> = None;
    let mut mileage_date: Option<String> = None;
    if let Some(remaining) = miles_remaining {
        if remaining <= 0 {
            mileage_date = Some(on_date.to_string());
        } else if days_since > 0 && miles_since > 0 {
            let date = add_days(on_date, round_div(remaining * days_since, miles_since, "up"));
            projected = Some(date.clone());
            mileage_date = Some(date);
        }
    }

    let mut next_due = due_date.clone();
    let mut due_by: Option<String> = due_date.as_ref().map(|_| "date".to_string());
    if let Some(md) = mileage_date {
        if next_due.as_ref().map_or(true, |n| md < *n) {
            next_due = Some(md);
            due_by = Some("mileage".to_string());
        }
    }

    ServiceDue {
        overdue: days_remaining.map_or(false, |d| d < 0) || miles_remaining.map_or(false, |m| m < 0),
        due_date,
        due_mileage,
        days_remaining,
        miles_remaining,
        projected_mileage_date: projected,
        next_due_date: next_due,
        due_by,
    }
}

fn opt_str(v: &Option<String>) -> Value {
    match v {
        Some(s) => Value::str(s),
        None => Value::Null,
    }
}

fn opt_int(v: Option<i64>) -> Value {
    match v {
        Some(i) => Value::Int(i),
        None => Value::Null,
    }
}

pub fn service_due_to_value(s: &ServiceDue) -> Value {
    Value::obj(vec![
        ("dueDate", opt_str(&s.due_date)),
        ("dueMileage", opt_int(s.due_mileage)),
        ("daysRemaining", opt_int(s.days_remaining)),
        ("milesRemaining", opt_int(s.miles_remaining)),
        ("projectedMileageDate", opt_str(&s.projected_mileage_date)),
        ("nextDueDate", opt_str(&s.next_due_date)),
        ("dueBy", opt_str(&s.due_by)),
        ("overdue", Value::Bool(s.overdue)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    for (i, name) in [(1usize, "lastServiceMileage"), (2, "intervalMonths"), (3, "intervalMiles"), (5, "currentMileage")] {
        if let Value::Float(f) = &args[i] {
            panic!("{} must be a whole number, 0 or more, received {}", name, f);
        }
    }
    service_due_to_value(&service_due(
        args[0].as_str(),
        args[1].as_i64(),
        args[2].as_i64(),
        args[3].as_i64(),
        args[4].as_str(),
        args[5].as_i64(),
    ))
}

Install

fune build

With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and its 4 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 fleet.service-due
Download for Rust fleet.service-due-1.0.0-rust.fune · 15,594 bytes sha256 ec835d087d48894f307fa6d2a1bd8bdc4afbe4cda1ba9258474fbf7230d19fc4

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

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