property.service-charge-apportion@1.0.1
impl/rust.rs
4,061 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_gcd_lcm::lcm; ← from math.gcd-lcm ^1.0.0 · built alongside by fune
use super::money_allocate::allocate; ← from money.allocate ^1.0.0 · built alongside by fune
use super::money_amount::{money_from_value, money_to_value, Money}; ← from money.amount ^1.0.0 · built alongside by fune
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<ServiceChargeLine> {
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<i128>;
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<i64> = 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<ServiceChargeUnit> = 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(),
)
}