impl/typescript.ts
3,187 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 { PALLETS } from "./inventory_pallet_fit_data.ts"; ← this capability’s own data, compiled from data/pallets.json into the same file by fune build
import { type Carton, type PalletFit, type PalletLimit, type PalletType } from "./inventory_pallet_fit_types.ts";
/**
* Cartons per layer on a `length` x `width` deck, base `a` x `b`, as the best
* two-block pattern: one split across the length or the width, each block a
* grid with the carton one way round.
*/
function cartonsPerLayer(length: number, width: number, a: number, b: number): number {
let best = 0;
for (let i = 0; i * a <= length; i++) {
best = Math.max(best, i * Math.floor(width / b) + Math.floor((length - i * a) / b) * Math.floor(width / a));
}
for (let j = 0; j * b <= width; j++) {
best = Math.max(best, j * Math.floor(length / a) + Math.floor((width - j * b) / a) * Math.floor(length / b));
}
return best;
}
function positive(name: string, value: number): void {
if (!Number.isInteger(value) || value <= 0) {
throw new RangeError(`${name} must be a whole number greater than zero, received ${value}`);
}
}
/** The best stack of identical cartons on a UK or EUR pallet within the limits. */
export function palletFit(carton: Carton, pallet: PalletType, maxLoadHeightMm: number, maxLoadWeightGrams: number | null, upright: boolean): PalletFit {
positive("lengthMm", carton.lengthMm);
positive("widthMm", carton.widthMm);
positive("heightMm", carton.heightMm);
if (!Number.isInteger(carton.weightGrams) || carton.weightGrams < 0) {
throw new RangeError(`weightGrams must be a whole number, not negative, received ${carton.weightGrams}`);
}
positive("maxLoadHeightMm", maxLoadHeightMm);
if (maxLoadWeightGrams !== null && (!Number.isInteger(maxLoadWeightGrams) || maxLoadWeightGrams < 0)) {
throw new RangeError(`maxLoadWeightGrams must be a whole number, not negative, received ${maxLoadWeightGrams}`);
}
const deck = PALLETS.find((p) => p.code === pallet);
if (!deck) throw new RangeError(`unknown pallet "${pallet}": expected uk or euro`);
const { lengthMm: l, widthMm: w, heightMm: h } = carton;
const choices: [number, number, number][] = upright ? [[h, l, w]] : [[h, l, w], [w, l, h], [l, w, h]];
let best: PalletFit | null = null;
for (const [vertical, a, b] of choices) {
const perLayer = cartonsPerLayer(deck.lengthMm, deck.widthMm, a, b);
const layers = Math.floor(maxLoadHeightMm / vertical);
let perPallet = perLayer * layers;
let limitedBy: PalletLimit = perLayer === 0 ? "footprint" : "height";
if (perLayer > 0 && maxLoadWeightGrams !== null && carton.weightGrams > 0) {
const byWeight = Math.floor(maxLoadWeightGrams / carton.weightGrams);
if (byWeight < perPallet) {
perPallet = byWeight;
limitedBy = "weight";
}
}
const fit: PalletFit = {
palletLengthMm: deck.lengthMm,
palletWidthMm: deck.widthMm,
cartonHeightMm: vertical,
perLayer,
layers,
perPallet,
limitedBy,
loadHeightMm: perLayer === 0 ? 0 : Math.ceil(perPallet / perLayer) * vertical,
loadWeightGrams: perPallet * carton.weightGrams,
};
if (best === null || fit.perPallet > best.perPallet) best = fit;
}
return best as PalletFit;
}