use super::funejson::Value; use super::math_gcd_lcm::lcm; use super::money_allocate::allocate; use super::money_amount::{money_from_value, money_to_value, Money}; const MAX_SAFE: i128 = 9_007_199_254_740_991; /// Share a service charge between units by lease fraction, floor area or /// fixed percentage. Every share becomes an integer weight, and /// money.allocate does the one split, so the lines always add up to the total /// exactly. /// /// # Panics /// Panics on an unknown basis, no units, a negative share, a missing or /// unexpected denominator, fractions or percentages that do not add up to the /// whole, or shares too fine to split exactly. pub fn apportion_service_charge(total: &Money, units: &[ServiceChargeUnit], basis: &str) -> Vec { if basis != "fraction" && basis != "floor-area" && basis != "percentage" { panic!("unknown basis \"{}\": use fraction, floor-area or percentage", basis); } if units.is_empty() { panic!("units must not be empty"); } for u in units { if u.share < 0 || u.share as i128 > MAX_SAFE { panic!("unit \"{}\": share must be a whole number, zero or more, received {}", u.unit, u.share); } if basis == "fraction" { match u.of { Some(of) if of > 0 && of as i128 <= MAX_SAFE => {} _ => panic!("unit \"{}\": a fraction needs a denominator \"of\" greater than zero", u.unit), } } else if u.of.is_some() { panic!("unit \"{}\": \"of\" is only for the fraction basis", u.unit); } } let weights: Vec; if basis == "fraction" { let mut common: i64 = 1; for u in units { common = lcm(common, u.of.unwrap()); } weights = units.iter().map(|u| u.share as i128 * (common / u.of.unwrap()) as i128).collect(); let sum: i128 = weights.iter().sum(); if sum > MAX_SAFE { panic!("the fractions are too fine to apportion exactly"); } if sum != common as i128 { panic!("the fractions add up to {}/{}, not exactly 1", sum, common); } } else { weights = units.iter().map(|u| u.share as i128).collect(); let sum: i128 = weights.iter().sum(); if basis == "percentage" && sum != 10000 { panic!("the percentages add up to {} basis points, not 10000", sum); } if sum == 0 { panic!("the floor areas add up to zero"); } } let sum: i128 = weights.iter().sum(); if sum > MAX_SAFE || (total.minor as i128).abs() * sum > MAX_SAFE { panic!("the fractions are too fine to apportion exactly"); } let ratios: Vec = weights.iter().map(|w| *w as i64).collect(); let parts = allocate(total, &ratios); units .iter() .zip(parts) .map(|(u, charge)| ServiceChargeLine { unit: u.unit.clone(), charge }) .collect() } pub fn service_charge_unit_from_value(v: &Value) -> ServiceChargeUnit { let name = v.get("unit").as_str().to_string(); let share = match v.get("share") { Value::Int(s) => *s, other => panic!("unit \"{}\": share must be a whole number, zero or more, received {:?}", name, other), }; let of = match v.get("of") { Value::Null => None, Value::Int(o) => Some(*o), _ => panic!("unit \"{}\": a fraction needs a denominator \"of\" greater than zero", name), }; ServiceChargeUnit { unit: name, share, of } } pub fn service_charge_line_to_value(line: &ServiceChargeLine) -> Value { Value::obj(vec![("unit", Value::str(&line.unit)), ("charge", money_to_value(&line.charge))]) } pub fn fune_vector(args: &[Value]) -> Value { let units: Vec = args[1].as_arr().iter().map(service_charge_unit_from_value).collect(); Value::Arr( apportion_service_charge(&money_from_value(&args[0]), &units, args[2].as_str()) .iter() .map(service_charge_line_to_value) .collect(), ) }