Functional Weave
Code in TypeScript

fleet.service-due@1.0.0

impl/rust.rs

4,525 bytes · the Rust implementation · view 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(),
    ))
}