use super::funejson::Value; 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, entries: &mut Vec) { 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 { 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 = 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 = (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 = 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(), ) }