construction.concrete-volume
Concrete for slabs, strip footings, pads and round columns: volume to the litre and an order rounded up to 0.1 m³.
1.0.0 · published 2026-10-03 by charlie · Anterra
Pinned by 19 tests, run in TypeScript, Python and Rust.
What it does
How much concrete a set of pours needs, and how much to order from the ready-mix plant. Each element is a slab, strip footing or pad (a rectangular box: length × width × depth) or a round column (π d² / 4 × height), with a count for identical ones.
## How it is worked out
For example
concrete_volume(elements ×1, 0%)→ element cubic metres 2, cubic metres 2, order cubic metres 2 a 5 by 4 metre slab 100 mm thick is 2 cubic metresconcrete_volume(elements ×1, 5%)→ element cubic metres 2.592, cubic metres 2.592, order cubic metres 2.8 a garage slab with 5 percent wastage orders up to the next tenthconcrete_volume(elements ×1, 0%)→ element cubic metres 1.688, cubic metres 1.688, order cubic metres 1.7 a strip footing lands on half a litre and rounds half up
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.
def concrete_volume(elements: Sequence[ConcreteElement], wastage_basis_points: int) -> ConcreteVolume
| elements | ConcreteElement[] | the pours; at least one |
| wastage_basis_points | int | spillage, over-dig and uneven sub-base, 500 = 5%; 0 to 10000 |
| returns | ConcreteVolume | exact volume, and what to order from the plant |
The types it declares, generated into your project
ConcreteElementKind = Literal["slab", "strip-footing", "pad", "column"]
@dataclass(frozen=True)
class ConcreteElement:
"""One pour, or several identical ones. Dimensions in metres, taken to the nearest millimetre."""
kind: ConcreteElementKind
#: slab, strip-footing, pad: length
length: Optional[float]
#: slab, strip-footing, pad: width
width: Optional[float]
#: slab, strip-footing, pad: thickness or depth
depth: Optional[float]
#: column: diameter
diameter: Optional[float]
#: column: height
height: Optional[float]
#: how many identical elements, 1 or more
count: int
@dataclass(frozen=True)
class ConcreteVolume:
"""The volume, element by element, and the order quantity."""
#: each element times its count, to the nearest litre (3 dp)
element_cubic_metres: List[float]
#: the total, to the nearest litre (3 dp)
cubic_metres: float
#: with wastage, rounded up to the next 0.1 m³
order_cubic_metres: float
Your code names it in one line, in the file that uses it
from fune.construction.concrete_volume import concrete_volume # construction.concrete-volume@^1
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
import math
from typing import List, Optional, Sequence
from .construction_concrete_volume_types import ConcreteElement, ConcreteVolume
from .math_round_div import round_div ← from math.round-div ^1.0.0 · built alongside by fune
from .math_round_float import round_float ← from math.round-float ^1.0.0 · built alongside by fune
MAX_SAFE = 9007199254740991
MM3_PER_LITRE = 1000000
PI = 3.141592653589793
def _millimetres(element: int, name: str, value: Optional[float]) -> int:
if (
value is None
or isinstance(value, bool)
or not isinstance(value, (int, float))
or not math.isfinite(value)
or value <= 0
or value > 1000
):
raise ValueError(
"element %d: %s must be a finite number greater than 0 and at most 1000 metres, received %r" % (element, name, value)
)
mm = int(round(round_float(value, 3) * 1000))
if mm < 1:
raise ValueError("element %d: %s must be at least 1 millimetre, received %r" % (element, name, value))
return mm
def _too_large() -> None:
raise ValueError("the total volume is too large (more than 9 million cubic metres)")
def concrete_volume(elements: Sequence[ConcreteElement], wastage_basis_points: int) -> ConcreteVolume:
"""Concrete volume for a set of pours, and the quantity to order.
Dimensions are taken to the nearest millimetre and rectangular volumes are
exact integers of cubic millimetres; a round column is pi d^2 / 4 h in
floating point, rounded to a whole cubic millimetre. Working in metres and
rounding up the float is the naive way: 3 x 1 x 0.1 is 0.30000000000000004,
which orders 0.4 m3.
"""
if len(elements) == 0:
raise ValueError("elements must not be empty")
if (
isinstance(wastage_basis_points, bool)
or not isinstance(wastage_basis_points, int)
or not (0 <= wastage_basis_points <= 10000)
):
raise ValueError("wastageBasisPoints must be a whole number from 0 to 10000, received %r" % (wastage_basis_points,))
volumes: List[float] = []
total = 0
for i, el in enumerate(elements):
n = i + 1
if isinstance(el.count, bool) or not isinstance(el.count, int) or el.count < 1:
raise ValueError("element %d: count must be a whole number of at least 1, received %r" % (n, el.count))
if el.kind in ("slab", "strip-footing", "pad"):
if el.length is None or el.width is None or el.depth is None:
raise ValueError("element %d: %s needs length, width and depth" % (n, el.kind))
one = _millimetres(n, "length", el.length) * _millimetres(n, "width", el.width) * _millimetres(n, "depth", el.depth)
# Python could carry on, but TypeScript cannot, and all three must agree.
if one > MAX_SAFE:
_too_large()
elif el.kind == "column":
if el.diameter is None or el.height is None:
raise ValueError("element %d: column needs diameter and height" % (n,))
d = _millimetres(n, "diameter", el.diameter)
h = _millimetres(n, "height", el.height)
one = int(round_float(((PI * d * d) / 4) * h, 0))
if one > MAX_SAFE:
_too_large()
else:
raise ValueError('element %d: unknown kind "%s"' % (n, el.kind))
volume = one * el.count
if volume > MAX_SAFE:
_too_large()
total += volume
if total > MAX_SAFE:
_too_large()
volumes.append(round_div(volume, MM3_PER_LITRE, "half-up") / 1000)
# Up to the whole litre, add the wastage, then up to the next 100 litres.
litres_up = round_div(total, MM3_PER_LITRE, "up")
tenths = round_div(litres_up * (10000 + wastage_basis_points), 100 * 10000, "up")
return ConcreteVolume(
element_cubic_metres=volumes,
cubic_metres=round_div(total, MM3_PER_LITRE, "half-up") / 1000,
order_cubic_metres=tenths / 10,
)Install
fune build
With that line in your source, in a Python project (language python in fune.project), fune build resolves it and its 2 dependencies, pins them in fune.lock, downloads only the Python 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 construction.concrete-volume
The manifest, vectors and README with only the Python implementation. Install it without the registry with fune add ./construction.concrete-volume-1.0.0-python.fune, or fetch it from a terminal with fune pull construction.concrete-volume@1.0.0:python.
The whole function, every language, is one file too: construction.concrete-volume-1.0.0.fune, 28,439 bytes, sha256 7bd20fa333b6f2473a9d63e4ad8ae997aa56a8f1d89f9c2115c7f5506c054920. 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 construction.concrete-volume
after — your function gets the result and the arguments, and returns the final result.
# fune: after construction.concrete-volume
replace — inside this capability’s code only, calls to a dependency go to your function, with the same signature. Other capabilities that use it are unaffected; write in * to replace it everywhere.
# fune: replace math.round-div in construction.concrete-volume
# fune: replace math.round-float in construction.concrete-volume
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 construction.concrete-volume --steps.
# fune: step construction.concrete-volume 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 | |
|---|---|---|---|
| a 5 by 4 metre slab 100 mm thick is 2 cubic metres | elements ×1, 0% | → | element cubic metres 2, cubic metres 2, order cubic metres 2 |
| a garage slab with 5 percent wastage orders up to the next tenth | elements ×1, 5% | → | element cubic metres 2.592, cubic metres 2.592, order cubic metres 2.8 |
| a strip footing lands on half a litre and rounds half up | elements ×1, 0% | → | element cubic metres 1.688, cubic metres 1.688, order cubic metres 1.7 |
| four identical pads | elements ×1, 0% | → | element cubic metres 2, cubic metres 2, order cubic metres 2 |
| a 300 mm round column 3 m high | elements ×1, 0% | → | element cubic metres 0.212, cubic metres 0.212, order cubic metres 0.3 |
| a slab and four columns, with 10 percent wastage | elements ×2, 10% | → | element cubic metres 0.9, 0.471, cubic metres 1.371, order cubic metres 1.6 |
| 3 by 1 by 0.1 is 0.3 exactly, not the 0.4 a float ceiling orders | elements ×1, 0% | → | element cubic metres 0.3, cubic metres 0.3, order cubic metres 0.3 |
| exactly a tenth orders a tenth | elements ×1, 0% | → | element cubic metres 0.1, cubic metres 0.1, order cubic metres 0.1 |
| a litre over a tenth orders the next tenth | elements ×1, 0% | → | element cubic metres 0.101, cubic metres 0.101, order cubic metres 0.2 |
| dimensions are taken to the nearest millimetre | elements ×2, 0% | → | element cubic metres 2.001, 2, cubic metres 4.001, order cubic metres 4.1 |
Show the other 9 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| 100 percent wastage doubles the order | elements ×1, 100% | → | element cubic metres 0.54, cubic metres 0.54, order cubic metres 1.1 |
| an empty list is an error | , 0% | → | error: elements must not be empty |
| a slab without a width is an error | elements ×1, 0% | → | error: element 1: slab needs length, width and depth |
| a column without a height is an error | elements ×2, 0% | → | error: element 2: column needs diameter and height |
| a negative depth is an error | elements ×1, 0% | → | error: element 1: depth must be a finite number greater than 0 and at most 1000 metres |
| a dimension under half a millimetre is an error | elements ×1, 0% | → | error: element 1: depth must be at least 1 millimetre |
| a count of zero is an error | elements ×1, 0% | → | error: element 1: count must be a whole number of at least 1 |
| negative wastage is an error | elements ×1, -0.01% | → | error: wastageBasisPoints must be a whole number from 0 to 10000 |
| a volume beyond 2^53 cubic millimetres is an error | elements ×1, 0% | → | error: the total volume is too large |
More from the author
1. Every dimension, given in metres, is taken to the nearest millimetre. 2. A rectangular element is an exact integer of cubic millimetres. A column is `π × d × d / 4 × h` in floating point, evaluated in that order in every language and rounded to a whole cubic millimetre (`math.round-float`). Then times the count. 3. `elementCubicMetres` and `cubicMetres` are those, and their total, to the nearest litre (half up), shown in m³ to 3 decimal places. The total is rounded once, not summed from the rounded elements. 4. The order: the total rounded **up** to a whole litre, plus wastage, then **up** to the next 0.1 m³, the smallest step most plants sell in.
Working in metres and rounding the float up is the naive way and orders too much: 3 × 1 × 0.1 is 0.30000000000000004 in floating point, which a ceiling to the tenth turns into 0.4 m³.
## What it does not do
It does not add wastage for you (typically 5-10% for foundations poured into trenches, less for formed slabs; that is the caller's call), deduct reinforcement, work out the trench volume for an uneven formation, or know a plant's minimum load. Other shapes (a stepped footing, a ramp) are several rectangular elements.
Limits: each dimension more than 0 and at most 1000 m, and at least 1 mm after rounding; count 1 or more; wastage 0 to 10,000 basis points; the total at most 2^53 − 1 mm³ (about 9 million m³), beyond which JavaScript integers stop being exact and the calculation is refused. A slab, footing or pad needs length, width and depth; a column needs diameter and height; other fields are ignored.
Files
| Path | Bytes |
|---|---|
| README.md | 1,923 |
| impl/python.py | 3,891 |
| impl/rust.rs | 5,650 |
| impl/typescript.ts | 3,715 |
| vectors.json | 8,185 |