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 };
}