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
pallet_fit(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 5pallet_fit(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 layerpallet_fit(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.
pub fn pallet_fit(carton: &Carton, pallet: &str, max_load_height_mm: i64, max_load_weight_grams: Option<i64>, upright: bool) -> PalletFit
| carton | Carton | outer dimensions and gross weight of one carton |
| pallet | PalletType | uk (1200 x 1000) or euro (1200 x 800) |
| max_load_height_mm | int | height available for goods above the pallet deck |
| max_load_weight_grams | int? | weight the pallet may carry; null for no limit |
| upright | bool | true keeps the carton's height vertical ("this way up"); false tries each side down |
| returns | PalletFit |
The types it declares, generated into your project
// PalletType is a string in Rust, one of: "uk", "euro".
// Parameters take it as &str and results hold it as String.
// PalletLimit is a string in Rust, one of: "footprint", "height", "weight".
// Parameters take it as &str and results hold it as String.
/// One carton, as shipped.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Carton {
pub length_mm: i64,
pub width_mm: i64,
pub height_mm: i64,
pub weight_grams: i64,
}
/// The best stack found, and what stopped it growing.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PalletFit {
pub pallet_length_mm: i64,
pub pallet_width_mm: i64,
/// the carton dimension standing vertical in the chosen stack
pub carton_height_mm: i64,
pub per_layer: i64,
/// full layers the height allows
pub layers: i64,
pub per_pallet: i64,
/// footprint: a carton does not fit the deck; height or weight otherwise
pub limited_by: String,
/// height of the goods as stacked, the top layer counted whole
pub load_height_mm: i64,
pub load_weight_grams: i64,
}
Your code names it in one line, in the file that uses it
fune!(inventory.pallet-fit@^1); // then call pallet_fit(…)
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
use super::funejson::Value; ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::inventory_pallet_fit_data::PALLETS; ← this capability’s own data, compiled from data/pallets.json into the same file by fune build
/// 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.
fn cartons_per_layer(length: i64, width: i64, a: i64, b: i64) -> i64 {
let mut best = 0;
let mut i = 0;
while i * a <= length {
best = best.max(i * (width / b) + ((length - i * a) / b) * (width / a));
i += 1;
}
let mut j = 0;
while j * b <= width {
best = best.max(j * (length / a) + ((width - j * b) / a) * (length / b));
j += 1;
}
best
}
fn positive(name: &str, value: i64) {
if value <= 0 {
panic!("{} must be a whole number greater than zero, received {}", name, value);
}
}
/// The best stack of identical cartons on a UK or EUR pallet within the limits.
///
/// # Panics
/// Panics on a dimension that is not positive, a negative weight or weight
/// limit, or an unknown pallet.
pub fn pallet_fit(carton: &Carton, pallet: &str, max_load_height_mm: i64, max_load_weight_grams: Option<i64>, upright: bool) -> PalletFit {
positive("lengthMm", carton.length_mm);
positive("widthMm", carton.width_mm);
positive("heightMm", carton.height_mm);
if carton.weight_grams < 0 {
panic!("weightGrams must be a whole number, not negative, received {}", carton.weight_grams);
}
positive("maxLoadHeightMm", max_load_height_mm);
if let Some(mw) = max_load_weight_grams {
if mw < 0 {
panic!("maxLoadWeightGrams must be a whole number, not negative, received {}", mw);
}
}
let deck = match PALLETS.iter().find(|p| p.code == pallet) {
Some(d) => d,
None => panic!("unknown pallet \"{}\": expected uk or euro", pallet),
};
let (l, w, h) = (carton.length_mm, carton.width_mm, carton.height_mm);
let choices: Vec<(i64, i64, i64)> = if upright {
vec![(h, l, w)]
} else {
vec![(h, l, w), (w, l, h), (l, w, h)]
};
let mut best: Option<PalletFit> = None;
for (vertical, a, b) in choices {
let per_layer = cartons_per_layer(deck.length_mm, deck.width_mm, a, b);
let layers = max_load_height_mm / vertical;
let mut per_pallet = per_layer * layers;
let mut limited_by = if per_layer == 0 { "footprint" } else { "height" };
if let Some(mw) = max_load_weight_grams {
if per_layer > 0 && carton.weight_grams > 0 {
let by_weight = mw / carton.weight_grams;
if by_weight < per_pallet {
per_pallet = by_weight;
limited_by = "weight";
}
}
}
let fit = PalletFit {
pallet_length_mm: deck.length_mm,
pallet_width_mm: deck.width_mm,
carton_height_mm: vertical,
per_layer,
layers,
per_pallet,
limited_by: limited_by.to_string(),
load_height_mm: if per_layer == 0 { 0 } else { (per_pallet + per_layer - 1) / per_layer * vertical },
load_weight_grams: per_pallet * carton.weight_grams,
};
if best.as_ref().map_or(true, |b| fit.per_pallet > b.per_pallet) {
best = Some(fit);
}
}
best.unwrap()
}
pub fn carton_from_value(v: &Value) -> Carton {
Carton {
length_mm: v.get("lengthMm").as_i64(),
width_mm: v.get("widthMm").as_i64(),
height_mm: v.get("heightMm").as_i64(),
weight_grams: v.get("weightGrams").as_i64(),
}
}
pub fn pallet_fit_to_value(fit: &PalletFit) -> Value {
Value::obj(vec![
("palletLengthMm", Value::Int(fit.pallet_length_mm)),
("palletWidthMm", Value::Int(fit.pallet_width_mm)),
("cartonHeightMm", Value::Int(fit.carton_height_mm)),
("perLayer", Value::Int(fit.per_layer)),
("layers", Value::Int(fit.layers)),
("perPallet", Value::Int(fit.per_pallet)),
("limitedBy", Value::str(&fit.limited_by)),
("loadHeightMm", Value::Int(fit.load_height_mm)),
("loadWeightGrams", Value::Int(fit.load_weight_grams)),
])
}
pub fn fune_vector(args: &[Value]) -> Value {
for key in ["lengthMm", "widthMm", "heightMm"] {
if let Value::Float(f) = args[0].get(key) {
panic!("{} must be a whole number greater than zero, received {}", key, f);
}
}
let max_weight = if args[3].is_null() { None } else { Some(args[3].as_i64()) };
pallet_fit_to_value(&pallet_fit(
&carton_from_value(&args[0]),
args[1].as_str(),
args[2].as_i64(),
max_weight,
args[4].as_bool(),
))
}Install
fune build
With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and nothing else, pins them in fune.lock, downloads only the Rust 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. A crate’s build.rs runs it before every compile. Or pin a range in fune.project and build in one step:
fune add inventory.pallet-fit
The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./inventory.pallet-fit-1.0.0-rust.fune, or fetch it from a terminal with fune pull inventory.pallet-fit@1.0.0:rust.
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.
| Case | Arguments | Expected | |
|---|---|---|---|
| 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
| Case | Arguments | Expected | |
|---|---|---|---|
| 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
| Path | Bytes |
|---|---|
| README.md | 2,139 |
| data/pallets.json | 217 |
| impl/python.py | 3,174 |
| impl/rust.rs | 4,738 |
| impl/typescript.ts | 3,187 |
| vectors.json | 5,513 |