Functional Weave
Code in Python

manufacturing.bom-explode@1.0.0

impl/typescript.ts

2,910 bytes · the TypeScript 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.

import { type Rational, addRational, multiplyRational, rational, rationalToInteger } from "./math_rational.ts";  ← from math.rational ^1.0.0 · built alongside by fune
import { type BomLine, type BomRequirement } from "./manufacturing_bom_explode_types.ts";

interface Entry {
  index: number;
  level: number;
  quantity: Rational;
}

/**
 * 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.
 */
export function explodeBom(lines: readonly BomLine[], item: string, buildQuantity: number): readonly BomRequirement[] {
  if (!Number.isSafeInteger(buildQuantity) || buildQuantity < 0) {
    throw new RangeError(`buildQuantity must be a whole number, not negative, received ${buildQuantity}`);
  }
  const children = new Map<string, { line: BomLine; factor: Rational }[]>();
  for (const line of lines) {
    const q = rational(line.quantity.numerator, line.quantity.denominator);
    if (q.numerator <= 0) {
      throw new RangeError(`quantity of "${line.component}" in "${line.parent}" must be greater than zero`);
    }
    if (!Number.isSafeInteger(line.scrapBasisPoints) || line.scrapBasisPoints < 0) {
      throw new RangeError(
        `scrapBasisPoints of "${line.component}" in "${line.parent}" must be a whole number, not negative, received ${line.scrapBasisPoints}`,
      );
    }
    const factor = multiplyRational(q, rational(10000 + line.scrapBasisPoints, 10000));
    const list = children.get(line.parent) ?? [];
    list.push({ line, factor });
    children.set(line.parent, list);
  }
  if (!children.has(item)) {
    throw new RangeError(`"${item}" has no bill of materials`);
  }
  const entries = new Map<string, Entry>();
  const walk = (parent: string, need: Rational, depth: number, path: string[]): void => {
    for (const { line, factor } of children.get(parent) ?? []) {
      if (path.includes(line.component)) {
        throw new RangeError(`bill of materials has a cycle: ${[...path, line.component].join(" -> ")}`);
      }
      const quantity = multiplyRational(need, factor);
      const entry = entries.get(line.component);
      if (entry === undefined) {
        entries.set(line.component, { index: entries.size, level: depth, quantity });
      } else {
        entry.level = Math.max(entry.level, depth);
        entry.quantity = addRational(entry.quantity, quantity);
      }
      if (children.has(line.component)) {
        walk(line.component, quantity, depth + 1, [...path, line.component]);
      }
    }
  };
  walk(item, rational(buildQuantity, 1), 1, [item]);
  return [...entries.entries()]
    .sort(([, a], [, b]) => a.level - b.level || a.index - b.index)
    .map(([name, e]) => ({
      item: name,
      level: e.level,
      quantity: e.quantity,
      wholeUnits: rationalToInteger(e.quantity, "up"),
      leaf: !children.has(name),
    }));
}