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(),
)
}