Functional Weave
Code in Rust

fleet.fuel-cost@1.0.0

impl/rust.rs

4,851 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::{parse_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_to_value};  ← from money.amount ^1.0.0 · built alongside by fune
use super::units_convert::convert_units;  ← from units.convert ^1.0.0 · built alongside by fune

const MAX_SAFE: i128 = 9007199254740991;

/// A plain decimal as an exact fraction: (digits without the point, power of ten).
fn decimal(name: &str, text: &str) -> (BigInt, BigInt) {
    let mut parts = text.splitn(2, '.');
    let whole = parts.next().unwrap_or("");
    let frac = parts.next();
    let ok = !whole.is_empty()
        && whole.bytes().all(|b| b.is_ascii_digit())
        && frac.map_or(true, |f| !f.is_empty() && f.bytes().all(|b| b.is_ascii_digit()));
    if !ok {
        panic!("{} must be a plain decimal of 0 or more, received \"{}\"", name, text);
    }
    let digits: String = text.chars().filter(|c| *c != '.').collect();
    let trimmed = digits.trim_start_matches('0');
    let value = parse_big_integer(if trimmed.is_empty() { "0" } else { trimmed });
    let places = frac.map_or(0, |f| f.len()) as u64;
    (value, BigInt::from_i64(10).pow(places))
}

/// The fuel a trip uses and what it costs.
///
/// Everything stays an exact fraction until the one rounding step: litres come
/// from the distance and economy, and the cost from those exact litres.
/// Rounding the litres first, or using 4.546 for the imperial gallon, is a
/// penny out often enough to break a reconciliation. The gallon and the mile
/// come from units.convert, which defines them exactly.
///
/// # Panics
/// Panics on an unknown unit, a malformed or negative number, a zero economy,
/// a negative price, a bad currency or mode, or a cost beyond 2^53 - 1.
pub fn fuel_cost(
    distance: &str,
    distance_unit: &str,
    economy: &str,
    economy_unit: &str,
    price_per_litre_tenths: i64,
    currency: &str,
    mode: &str,
) -> FuelCost {
    if distance_unit != "mi" && distance_unit != "km" {
        panic!("unknown distance unit \"{}\"", distance_unit);
    }
    if economy_unit != "mpg-uk" && economy_unit != "mpg-us" && economy_unit != "l-per-100km" {
        panic!("unknown economy unit \"{}\"", economy_unit);
    }
    let (d_num, d_den) = decimal("distance", distance);
    let (e_num, e_den) = decimal("economy", economy);
    if e_num.is_zero() {
        panic!("economy must be greater than zero");
    }
    if price_per_litre_tenths < 0 {
        panic!("pricePerLitreTenths must not be negative, received {}", price_per_litre_tenths);
    }
    money(0, currency);

    let one = BigInt::from_i64(1);
    let (km_num, km_den) = decimal("mile", &convert_units("1", "mi", "km", 12));
    let (l_num, l_den) = if economy_unit == "l-per-100km" {
        // litres = km x (L/100km) / 100
        let (fn_, fd) = if distance_unit == "mi" { (km_num, km_den) } else { (one.clone(), one.clone()) };
        (d_num.mul(&e_num).mul(&fn_), d_den.mul(&e_den).mul(&BigInt::from_i64(100)).mul(&fd))
    } else {
        // litres = miles / mpg x litres per gallon
        let gallon = if economy_unit == "mpg-uk" { "gal_imp" } else { "gal_us" };
        let (g_num, g_den) = decimal("gallon", &convert_units("1", gallon, "L", 12));
        let (fn_, fd) = if distance_unit == "km" { (km_den, km_num) } else { (one.clone(), one.clone()) };
        (d_num.mul(&e_den).mul(&g_num).mul(&fn_), d_den.mul(&e_num).mul(&g_den).mul(&fd))
    };

    let cost_text = round_div_big(
        &l_num.mul(&BigInt::from_i64(price_per_litre_tenths)).to_string(),
        &l_den.mul(&BigInt::from_i64(10)).to_string(),
        mode,
    );
    let cost = parse_big_integer(&cost_text);
    if cost > BigInt::from_i128(MAX_SAFE) {
        panic!("the fuel cost exceeds 2^53 - 1 minor units");
    }
    let thousandths = parse_big_integer(&round_div_big(
        &l_num.mul(&BigInt::from_i64(1000)).to_string(),
        &l_den.to_string(),
        "half-up",
    ));
    let (whole, frac) = thousandths.div_rem(&BigInt::from_i64(1000));
    FuelCost {
        litres: format!("{}.{:03}", whole, frac.to_i64()),
        cost: money(cost.to_i64(), currency),
    }
}

pub fn fuel_cost_to_value(f: &FuelCost) -> Value {
    Value::obj(vec![("litres", Value::str(&f.litres)), ("cost", money_to_value(&f.cost))])
}

pub fn fune_vector(args: &[Value]) -> Value {
    if let Value::Float(f) = &args[4] {
        panic!("pricePerLitreTenths must be an integer, received {}", f);
    }
    for (i, name) in [(0usize, "distance"), (2usize, "economy")] {
        if !matches!(args[i], Value::Str(_)) {
            panic!("{} must be a plain decimal of 0 or more, received a non-string", name);
        }
    }
    fuel_cost_to_value(&fuel_cost(
        args[0].as_str(),
        args[1].as_str(),
        args[2].as_str(),
        args[3].as_str(),
        args[4].as_i64(),
        args[5].as_str(),
        args[6].as_str(),
    ))
}