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