Functional Weave
Code in Rust

fleet.vehicle-depreciation@1.0.0

impl/rust.rs

4,148 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::math_big_integer::BigInt;  ← from math.big-integer ^1.0.0 · built alongside by fune
use super::math_round_div_big::round_div_big;  ← from math.round-div-big ^1.0.0 · built alongside by fune
use super::money_amount::{money, money_from_value, money_to_value, Money};  ← from money.amount ^1.0.0 · built alongside by fune

fn whole(name: &str, value: i64, max: i64) {
    if value < 0 || value > max {
        panic!("{} must be a whole number from 0 to {}, received {}", name, max, value);
    }
}

/// A vehicle's value from its age and mileage, on the caller's retention curves.
///
/// Each whole year multiplies by that year's retention and a part year by a
/// straight-line share of it; each whole 10,000 miles multiplies by the
/// mileage retention and the remaining miles by a straight-line share. The
/// product is kept as one exact fraction and rounded once: rounding the value
/// year by year drifts by a penny or more over a long life.
///
/// # Panics
/// Panics on a negative cost, an out-of-range age, mileage or rate, an empty
/// curve or an unknown mode.
pub fn vehicle_depreciation(
    cost: &Money,
    age_months: i64,
    mileage: i64,
    retained_per_year_basis_points: &[i64],
    retained_per10k_miles_basis_points: i64,
    mode: &str,
) -> VehicleValue {
    if cost.minor < 0 {
        panic!("cost must not be negative, received {}", cost.minor);
    }
    whole("ageMonths", age_months, 600);
    whole("mileage", mileage, 2000000);
    if retained_per_year_basis_points.is_empty() {
        panic!("retainedPerYearBasisPoints must have at least one year");
    }
    for bp in retained_per_year_basis_points {
        whole("a retention rate", *bp, 10000);
    }
    whole("a retention rate", retained_per10k_miles_basis_points, 10000);

    let last = retained_per_year_basis_points.len() - 1;
    let rate_for = |year: usize| retained_per_year_basis_points[year.min(last)];
    let ten_thousand = BigInt::from_i64(10000);
    let mut num = BigInt::from_i64(1);
    let mut den = BigInt::from_i64(1);
    let years = (age_months / 12) as usize;
    for year in 0..years {
        num = num.mul(&BigInt::from_i64(rate_for(year)));
        den = den.mul(&ten_thousand);
    }
    let months = age_months % 12;
    if months > 0 {
        num = num.mul(&BigInt::from_i64(120000 - (10000 - rate_for(years)) * months));
        den = den.mul(&BigInt::from_i64(120000));
    }
    let mile_rate = retained_per10k_miles_basis_points;
    for _ in 0..(mileage / 10000) {
        num = num.mul(&BigInt::from_i64(mile_rate));
        den = den.mul(&ten_thousand);
    }
    let miles = mileage % 10000;
    if miles > 0 {
        num = num.mul(&BigInt::from_i64(100000000 - (10000 - mile_rate) * miles));
        den = den.mul(&BigInt::from_i64(100000000));
    }

    let value: i64 = round_div_big(&BigInt::from_i64(cost.minor).mul(&num).to_string(), &den.to_string(), mode)
        .parse()
        .unwrap();
    let retained: i64 = round_div_big(&num.mul(&ten_thousand).to_string(), &den.to_string(), "half-up")
        .parse()
        .unwrap();
    VehicleValue {
        value: money(value, &cost.currency),
        depreciation: money(cost.minor - value, &cost.currency),
        retained_basis_points: retained,
    }
}

pub fn vehicle_value_to_value(v: &VehicleValue) -> Value {
    Value::obj(vec![
        ("value", money_to_value(&v.value)),
        ("depreciation", money_to_value(&v.depreciation)),
        ("retainedBasisPoints", Value::Int(v.retained_basis_points)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    for (i, name) in [(1usize, "ageMonths"), (2, "mileage"), (4, "a retention rate")] {
        if let Value::Float(f) = &args[i] {
            panic!("{} must be a whole number, received {}", name, f);
        }
    }
    let curve: Vec<i64> = args[3]
        .as_arr()
        .iter()
        .map(|v| {
            if let Value::Float(f) = v {
                panic!("a retention rate must be a whole number, received {}", f);
            }
            v.as_i64()
        })
        .collect();
    vehicle_value_to_value(&vehicle_depreciation(
        &money_from_value(&args[0]),
        args[1].as_i64(),
        args[2].as_i64(),
        &curve,
        args[4].as_i64(),
        args[5].as_str(),
    ))
}