Functional Weave
Code in TypeScript

inventory.pallet-fit

How many cartons fit per layer and per pallet (UK 1200x1000 or EUR 1200x800), within a height and weight limit.

1.0.0 · published 2026-10-03 by charlie · Anterra

Pinned by 17 tests, run in TypeScript, Python and Rust.

What it does

How many identical cartons fit on a pallet: per layer, how many layers, and in total, within a load height and an optional weight limit.

**Pallets** are data (`data/pallets.json`): the UK standard pallet, 1200 x 1000 mm, and the EUR pallet (EPAL 1), 1200 x 800 mm, both ISO 6780 sizes. Cartons may not overhang the deck.

For example

  • palletFit(length mm 600, width mm 400, height mm 300, weight grams 12,000, uk, 1,500, 1,000,000, true) → pallet length mm 1,200, pallet width mm 1,000, carton height mm 300, per layer 5, layers 5, per pallet 25, limited by height, load height mm 1,500, load weight grams 300,000 600x400 on a UK pallet: a plain grid gets 4 a layer, turning one row gets 5
  • palletFit(length mm 600, width mm 400, height mm 300, weight grams 12,000, euro, 1,500, 1,000,000, true) → pallet length mm 1,200, pallet width mm 800, carton height mm 300, per layer 4, layers 5, per pallet 20, limited by height, load height mm 1,500, load weight grams 240,000 600x400 on a EUR pallet is the classic 4 a layer
  • palletFit(length mm 400, width mm 300, height mm 250, weight grams 5,000, euro, 1,600, —, true) → pallet length mm 1,200, pallet width mm 800, carton height mm 250, per layer 8, layers 6, per pallet 48, limited by height, load height mm 1,500, load weight grams 240,000 400x300 on a EUR pallet: 8 a layer turned the other way, not 6

The function

The same function in TypeScript, Python and Rust, pinned by the same tests. Pick your language; the choice follows you around the registry.

export function palletFit(carton: Carton, pallet: PalletType, maxLoadHeightMm: number, maxLoadWeightGrams: number | null, upright: boolean): PalletFit
cartonCartonouter dimensions and gross weight of one carton
palletPalletTypeuk (1200 x 1000) or euro (1200 x 800)
maxLoadHeightMmintheight available for goods above the pallet deck
maxLoadWeightGramsint?weight the pallet may carry; null for no limit
uprightbooltrue keeps the carton's height vertical ("this way up"); false tries each side down
returnsPalletFit

The types it declares, generated into your project

export type PalletType = "uk" | "euro";

export type PalletLimit = "footprint" | "height" | "weight";

/** One carton, as shipped. */
export interface Carton {
  readonly lengthMm: number;
  readonly widthMm: number;
  readonly heightMm: number;
  readonly weightGrams: number;
}

/** The best stack found, and what stopped it growing. */
export interface PalletFit {
  readonly palletLengthMm: number;
  readonly palletWidthMm: number;
  /** the carton dimension standing vertical in the chosen stack */
  readonly cartonHeightMm: number;
  readonly perLayer: number;
  /** full layers the height allows */
  readonly layers: number;
  readonly perPallet: number;
  /** footprint: a carton does not fit the deck; height or weight otherwise */
  readonly limitedBy: PalletLimit;
  /** height of the goods as stacked, the top layer counted whole */
  readonly loadHeightMm: number;
  readonly loadWeightGrams: number;
}

Your code names it in one line, in the file that uses it

import { palletFit } from "#fune/inventory.pallet-fit@^1";
impl/typescript.ts · 70 lines · open · 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;
}

Install

fune build

With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and nothing else, pins them in fune.lock, downloads only the TypeScript package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. Or pin a range in fune.project and build in one step:

fune add inventory.pallet-fit
Download for TypeScript inventory.pallet-fit-1.0.0-typescript.fune · 15,914 bytes sha256 870b33d979b5c2bce6ce13aaf46d018647aca54897ba8374aec43922ca9b9a66

The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./inventory.pallet-fit-1.0.0-typescript.fune, or fetch it from a terminal with fune pull inventory.pallet-fit@1.0.0:typescript.

The whole function, every language, is one file too: inventory.pallet-fit-1.0.0.fune, 24,143 bytes, sha256 32db121a67b47e361aaa3a1440444154e6a0fb75cd286a87e29836986c11e78c. It installs into a project of any language.

Customise it in your app

The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.

before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.

// fune: before inventory.pallet-fit

after — your function gets the result and the arguments, and returns the final result.

// fune: after inventory.pallet-fit

replace — it requires no other capability, so there is no dependency to replace.

step — your function runs at a numbered point inside the function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show inventory.pallet-fit --steps.

// fune: step inventory.pallet-fit after <n|label>

Tests

A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.

CaseArgumentsExpected
600x400 on a UK pallet: a plain grid gets 4 a layer, turning one row gets 5 length mm 600, width mm 400, height mm 300, weight grams 12,000, uk, 1,500, 1,000,000, true → pallet length mm 1,200, pallet width mm 1,000, carton height mm 300, per layer 5, layers 5, per pallet 25, limited by height, load height mm 1,500, load weight grams 300,000
600x400 on a EUR pallet is the classic 4 a layer length mm 600, width mm 400, height mm 300, weight grams 12,000, euro, 1,500, 1,000,000, true → pallet length mm 1,200, pallet width mm 800, carton height mm 300, per layer 4, layers 5, per pallet 20, limited by height, load height mm 1,500, load weight grams 240,000
400x300 on a EUR pallet: 8 a layer turned the other way, not 6 length mm 400, width mm 300, height mm 250, weight grams 5,000, euro, 1,600, —, true → pallet length mm 1,200, pallet width mm 800, carton height mm 250, per layer 8, layers 6, per pallet 48, limited by height, load height mm 1,500, load weight grams 240,000
weight binds first: 25 cartons of 20 kg reach 500 kg in the fourth layer length mm 400, width mm 300, height mm 250, weight grams 20,000, euro, 1,600, 500,000, true → pallet length mm 1,200, pallet width mm 800, carton height mm 250, per layer 8, layers 6, per pallet 25, limited by weight, load height mm 1,000, load weight grams 500,000
upright: 500x300x200 is 8 a layer, 7 layers in 1500 mm, 56 cartons length mm 500, width mm 300, height mm 200, weight grams 1,000, uk, 1,500, —, true → pallet length mm 1,200, pallet width mm 1,000, carton height mm 200, per layer 8, layers 7, per pallet 56, limited by height, load height mm 1,400, load weight grams 56,000
laid on its side the same carton gets 60: 12 a layer, 5 layers of 300 mm length mm 500, width mm 300, height mm 200, weight grams 1,000, uk, 1,500, —, false → pallet length mm 1,200, pallet width mm 1,000, carton height mm 300, per layer 12, layers 5, per pallet 60, limited by height, load height mm 1,500, load weight grams 60,000
a carton longer than the deck does not fit at all length mm 1,300, width mm 500, height mm 500, weight grams 1,000, euro, 1,600, —, true → pallet length mm 1,200, pallet width mm 800, carton height mm 500, per layer 0, layers 3, per pallet 0, limited by footprint, load height mm 0, load weight grams 0
a carton taller than the load height makes no layers length mm 400, width mm 300, height mm 1,800, weight grams 1,000, euro, 1,600, —, true → pallet length mm 1,200, pallet width mm 800, carton height mm 1,800, per layer 8, layers 0, per pallet 0, limited by height, load height mm 0, load weight grams 0
a weight limit below one carton loads nothing length mm 400, width mm 300, height mm 250, weight grams 20,000, euro, 1,600, 10,000, true → pallet length mm 1,200, pallet width mm 800, carton height mm 250, per layer 8, layers 6, per pallet 0, limited by weight, load height mm 0, load weight grams 0
weightless cartons are never weight-limited length mm 400, width mm 300, height mm 250, weight grams 0, euro, 1,600, 1, true → pallet length mm 1,200, pallet width mm 800, carton height mm 250, per layer 8, layers 6, per pallet 48, limited by height, load height mm 1,500, load weight grams 0
Show the other 7 tests
CaseArgumentsExpected
a perfect fit: 1200x1000 fills a UK layer with one carton length mm 1,200, width mm 1,000, height mm 500, weight grams 30,000, uk, 1,000, —, true → pallet length mm 1,200, pallet width mm 1,000, carton height mm 500, per layer 1, layers 2, per pallet 2, limited by height, load height mm 1,000, load weight grams 60,000
a zero dimension is an error length mm 0, width mm 300, height mm 250, weight grams 1,000, euro, 1,600, —, true → error: lengthMm must be a whole number greater than zero
a fractional dimension is an error length mm 400, width mm 300.5, height mm 250, weight grams 1,000, euro, 1,600, —, true → error: widthMm must be a whole number greater than zero
a negative weight is an error length mm 400, width mm 300, height mm 250, weight grams -1, euro, 1,600, —, true → error: weightGrams must be a whole number, not negative
a zero load height is an error length mm 400, width mm 300, height mm 250, weight grams 1,000, euro, 0, —, true → error: maxLoadHeightMm must be a whole number greater than zero
a negative weight limit is an error length mm 400, width mm 300, height mm 250, weight grams 1,000, euro, 1,600, -5, true → error: maxLoadWeightGrams must be a whole number, not negative
an unknown pallet is an error length mm 400, width mm 300, height mm 250, weight grams 1,000, us, 1,600, —, true → error: unknown pallet "us": expected uk or euro

More from the author

**Layer pattern.** Cartons in a layer may be turned 90 degrees. The layer is the best *two-block* pattern: the deck is split once, across its length or its width, and each block is filled in a grid with the carton one way round. That includes the two plain grids, and it is where mixed patterns win: a 600 x 400 carton gets 4 a layer on a UK pallet in a plain grid, and 5 with a row turned (one of the vectors). It does not search the pinwheel and multi-block patterns of dedicated palletisation software, so on some awkward sizes it is a cautious answer, never an impossible one.

**Upright or not.** With `upright` true the carton's height stays vertical, as "this way up" cartons must. With false, each of the three faces is tried down (height, then width, then length vertical) and the first stack holding the most cartons wins.

**Limits.** `layers` is how many whole layers fit in `maxLoadHeightMm` (the height above the deck: take the pallet's own height, about 144 mm for a EUR pallet, off the vehicle or racking limit first). The pallet then holds `perLayer x layers` cartons, fewer if `maxLoadWeightGrams` is reached first; a part layer on top counts whole in `loadHeightMm`. `limitedBy` says which limit bound: `footprint` when a carton does not fit the deck at all, else `height` or `weight`. Carrier and racking limits vary (a typical UK pallet network allows about 1000 kg and 2200 mm overall), so they are arguments, not data.

Dimensions and weights are whole millimetres and grams; dimensions must be positive, weights not negative.

Sources: ISO 6780:2003 *Flat pallets for intercontinental materials handling - Principal dimensions and tolerances*; EPAL, "EPAL Euro pallet (EPAL 1)" (https://www.epal-pallets.org/eu-en/load-carriers/epal-euro-pallet), 800 x 1200 x 144 mm.

Files

PathBytes
README.md2,139
data/pallets.json217
impl/python.py3,174
impl/rust.rs4,738
impl/typescript.ts3,187
vectors.json5,513