Functional Weave
Code in Rust

manufacturing.bom-cost@1.0.0

impl/rust.rs

5,136 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::manufacturing_bom_explode::{bom_line_from_value, explode_bom, BomLine};  ← from manufacturing.bom-explode ^1.0.0 · built alongside by fune
use super::math_rational::{add_rational, multiply_rational, rational, rational_to_integer, Rational};  ← from math.rational ^1.0.0 · built alongside by fune
use super::money_amount::{assert_same_currency, money, money_from_value, money_to_value};  ← from money.amount ^1.0.0 · built alongside by fune

/// Standard cost of one unit, by cost element, rolled up through the BOM.
/// Every element is an exact fraction of a minor unit until it is rounded once.
///
/// # Panics
/// Panics on anything `explode_bom` refuses, a missing, duplicate or negative
/// material cost, a cost for a made item, negative operation time or rates,
/// or a cost or rate in another currency.
pub fn bom_cost(
    lines: &[BomLine],
    item: &str,
    material_costs: &[ItemCost],
    operations: &[RoutingOperation],
    currency: &str,
    mode: &str,
) -> CostRollup {
    let zero = money(0, currency);
    let mut needs: Vec<(String, Rational, bool)> = vec![(item.to_string(), rational(1, 1), false)];
    for r in explode_bom(lines, item, 1) {
        needs.push((r.item, r.quantity, r.leaf));
    }

    let mut costs: Vec<(&str, i64)> = Vec::new();
    for c in material_costs {
        assert_same_currency(&zero, &c.unit_cost);
        if costs.iter().any(|(n, _)| *n == c.item) {
            panic!("duplicate material cost for \"{}\"", c.item);
        }
        if c.unit_cost.minor < 0 {
            panic!("unitCost of \"{}\" must not be negative, received {}", c.item, c.unit_cost.minor);
        }
        costs.push((&c.item, c.unit_cost.minor));
    }

    let mut material = rational(0, 1);
    for (name, quantity, leaf) in &needs {
        let cost = costs.iter().find(|(n, _)| n == name).map(|(_, c)| *c);
        if !leaf {
            if cost.is_some() {
                panic!(
                    "\"{}\" is made from its own bill of materials, so its cost is rolled up; remove it from materialCosts",
                    name
                );
            }
            continue;
        }
        let cost = match cost {
            Some(c) => c,
            None => panic!("no material cost for \"{}\"", name),
        };
        material = add_rational(&material, &multiply_rational(quantity, &rational(cost, 1)));
    }

    let mut labour = rational(0, 1);
    let mut overhead = rational(0, 1);
    for op in operations {
        if op.seconds < 0 || op.seconds > (1i64 << 53) - 1 {
            panic!(
                "seconds of an operation on \"{}\" must be a whole number, not negative, received {}",
                op.item, op.seconds
            );
        }
        assert_same_currency(&zero, &op.labour_rate);
        assert_same_currency(&zero, &op.overhead_rate);
        if op.labour_rate.minor < 0 || op.overhead_rate.minor < 0 {
            panic!("the rates of an operation on \"{}\" must not be negative", op.item);
        }
        let need = match needs.iter().find(|(n, _, _)| *n == op.item) {
            Some((_, q, _)) => *q,
            None => continue,
        };
        let hours = multiply_rational(&need, &rational(op.seconds, 3600));
        labour = add_rational(&labour, &multiply_rational(&hours, &rational(op.labour_rate.minor, 1)));
        overhead = add_rational(&overhead, &multiply_rational(&hours, &rational(op.overhead_rate.minor, 1)));
    }

    let m = rational_to_integer(&material, mode);
    let l = rational_to_integer(&labour, mode);
    let o = rational_to_integer(&overhead, mode);
    CostRollup {
        material: money(m, currency),
        labour: money(l, currency),
        overhead: money(o, currency),
        total: money(m + l + o, currency),
    }
}

pub fn cost_rollup_to_value(c: &CostRollup) -> Value {
    Value::obj(vec![
        ("material", money_to_value(&c.material)),
        ("labour", money_to_value(&c.labour)),
        ("overhead", money_to_value(&c.overhead)),
        ("total", money_to_value(&c.total)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    let lines: Vec<BomLine> = args[0].as_arr().iter().map(bom_line_from_value).collect();
    let costs: Vec<ItemCost> = args[2]
        .as_arr()
        .iter()
        .map(|v| ItemCost {
            item: v.get("item").as_str().to_string(),
            unit_cost: money_from_value(v.get("unitCost")),
        })
        .collect();
    let operations: Vec<RoutingOperation> = args[3]
        .as_arr()
        .iter()
        .map(|v| {
            if let Value::Float(f) = v.get("seconds") {
                panic!(
                    "seconds of an operation on \"{}\" must be a whole number, not negative, received {}",
                    v.get("item").as_str(),
                    f
                );
            }
            RoutingOperation {
                item: v.get("item").as_str().to_string(),
                seconds: v.get("seconds").as_i64(),
                labour_rate: money_from_value(v.get("labourRate")),
                overhead_rate: money_from_value(v.get("overheadRate")),
            }
        })
        .collect();
    cost_rollup_to_value(&bom_cost(&lines, args[1].as_str(), &costs, &operations, args[4].as_str(), args[5].as_str()))
}