use super::funejson::Value; use super::math_big_integer::{parse_big_integer, BigInt}; use super::math_round_div_big::round_div_big; use super::money_amount::{money, money_to_value}; use super::units_convert::convert_units; 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(), )) }