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(),
))
}