Functional Weave
Code in Python

construction.timber-length@1.0.0

impl/typescript.ts

2,069 bytes · the TypeScript implementation · view raw

import { type CutBar, type CuttingList } from "./construction_timber_length_types.ts";

/**
 * Plan which pieces to cut from which stock length, first-fit decreasing.
 *
 * A bar holds pieces p1..pn when their lengths plus a kerf between each pair
 * fit: sum + kerf * (n - 1) <= stockLength. The last piece may end exactly at
 * the end of the bar, which needs no final cut. Ignoring kerf is the usual
 * mistake: three 1200 mm pieces do not come out of one 3600 mm length.
 */
export function timberCuttingList(cuts: readonly number[], stockLength: number, kerf: number): CuttingList {
  if (!Number.isInteger(stockLength) || stockLength <= 0) {
    throw new RangeError(`stockLength must be a whole number of millimetres greater than 0, received ${stockLength}`);
  }
  if (!Number.isInteger(kerf) || kerf < 0) {
    throw new RangeError(`kerf must be a whole number of millimetres, 0 or more, received ${kerf}`);
  }
  cuts.forEach((cut, i) => {
    if (!Number.isInteger(cut) || cut <= 0) {
      throw new RangeError(`cut ${i + 1} must be a whole number of millimetres greater than 0, received ${cut}`);
    }
    if (cut > stockLength) {
      throw new RangeError(`cut ${i + 1} (${cut} mm) is longer than the stock length (${stockLength} mm)`);
    }
  });

  // Equal lengths are interchangeable, so a plain descending sort is deterministic.
  const sorted = [...cuts].sort((a, b) => b - a);
  const pieces: number[][] = [];
  const used: number[] = [];
  for (const cut of sorted) {
    let placed = false;
    for (let b = 0; b < pieces.length; b++) {
      if (used[b] + kerf + cut <= stockLength) {
        pieces[b].push(cut);
        used[b] += kerf + cut;
        placed = true;
        break;
      }
    }
    if (!placed) {
      pieces.push([cut]);
      used.push(cut);
    }
  }

  let total = 0;
  for (const cut of cuts) total += cut;
  const bars: CutBar[] = pieces.map((p, b) => ({
    cuts: p,
    offcut: Math.max(0, stockLength - used[b] - kerf),
  }));
  return { bars, barCount: bars.length, waste: bars.length * stockLength - total };
}