use super::funejson::Value; use super::math_rational::{multiply_rational, rational, rational_from_value, rational_to_integer, Rational}; use super::money_amount::{assert_same_currency, money, money_from_value, money_to_value, Money}; fn not_negative(name: &str, value: &Rational) -> Rational { let r = rational(value.numerator, value.denominator); if r.numerator < 0 { panic!("{} must not be negative", name); } r } fn cost_of(name: &str, value: &Money, unit: &Money) -> i64 { assert_same_currency(unit, value); if value.minor < 0 { panic!("{} must not be negative, received {}", name, value.minor); } value.minor } fn variance(signed: i64, currency: &str) -> Variance { let effect = if signed > 0 { "adverse" } else if signed < 0 { "favourable" } else { "none" }; Variance { amount: money(signed.abs(), currency), effect: effect.to_string(), } } /// Material price and usage, labour rate and efficiency variances, flexed to /// actual output. The four flexed costs are each rounded once and every /// variance is a difference of them, so the variances always sum exactly. /// /// # Panics /// Panics on negative output, prices, rates, quantities, hours or costs, mixed /// currencies, or an unknown rounding mode. pub fn cost_variances(material: &MaterialActual, labour: &LabourActual, actual_output: i64, mode: &str) -> CostVariances { if !(0..=(1i64 << 53) - 1).contains(&actual_output) { panic!("actualOutput must be a whole number, not negative, received {}", actual_output); } let unit = &material.standard_price; let currency = unit.currency.as_str(); let sp = cost_of("material standardPrice", &material.standard_price, unit); let mat_actual = cost_of("material actualCost", &material.actual_cost, unit); let sr = cost_of("labour standardRate", &labour.standard_rate, unit); let lab_actual = cost_of("labour actualCost", &labour.actual_cost, unit); let sq = not_negative("material standardQuantity", &material.standard_quantity); let aq = not_negative("material actualQuantity", &material.actual_quantity); let sh = not_negative("labour standardHours", &labour.standard_hours); let ah = not_negative("labour actualHours", &labour.actual_hours); let output = rational(actual_output, 1); let actual_at_standard = rational_to_integer(&multiply_rational(&aq, &rational(sp, 1)), mode); let standard_material = rational_to_integer(&multiply_rational(&multiply_rational(&sq, &output), &rational(sp, 1)), mode); let hours_at_standard = rational_to_integer(&multiply_rational(&ah, &rational(sr, 1)), mode); let standard_labour = rational_to_integer(&multiply_rational(&multiply_rational(&sh, &output), &rational(sr, 1)), mode); let standard_cost = standard_material + standard_labour; let actual_cost = mat_actual + lab_actual; CostVariances { material_price: variance(mat_actual - actual_at_standard, currency), material_usage: variance(actual_at_standard - standard_material, currency), labour_rate: variance(lab_actual - hours_at_standard, currency), labour_efficiency: variance(hours_at_standard - standard_labour, currency), total: variance(actual_cost - standard_cost, currency), standard_cost: money(standard_cost, currency), actual_cost: money(actual_cost, currency), } } fn variance_to_value(v: &Variance) -> Value { Value::obj(vec![("amount", money_to_value(&v.amount)), ("effect", Value::str(&v.effect))]) } pub fn cost_variances_to_value(c: &CostVariances) -> Value { Value::obj(vec![ ("materialPrice", variance_to_value(&c.material_price)), ("materialUsage", variance_to_value(&c.material_usage)), ("labourRate", variance_to_value(&c.labour_rate)), ("labourEfficiency", variance_to_value(&c.labour_efficiency)), ("total", variance_to_value(&c.total)), ("standardCost", money_to_value(&c.standard_cost)), ("actualCost", money_to_value(&c.actual_cost)), ]) } pub fn fune_vector(args: &[Value]) -> Value { let m = &args[0]; let l = &args[1]; if let Value::Float(f) = &args[2] { panic!("actualOutput must be a whole number, not negative, received {}", f); } let material = MaterialActual { standard_price: money_from_value(m.get("standardPrice")), standard_quantity: rational_from_value(m.get("standardQuantity")), actual_quantity: rational_from_value(m.get("actualQuantity")), actual_cost: money_from_value(m.get("actualCost")), }; let labour = LabourActual { standard_rate: money_from_value(l.get("standardRate")), standard_hours: rational_from_value(l.get("standardHours")), actual_hours: rational_from_value(l.get("actualHours")), actual_cost: money_from_value(l.get("actualCost")), }; cost_variances_to_value(&cost_variances(&material, &labour, args[2].as_i64(), args[3].as_str())) }