Functional Weave
Code in Rust

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