use super::funejson::Value; use super::math_round_div::round_div; use super::money_add::subtract_money; use super::money_amount::{money, money_from_value, money_to_value, Money}; use super::money_sum::sum_money; const MAX_MINOR: i64 = 900_000_000_000; /// Gross margin: enterprise output less its variable costs, per hectare or head. /// /// Each per-unit figure is its own total divided once, half up. Taking output /// per head less costs per head instead can be a penny out: 3 head with 10000 /// output and 5000 costs is 3333 - 1667 = 1666 that way, but 5000 / 3 = 1667. /// /// # Panics /// Panics on a bad basis or unit count, mixed currencies, or totals too large. pub fn gross_margin(outputs: &[MarginLine], variable_costs: &[MarginLine], per: &str, units: f64, currency: &str) -> GrossMargin { let (divisor, scale) = match per { "hectare" => { if !units.is_finite() || units <= 0.0 || units > 10_000_000.0 { panic!("units must be a finite number of hectares greater than 0 and at most 10000000, received {}", units); } let d = (units * 10000.0 + 0.5).floor() as i64; if d < 1 { panic!("units must be at least 0.0001 hectares, received {}", units); } (d, 10000) } "head" => { if units.fract() != 0.0 || units < 1.0 || units > 10_000_000.0 { panic!("units must be a whole number of head from 1 to 10000000, received {}", units); } (units as i64, 1) } other => panic!("unknown basis \"{}\": use hectare or head", other), }; let output_amounts: Vec = outputs.iter().map(|l| l.amount.clone()).collect(); let cost_amounts: Vec = variable_costs.iter().map(|l| l.amount.clone()).collect(); let output = sum_money(&output_amounts, currency); let costs = sum_money(&cost_amounts, currency); let margin = subtract_money(&output, &costs); for m in [&output, &costs, &margin] { if m.minor.abs() > MAX_MINOR { panic!("totals must be within 900000000000 minor units either side of zero"); } } let per_unit = |m: &Money| money(round_div(m.minor * scale, divisor, "half-up"), currency); GrossMargin { output_per_unit: per_unit(&output), variable_costs_per_unit: per_unit(&costs), gross_margin_per_unit: per_unit(&margin), margin_basis_points: if output.minor == 0 { None } else { Some(round_div(margin.minor * 10000, output.minor, "half-up")) }, output, variable_costs: costs, gross_margin: margin, } } pub fn margin_line_from_value(v: &Value) -> MarginLine { MarginLine { name: v.get("name").as_str().to_string(), amount: money_from_value(v.get("amount")), } } pub fn gross_margin_to_value(g: &GrossMargin) -> Value { Value::obj(vec![ ("output", money_to_value(&g.output)), ("variableCosts", money_to_value(&g.variable_costs)), ("grossMargin", money_to_value(&g.gross_margin)), ("outputPerUnit", money_to_value(&g.output_per_unit)), ("variableCostsPerUnit", money_to_value(&g.variable_costs_per_unit)), ("grossMarginPerUnit", money_to_value(&g.gross_margin_per_unit)), ("marginBasisPoints", g.margin_basis_points.map_or(Value::Null, Value::Int)), ]) } pub fn fune_vector(args: &[Value]) -> Value { let outputs: Vec = args[0].as_arr().iter().map(margin_line_from_value).collect(); let costs: Vec = args[1].as_arr().iter().map(margin_line_from_value).collect(); gross_margin_to_value(&gross_margin(&outputs, &costs, args[2].as_str(), args[3].as_f64(), args[4].as_str())) }