impl/typescript.ts
3,379 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 { CONTAINERS } from "./logistics_container_fit_data.ts"; ← this capability’s own data, compiled from data/containers.json into the same file by fune build
import { type ContainerFit } from "./logistics_container_fit_types.ts";
/**
* How many identical cartons fit in a container or trailer: the best block
* stow over the allowed orientations that pass the door, capped by payload.
*/
export function containerFit(
container: string,
lengthMm: number,
widthMm: number,
heightMm: number,
cartonGrams: number,
keepUpright: boolean,
maxPayloadKg: number | null,
): ContainerFit {
for (const d of [lengthMm, widthMm, heightMm]) {
if (!Number.isInteger(d) || d < 1) throw new RangeError(`carton dimensions must be 1 mm or more, received ${d}`);
}
if (!Number.isInteger(cartonGrams) || cartonGrams < 1) {
throw new RangeError(`cartonGrams must be 1 or more, received ${cartonGrams}`);
}
const spec = CONTAINERS.find((c) => c.code === container);
if (spec === undefined) {
throw new RangeError(`unknown container "${container}": expected ${CONTAINERS.map((c) => c.code).join(", ")}`);
}
let payloadKg = spec.maxPayloadKg;
if (maxPayloadKg !== null) {
if (!Number.isInteger(maxPayloadKg) || maxPayloadKg < 1) {
throw new RangeError(`maxPayloadKg must be 1 or more, received ${maxPayloadKg}`);
}
if (maxPayloadKg > spec.maxPayloadKg) {
throw new RangeError(`maxPayloadKg must not exceed ${spec.maxPayloadKg} kg for ${container}, received ${maxPayloadKg}`);
}
payloadKg = maxPayloadKg;
}
const l = lengthMm;
const w = widthMm;
const h = heightMm;
// Upright first (as given, then turned on the floor), then the four tipped ones.
const orientations: ReadonlyArray<readonly [number, number, number]> = keepUpright
? [[l, w, h], [w, l, h]]
: [[l, w, h], [w, l, h], [l, h, w], [h, l, w], [w, h, l], [h, w, l]];
let best: { along: number; across: number; layers: number; placed: readonly [number, number, number] } | null = null;
let bestCount = 0;
for (const o of orientations) {
const [a, b, c] = o;
if (a > spec.lengthMm || b > spec.widthMm || c > spec.heightMm) continue;
// End-loaded containers: the carton's cross-section has to pass the door.
if (spec.doorWidthMm !== null && b > spec.doorWidthMm) continue;
if (spec.doorHeightMm !== null && c > spec.doorHeightMm) continue;
const along = Math.floor(spec.lengthMm / a);
const across = Math.floor(spec.widthMm / b);
const layers = Math.floor(spec.heightMm / c);
const count = along * across * layers;
if (best === null || count > bestCount) {
best = { along, across, layers, placed: o };
bestCount = count;
}
}
if (best === null) throw new RangeError(`carton does not fit in ${container} in any allowed orientation`);
const weightLimit = Math.floor((payloadKg * 1000) / cartonGrams);
const cartons = Math.min(bestCount, weightLimit);
const space = spec.lengthMm * spec.widthMm * spec.heightMm;
return {
container,
cartons,
spaceLimit: bestCount,
weightLimit,
limitedBy: weightLimit < bestCount ? "weight" : "space",
alongLength: best.along,
acrossWidth: best.across,
layers: best.layers,
placedLengthMm: best.placed[0],
placedWidthMm: best.placed[1],
placedHeightMm: best.placed[2],
cargoGrams: cartons * cartonGrams,
volumeUsedBasisPoints: Math.floor((cartons * l * w * h * 10000) / space),
};
}