Functional Weave
Code in TypeScript

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

  • concreteVolume(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 metres
  • concreteVolume(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 tenth
  • concreteVolume(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.

export function concreteVolume(elements: readonly ConcreteElement[], wastageBasisPoints: number): ConcreteVolume
elementsConcreteElement[]the pours; at least one
wastageBasisPointsintspillage, over-dig and uneven sub-base, 500 = 5%; 0 to 10000
returnsConcreteVolumeexact volume, and what to order from the plant

The types it declares, generated into your project

export type ConcreteElementKind = "slab" | "strip-footing" | "pad" | "column";

/** One pour, or several identical ones. Dimensions in metres, taken to the nearest millimetre. */
export interface ConcreteElement {
  readonly kind: ConcreteElementKind;
  /** slab, strip-footing, pad: length */
  readonly length: number | null;
  /** slab, strip-footing, pad: width */
  readonly width: number | null;
  /** slab, strip-footing, pad: thickness or depth */
  readonly depth: number | null;
  /** column: diameter */
  readonly diameter: number | null;
  /** column: height */
  readonly height: number | null;
  /** how many identical elements, 1 or more */
  readonly count: number;
}

/** The volume, element by element, and the order quantity. */
export interface ConcreteVolume {
  /** each element times its count, to the nearest litre (3 dp) */
  readonly elementCubicMetres: readonly number[];
  /** the total, to the nearest litre (3 dp) */
  readonly cubicMetres: number;
  /** with wastage, rounded up to the next 0.1 m³ */
  readonly orderCubicMetres: number;
}

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

import { concreteVolume } from "#fune/construction.concrete-volume@^1";
impl/typescript.ts · 83 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 { roundDiv } from "./math_round_div.ts";  ← from math.round-div ^1.0.0 · built alongside by fune
import { roundFloat } from "./math_round_float.ts";  ← from math.round-float ^1.0.0 · built alongside by fune
import { type ConcreteElement, type ConcreteVolume } from "./construction_concrete_volume_types.ts";

const MAX_SAFE = 9007199254740991;
const MM3_PER_LITRE = 1000000;
const PI = 3.141592653589793;

function millimetres(element: number, name: string, value: number | null): number {
  if (value === null || value === undefined || typeof value !== "number" || !Number.isFinite(value) || value <= 0 || value > 1000) {
    throw new RangeError(`element ${element}: ${name} must be a finite number greater than 0 and at most 1000 metres, received ${value}`);
  }
  const mm = Math.round(roundFloat(value, 3) * 1000);
  if (mm < 1) {
    throw new RangeError(`element ${element}: ${name} must be at least 1 millimetre, received ${value}`);
  }
  return mm;
}

function tooLarge(): never {
  throw new RangeError("the total volume is too large (more than 9 million cubic metres)");
}

/**
 * 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.
 */
export function concreteVolume(elements: readonly ConcreteElement[], wastageBasisPoints: number): ConcreteVolume {
  if (elements.length === 0) {
    throw new RangeError("elements must not be empty");
  }
  if (!Number.isInteger(wastageBasisPoints) || wastageBasisPoints < 0 || wastageBasisPoints > 10000) {
    throw new RangeError(`wastageBasisPoints must be a whole number from 0 to 10000, received ${wastageBasisPoints}`);
  }

  const volumes: number[] = [];
  let total = 0;
  elements.forEach((el, i) => {
    const n = i + 1;
    if (!Number.isInteger(el.count) || el.count < 1) {
      throw new RangeError(`element ${n}: count must be a whole number of at least 1, received ${el.count}`);
    }
    let one: number;
    if (el.kind === "slab" || el.kind === "strip-footing" || el.kind === "pad") {
      if (el.length === null || el.width === null || el.depth === null) {
        throw new RangeError(`element ${n}: ${el.kind} needs length, width and depth`);
      }
      one = millimetres(n, "length", el.length) * millimetres(n, "width", el.width) * millimetres(n, "depth", el.depth);
      // Beyond 2^53 a JavaScript product is no longer exact; the other
      // languages could carry on, but all three must agree.
      if (one > MAX_SAFE) tooLarge();
    } else if (el.kind === "column") {
      if (el.diameter === null || el.height === null) {
        throw new RangeError(`element ${n}: column needs diameter and height`);
      }
      const d = millimetres(n, "diameter", el.diameter);
      const h = millimetres(n, "height", el.height);
      one = roundFloat(((PI * d * d) / 4) * h, 0);
      if (one > MAX_SAFE) tooLarge();
    } else {
      throw new RangeError(`element ${n}: unknown kind "${el.kind}"`);
    }
    const volume = one * el.count;
    if (volume > MAX_SAFE) tooLarge();
    total += volume;
    if (total > MAX_SAFE) tooLarge();
    volumes.push(roundDiv(volume, MM3_PER_LITRE, "half-up") / 1000);
  });

  // Up to the whole litre, add the wastage, then up to the next 100 litres.
  const litresUp = roundDiv(total, MM3_PER_LITRE, "up");
  const tenths = roundDiv(litresUp * (10000 + wastageBasisPoints), 100 * 10000, "up");
  return {
    elementCubicMetres: volumes,
    cubicMetres: roundDiv(total, MM3_PER_LITRE, "half-up") / 1000,
    orderCubicMetres: tenths / 10,
  };
}

Install

fune build

With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and its 2 dependencies, 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 construction.concrete-volume
Download for TypeScript construction.concrete-volume-1.0.0-typescript.fune · 18,514 bytes sha256 ed834c0b4fe206c691f9dbf9b147aa04999e1b15812dd9d6033ec323e4db6ceb

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

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.

CaseArgumentsExpected
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
CaseArgumentsExpected
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

PathBytes
README.md1,923
impl/python.py3,891
impl/rust.rs5,650
impl/typescript.ts3,715
vectors.json8,185