Functional Weave
Code in TypeScript

manufacturing.bom-explode@1.0.0

impl/rust.rs

5,359 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_rational::{
    add_rational, multiply_rational, rational, rational_from_value, rational_to_integer, rational_to_value, Rational,
};

const MAX_SAFE: i64 = (1i64 << 53) - 1;

struct Entry {
    name: String,
    level: i64,
    quantity: Rational,
}

struct Bom<'a> {
    lines: Vec<(&'a BomLine, Rational)>,
}

impl<'a> Bom<'a> {
    fn has_children(&self, item: &str) -> bool {
        self.lines.iter().any(|(l, _)| l.parent == item)
    }

    fn walk(&self, parent: &str, need: &Rational, depth: i64, path: &mut Vec<String>, entries: &mut Vec<Entry>) {
        for (line, factor) in self.lines.iter().filter(|(l, _)| l.parent == parent) {
            if path.iter().any(|p| *p == line.component) {
                let mut cycle = path.clone();
                cycle.push(line.component.clone());
                panic!("bill of materials has a cycle: {}", cycle.join(" -> "));
            }
            let quantity = multiply_rational(need, factor);
            match entries.iter_mut().find(|e| e.name == line.component) {
                None => entries.push(Entry {
                    name: line.component.clone(),
                    level: depth,
                    quantity,
                }),
                Some(e) => {
                    e.level = e.level.max(depth);
                    e.quantity = add_rational(&e.quantity, &quantity);
                }
            }
            if self.has_children(&line.component) {
                path.push(line.component.clone());
                self.walk(&line.component, &quantity, depth + 1, path, entries);
                path.pop();
            }
        }
    }
}

/// Total quantity of every item under `item`, summed over every place it is
/// used, in exact fractions. Walks the BOM depth first, carrying the path so a
/// loop is reported by name instead of recursing for ever.
///
/// # Panics
/// Panics on a negative build quantity, a quantity that is not positive,
/// negative scrap, a product with no lines, a cycle, or a total beyond 2^53 - 1.
pub fn explode_bom(lines: &[BomLine], item: &str, build_quantity: i64) -> Vec<BomRequirement> {
    if !(0..=MAX_SAFE).contains(&build_quantity) {
        panic!("buildQuantity must be a whole number, not negative, received {}", build_quantity);
    }
    let mut bom = Bom { lines: Vec::new() };
    for line in lines {
        let q = rational(line.quantity.numerator, line.quantity.denominator);
        if q.numerator <= 0 {
            panic!("quantity of \"{}\" in \"{}\" must be greater than zero", line.component, line.parent);
        }
        if !(0..=MAX_SAFE).contains(&line.scrap_basis_points) {
            panic!(
                "scrapBasisPoints of \"{}\" in \"{}\" must be a whole number, not negative, received {}",
                line.component, line.parent, line.scrap_basis_points
            );
        }
        let factor = multiply_rational(&q, &rational(10000 + line.scrap_basis_points, 10000));
        bom.lines.push((line, factor));
    }
    if !bom.has_children(item) {
        panic!("\"{}\" has no bill of materials", item);
    }
    let mut entries: Vec<Entry> = Vec::new();
    let mut path = vec![item.to_string()];
    bom.walk(item, &rational(build_quantity, 1), 1, &mut path, &mut entries);
    // A stable sort on level keeps first-appearance order within a level.
    let mut order: Vec<usize> = (0..entries.len()).collect();
    order.sort_by_key(|&i| entries[i].level);
    order
        .into_iter()
        .map(|i| {
            let e = &entries[i];
            BomRequirement {
                item: e.name.clone(),
                level: e.level,
                quantity: e.quantity,
                whole_units: rational_to_integer(&e.quantity, "up"),
                leaf: !bom.has_children(&e.name),
            }
        })
        .collect()
}

pub fn bom_line_from_value(v: &Value) -> BomLine {
    BomLine {
        parent: v.get("parent").as_str().to_string(),
        component: v.get("component").as_str().to_string(),
        quantity: rational_from_value(v.get("quantity")),
        scrap_basis_points: v.get("scrapBasisPoints").as_i64(),
    }
}

pub fn bom_requirement_to_value(r: &BomRequirement) -> Value {
    Value::obj(vec![
        ("item", Value::str(&r.item)),
        ("level", Value::Int(r.level)),
        ("quantity", rational_to_value(&r.quantity)),
        ("wholeUnits", Value::Int(r.whole_units)),
        ("leaf", Value::Bool(r.leaf)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    if let Value::Float(f) = &args[2] {
        panic!("buildQuantity must be a whole number, not negative, received {}", f);
    }
    let lines: Vec<BomLine> = args[0]
        .as_arr()
        .iter()
        .map(|v| {
            if let Value::Float(f) = v.get("scrapBasisPoints") {
                panic!(
                    "scrapBasisPoints of \"{}\" in \"{}\" must be a whole number, not negative, received {}",
                    v.get("component").as_str(),
                    v.get("parent").as_str(),
                    f
                );
            }
            bom_line_from_value(v)
        })
        .collect();
    Value::Arr(
        explode_bom(&lines, args[1].as_str(), args[2].as_i64())
            .iter()
            .map(bom_requirement_to_value)
            .collect(),
    )
}