Functional Weave
Code in Rust

logistics.container-fit

How many identical cartons fit in a 20ft, 40ft or 40ft high-cube container or a 13.6 m trailer, by space and by weight.

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

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

What it does

How many identical cartons fit in a shipping container or a road trailer: the best of the carton's orientations, in a simple block stow (every carton the same way round, in rows, columns and layers), then capped by the payload.

## Orientations

For example

  • container_fit(20gp, 600, 400, 400, 12,000, false, —) → container 20gp, cartons 225, space limit 225, weight limit 2,516, limited by space, along length 9, across width 5, layers 5, placed length mm 600, placed width mm 400, placed hei… 600 x 400 x 400 cartons in a 20ft box, space limited
  • container_fit(40hc, 580, 380, 300, 8,000, true, —) → container 40hc, cartons 1,116, space limit 1,116, weight limit 3,581, limited by space, along length 31, across width 4, layers 9, placed length mm 380, placed width mm 580, place… turned on the floor beats as given: 1116, not 1080
  • container_fit(40hc, 580, 380, 300, 8,000, false, —) → container 40hc, cartons 1,120, space limit 1,120, weight limit 3,581, limited by space, along length 40, across width 4, layers 7, placed length mm 300, placed width mm 580, place… allowed to tip, a tipped orientation gives 1120

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 container_fit(container: &str, length_mm: i64, width_mm: i64, height_mm: i64, carton_grams: i64, keep_upright: bool, max_payload_kg: Option<i64>) -> ContainerFit
containerstring20gp, 40gp, 40hc or trailer-13.6
length_mmintthe carton's outside dimensions, 1 or more; heightMm is its "this way up" axis
width_mmint
height_mmint
carton_gramsintgross weight of one packed carton, 1 or more
keep_uprightbooltrue for "this way up" cartons: only turn them on the floor, never tip them
max_payload_kgint?a lower limit than the container's rated payload, e.g. a road weight limit; null for the rating
returnsContainerFit

The types it declares, generated into your project

// FitLimit is a string in Rust, one of: "space", "weight".
// Parameters take it as &str and results hold it as String.

/// The load, how it is stacked, and what stopped it growing.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ContainerFit {
    pub container: String,
    /// how many to load: the smaller of the two limits
    pub cartons: i64,
    /// how many fit by dimensions alone
    pub space_limit: i64,
    /// how many the payload allows
    pub weight_limit: i64,
    /// weight only when it is strictly the smaller
    pub limited_by: String,
    /// cartons in a row along the container
    pub along_length: i64,
    /// rows side by side
    pub across_width: i64,
    /// tiers stacked
    pub layers: i64,
    /// the carton dimension lying along the container's length
    pub placed_length_mm: i64,
    /// ...across its width
    pub placed_width_mm: i64,
    /// ...upright
    pub placed_height_mm: i64,
    /// cartons x cartonGrams
    pub cargo_grams: i64,
    /// carton volume loaded over the space's volume, rounded down; 10000 = full
    pub volume_used_basis_points: i64,
}

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

fune!(logistics.container-fit@^1);  // then call container_fit(…)
impl/rust.rs · 137 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.

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::logistics_container_fit_data::CONTAINERS;  ← this capability’s own data, compiled from data/containers.json into the same file by fune build

/// 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.
///
/// # Panics
/// Panics on a dimension or weight below 1, an unknown container, a payload
/// limit above the container's rating, or a carton no orientation fits.
pub fn container_fit(
    container: &str,
    length_mm: i64,
    width_mm: i64,
    height_mm: i64,
    carton_grams: i64,
    keep_upright: bool,
    max_payload_kg: Option<i64>,
) -> ContainerFit {
    for d in [length_mm, width_mm, height_mm] {
        if d < 1 {
            panic!("carton dimensions must be 1 mm or more, received {}", d);
        }
    }
    if carton_grams < 1 {
        panic!("cartonGrams must be 1 or more, received {}", carton_grams);
    }
    let spec = match CONTAINERS.iter().find(|c| c.code == container) {
        Some(s) => s,
        None => {
            let codes: Vec<&str> = CONTAINERS.iter().map(|c| c.code).collect();
            panic!("unknown container \"{}\": expected {}", container, codes.join(", "));
        }
    };
    let mut payload_kg = spec.max_payload_kg;
    if let Some(limit) = max_payload_kg {
        if limit < 1 {
            panic!("maxPayloadKg must be 1 or more, received {}", limit);
        }
        if limit > spec.max_payload_kg {
            panic!(
                "maxPayloadKg must not exceed {} kg for {}, received {}",
                spec.max_payload_kg, container, limit
            );
        }
        payload_kg = limit;
    }

    let (l, w, h) = (length_mm, width_mm, height_mm);
    // Upright first (as given, then turned on the floor), then the four tipped ones.
    let orientations: Vec<(i64, i64, i64)> = if keep_upright {
        vec![(l, w, h), (w, l, h)]
    } else {
        vec![(l, w, h), (w, l, h), (l, h, w), (h, l, w), (w, h, l), (h, w, l)]
    };

    let mut best: Option<(i64, i64, i64, (i64, i64, i64))> = None;
    let mut best_count = 0;
    for o in orientations {
        let (a, b, c) = o;
        if a > spec.length_mm || b > spec.width_mm || c > spec.height_mm {
            continue;
        }
        // End-loaded containers: the carton's cross-section has to pass the door.
        if let Some(door) = spec.door_width_mm {
            if b > door {
                continue;
            }
        }
        if let Some(door) = spec.door_height_mm {
            if c > door {
                continue;
            }
        }
        let along = spec.length_mm / a;
        let across = spec.width_mm / b;
        let layers = spec.height_mm / c;
        let count = along * across * layers;
        if best.is_none() || count > best_count {
            best = Some((along, across, layers, o));
            best_count = count;
        }
    }
    let (along, across, layers, placed) = match best {
        Some(b) => b,
        None => panic!("carton does not fit in {} in any allowed orientation", container),
    };

    let weight_limit = payload_kg * 1000 / carton_grams;
    let cartons = best_count.min(weight_limit);
    let space = spec.length_mm * spec.width_mm * spec.height_mm;
    ContainerFit {
        container: container.to_string(),
        cartons,
        space_limit: best_count,
        weight_limit,
        limited_by: if weight_limit < best_count { "weight" } else { "space" }.to_string(),
        along_length: along,
        across_width: across,
        layers,
        placed_length_mm: placed.0,
        placed_width_mm: placed.1,
        placed_height_mm: placed.2,
        cargo_grams: cartons * carton_grams,
        volume_used_basis_points: cartons * l * w * h * 10000 / space,
    }
}

pub fn container_fit_to_value(f: &ContainerFit) -> Value {
    Value::obj(vec![
        ("container", Value::str(&f.container)),
        ("cartons", Value::Int(f.cartons)),
        ("spaceLimit", Value::Int(f.space_limit)),
        ("weightLimit", Value::Int(f.weight_limit)),
        ("limitedBy", Value::str(&f.limited_by)),
        ("alongLength", Value::Int(f.along_length)),
        ("acrossWidth", Value::Int(f.across_width)),
        ("layers", Value::Int(f.layers)),
        ("placedLengthMm", Value::Int(f.placed_length_mm)),
        ("placedWidthMm", Value::Int(f.placed_width_mm)),
        ("placedHeightMm", Value::Int(f.placed_height_mm)),
        ("cargoGrams", Value::Int(f.cargo_grams)),
        ("volumeUsedBasisPoints", Value::Int(f.volume_used_basis_points)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    let limit = if args[6].is_null() { None } else { Some(args[6].as_i64()) };
    container_fit_to_value(&container_fit(
        args[0].as_str(),
        args[1].as_i64(),
        args[2].as_i64(),
        args[3].as_i64(),
        args[4].as_i64(),
        args[5].as_bool(),
        limit,
    ))
}

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 logistics.container-fit
Download for Rust logistics.container-fit-1.0.0-rust.fune · 20,372 bytes sha256 ee2a7717e97ec029a390ce5e32e06790488bcf1c1c14ab8a13839661dc335c99

The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./logistics.container-fit-1.0.0-rust.fune, or fetch it from a terminal with fune pull logistics.container-fit@1.0.0:rust.

The whole function, every language, is one file too: logistics.container-fit-1.0.0.fune, 27,549 bytes, sha256 0724243703a57d2a7546dcf573145a83d6aab928f399362008abab1ef93294b6. 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 logistics.container-fit

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

// fune: after logistics.container-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 logistics.container-fit --steps.

// fune: step logistics.container-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
600 x 400 x 400 cartons in a 20ft box, space limited 20gp, 600, 400, 400, 12,000, false, — → container 20gp, cartons 225, space limit 225, weight limit 2,516, limited by space, along length 9, across width 5, layers 5, placed length mm 600, placed width mm 400, placed hei…
turned on the floor beats as given: 1116, not 1080 40hc, 580, 380, 300, 8,000, true, — → container 40hc, cartons 1,116, space limit 1,116, weight limit 3,581, limited by space, along length 31, across width 4, layers 9, placed length mm 380, placed width mm 580, place…
allowed to tip, a tipped orientation gives 1120 40hc, 580, 380, 300, 8,000, false, — → container 40hc, cartons 1,120, space limit 1,120, weight limit 3,581, limited by space, along length 40, across width 4, layers 7, placed length mm 300, placed width mm 580, place…
heavy cartons: the payload runs out first 20gp, 600, 400, 400, 150,000, false, — → container 20gp, cartons 201, space limit 225, weight limit 201, limited by weight, along length 9, across width 5, layers 5, placed length mm 600, placed width mm 400, placed heig…
a lower road limit caps it further 20gp, 600, 400, 400, 150,000, false, 20,000 → container 20gp, cartons 133, space limit 225, weight limit 133, limited by weight, along length 9, across width 5, layers 5, placed length mm 600, placed width mm 400, placed heig…
taller than the door: only a tipped orientation gets in 20gp, 1,000, 1,000, 2,300, 50,000, false, — → container 20gp, cartons 10, space limit 10, weight limit 604, limited by space, along length 5, across width 1, layers 2, placed length mm 1,000, placed width mm 2,300, placed hei…
pallet-sized loads in a trailer: 34 fit, the payload allows 24 trailer-13.6, 1,200, 800, 1,500, 1,000,000, true, — → container trailer-13.6, cartons 24, space limit 34, weight limit 24, limited by weight, along length 17, across width 2, layers 1, placed length mm 800, placed width mm 1,200, pla…
lighter pallets: space limited at 34 (800 mm along the length, 17 x 800 = 13600) trailer-13.6, 1,200, 800, 1,500, 500,000, true, — → container trailer-13.6, cartons 34, space limit 34, weight limit 48, limited by space, along length 17, across width 2, layers 1, placed length mm 800, placed width mm 1,200, plac…
equal limits count as space 20gp, 600, 400, 400, 134,222, false, — → container 20gp, cartons 225, space limit 225, weight limit 225, limited by space, along length 9, across width 5, layers 5, placed length mm 600, placed width mm 400, placed heigh…
taller than the door and kept upright 20gp, 1,000, 1,000, 2,300, 50,000, true, — → error: carton does not fit in 20gp in any allowed orientation
Show the other 6 tests
CaseArgumentsExpected
a carton the size of the inside cannot pass the door 20gp, 5,900, 2,352, 2,393, 1,000, false, — → error: carton does not fit in 20gp in any allowed orientation
an unknown container 45hc, 600, 400, 400, 12,000, false, — → error: unknown container "45hc"
a road limit above the container's rating 20gp, 600, 400, 400, 12,000, false, 40,000 → error: maxPayloadKg must not exceed 30200 kg for 20gp
a zero payload limit 20gp, 600, 400, 400, 12,000, false, 0 → error: maxPayloadKg must be 1 or more
a zero dimension 20gp, 600, 0, 400, 12,000, false, — → error: carton dimensions must be 1 mm or more
a weightless carton 20gp, 600, 400, 400, 0, false, — → error: cartonGrams must be 1 or more

More from the author

A carton can lie six ways. With `keepUpright` ("this way up" cartons) only the two that keep `heightMm` vertical are tried; otherwise all six. Each orientation's count is `floor(L / a) x floor(W / b) x floor(H / c)`, and the largest wins, ties going to the first in this order: as given, turned on the floor, then the four tipped ones (length-height-width, height-length-width, width-height-length, height-width-length).

Turning a carton on the floor often matters more than people expect: 580 x 380 x 300 mm cartons in a 40ft high cube give 1,080 as given and 1,116 turned (31 rows of 380 mm beat 20 rows of 580 mm). Dividing the volumes instead (`container volume / carton volume`) overstates the answer every time, since the gaps at the walls and roof are real.

It is a block stow, not an optimiser: filling the leftover strip along a wall with cartons turned the other way can add a few more, and load-planning software does that. The answer here is a safe, reproducible lower bound.

## Doors

A carton has to get in. Containers are loaded through the end doors, which are smaller than the inside (a 20ft box is 2,393 mm high inside but 2,292 mm at the door), so an orientation whose width or height does not pass the door is not used. The trailer is a curtainsider, loaded from the side, so it has no door limit. No usable orientation at all is an error.

## Weight

`weightLimit = floor(maxPayloadKg x 1000 / cartonGrams)`, using the container's rated payload or the caller's lower `maxPayloadKg` (road weight limits on the vehicle carrying the box are often the real constraint: ask the haulier). A figure above the container's rating is an error. `cartons` is the smaller limit, and `limitedBy` says which one it was. The payload is assumed spread evenly; concentrated loads have their own limits.

## Container data

Inside and door dimensions are the carrier's nominal figures, not the ISO minimums: an ISO 668 / ISO 1496-1 container is only guaranteed 5,898 x 2,350 x 2,350 mm inside (20ft), so a plan that needs the last few millimetres may not fit every box. Payloads vary with the tare of the individual box; the figures are one carrier's.

| code | inside (mm) | door (mm) | payload | source | |---|---|---|---|---| | 20gp | 5,900 x 2,352 x 2,393 | 2,340 x 2,292 | 30,200 kg | Hapag-Lloyd | | 40gp | 12,032 x 2,352 x 2,395 | 2,340 x 2,292 | 28,800 kg | Hapag-Lloyd | | 40hc | 12,032 x 2,352 x 2,700 | 2,340 x 2,597 | 28,650 kg | Hapag-Lloyd | | trailer-13.6 | 13,600 x 2,440 x 2,650 | side loaded | 24,000 kg | haulier reference values |

Trailers are not standardised the way containers are; the figures are the cautious end of the published range, and your haulier's own figures should be preferred.

## Sources

- Hapag-Lloyd, Container Specification (general purpose and high cube tables; it also reproduces the ISO 668 external and minimum internal dimensions), https://www.hapag-lloyd.com/content/dam/website/downloads/press_and_media/publications/15211_Container_Specification_engl_Gesamt_web.pdf - ISO 668:2020, Series 1 freight containers: classification, dimensions and ratings; ISO 1496-1, general cargo containers. - PLS Transport, "Trailer dimensions: technical guide", https://pls-transport.be/en/articles/trailer-dimensions ; Calcargo, "Trailer dimensions: what fits inside", https://calcargo.eu/en/blog/trailer-dimensions-what-fits-inside

Files

PathBytes
README.md3,660
data/containers.json1,623
impl/python.py3,555
impl/rust.rs4,879
impl/typescript.ts3,379
vectors.json4,753