Functional Weave
Code in TypeScript

construction.materials-area

Packs of tiles, paint, plasterboard or flooring to cover an area, with waste and coats, rounded up to whole packs.

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

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

What it does

How many packs of a sheet or surface material to buy for an area: boxes of tiles, tins of paint, sheets of plasterboard, packs of flooring. The caller says what one pack covers in one coat; the answer is whole packs, rounded up, and what will be left over.

## How it is worked out

For example

  • materialsArea(10, 1, 1, 10%) → area to cover 11, packs 11, surplus 0 ten square metres of tiles in 1 square metre boxes with 10 percent waste
  • materialsArea(8.64, 2.88, 1, 0%) → area to cover 8.64, packs 3, surplus 0 8.64 square metres of 2.88 plasterboard is 3 sheets, not the 4 a float ceiling gives
  • materialsArea(2.1, 0.3, 1, 0%) → area to cover 2.1, packs 7, surplus 0 2.1 over 0.3 is exactly 7 packs

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 materialsArea(areaSquareMetres: number, coveragePerPack: number, coats: number, wastageBasisPoints: number): MaterialsQuantity
areaSquareMetresfloatthe area to cover, 0 to 1,000,000; taken to the nearest square centimetre
coveragePerPackfloatsquare metres one pack covers in one coat: a box of tiles, a tin of paint, a sheet of board
coatsint1 for tiles, board and flooring; 2 or more for paint
wastageBasisPointsintcuts and breakage, 1000 = 10%; 0 to 10000
returnsMaterialsQuantity

The type it declares, generated into your project

/** How much to buy, and how much of it will be left over. */
export interface MaterialsQuantity {
  /** square metres including coats and waste, rounded up to the square centimetre */
  readonly areaToCover: number;
  /** whole packs to buy */
  readonly packs: number;
  /** square metres the packs cover beyond areaToCover */
  readonly surplus: number;
}

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

import { materialsArea } from "#fune/construction.materials-area@^1";
impl/typescript.ts · 52 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 MaterialsQuantity } from "./construction_materials_area_types.ts";

const CM2_PER_M2 = 10000;

// The nearest whole square centimetre to the double given, so 2.88 is 28800,
// not the 28799.999... that 2.88 * 10000 might suggest.
function squareCentimetres(m2: number): number {
  return Math.round(roundFloat(m2, 4) * CM2_PER_M2);
}

/**
 * Packs needed to cover an area, rounded up to whole packs.
 *
 * Everything is converted to whole square centimetres first and the division
 * is done in integers. Dividing floats and taking the ceiling is the naive way
 * and it is wrong: 8.64 / 2.88 is 3.0000000000000004, which buys a fourth
 * sheet of plasterboard nobody needs.
 */
export function materialsArea(
  areaSquareMetres: number,
  coveragePerPack: number,
  coats: number,
  wastageBasisPoints: number
): MaterialsQuantity {
  if (typeof areaSquareMetres !== "number" || !Number.isFinite(areaSquareMetres) || areaSquareMetres < 0 || areaSquareMetres > 1000000) {
    throw new RangeError(`areaSquareMetres must be a finite number from 0 to 1000000, received ${areaSquareMetres}`);
  }
  if (typeof coveragePerPack !== "number" || !Number.isFinite(coveragePerPack) || coveragePerPack > 100000) {
    throw new RangeError(`coveragePerPack must be at least 0.0001 and at most 100000 square metres, received ${coveragePerPack}`);
  }
  const cover = squareCentimetres(coveragePerPack);
  if (cover < 1) {
    throw new RangeError(`coveragePerPack must be at least 0.0001 and at most 100000 square metres, received ${coveragePerPack}`);
  }
  if (!Number.isInteger(coats) || coats < 1 || coats > 10) {
    throw new RangeError(`coats must be a whole number from 1 to 10, received ${coats}`);
  }
  if (!Number.isInteger(wastageBasisPoints) || wastageBasisPoints < 0 || wastageBasisPoints > 10000) {
    throw new RangeError(`wastageBasisPoints must be a whole number from 0 to 10000, received ${wastageBasisPoints}`);
  }

  const area = squareCentimetres(areaSquareMetres);
  const needed = roundDiv(area * coats * (10000 + wastageBasisPoints), 10000, "up");
  const packs = roundDiv(needed, cover, "up");
  return {
    areaToCover: needed / CM2_PER_M2,
    packs,
    surplus: (packs * cover - needed) / CM2_PER_M2,
  };
}

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.materials-area
Download for TypeScript construction.materials-area-1.0.0-typescript.fune · 11,009 bytes sha256 71d6eedb0685ea3eb5ebb205cff429d208da5fcfa9ee5fdd2431ce993e6aeafc

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

The whole function, every language, is one file too: construction.materials-area-1.0.0.fune, 16,530 bytes, sha256 ec98f3e9cf334e260a5f1e0af38a1e2415fbd563bd2111edbe6616f6e4ec6079. 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.materials-area

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

// fune: after construction.materials-area

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.materials-area
// fune: replace math.round-float in construction.materials-area

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.materials-area --steps.

// fune: step construction.materials-area 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
ten square metres of tiles in 1 square metre boxes with 10 percent waste 10, 1, 1, 10% → area to cover 11, packs 11, surplus 0
8.64 square metres of 2.88 plasterboard is 3 sheets, not the 4 a float ceiling gives 8.64, 2.88, 1, 0% → area to cover 8.64, packs 3, surplus 0
2.1 over 0.3 is exactly 7 packs 2.1, 0.3, 1, 0% → area to cover 2.1, packs 7, surplus 0
two coats of paint on 42.5 square metres from 30 square metre tins 42.5, 30, 2, 0% → area to cover 85, packs 3, surplus 5
20 square metres of plasterboard with 10 percent waste 20, 2.88, 1, 10% → area to cover 22, packs 8, surplus 1.04
flooring at 1.76 square metres a pack with 5 percent waste 14.3, 1.76, 1, 5% → area to cover 15.015, packs 9, surplus 0.825
one basis point of waste tips over into a second pack 1, 1, 1, 0.01% → area to cover 1, packs 2, surplus 1
area is taken to the square centimetre and the waste rounded up to it 0.333, 0.5, 1, 3.33% → area to cover 0.344, packs 1, surplus 0.156
100 percent waste doubles the area 5, 3, 1, 100% → area to cover 10, packs 4, surplus 2
an exact fit needs no spare 30, 30, 1, 0% → area to cover 30, packs 1, surplus 0
Show the other 8 tests
CaseArgumentsExpected
nothing to cover needs no packs 0, 2.5, 2, 10% → area to cover 0, packs 0, surplus 0
zero coverage is an error 10, 0, 1, 0% → error: coveragePerPack must be at least 0.0001 and at most 100000 square metres
coverage below a square centimetre is an error 10, 0, 1, 0% → error: coveragePerPack must be at least 0.0001 and at most 100000 square metres
a negative area is an error -1, 1, 1, 0% → error: areaSquareMetres must be a finite number from 0 to 1000000
zero coats is an error 10, 1, 0, 0% → error: coats must be a whole number from 1 to 10
half a coat is an error 10, 1, 1.5, 0% → error: coats must be a whole number from 1 to 10
more than 100 percent waste is an error 10, 1, 1, 100.01% → error: wastageBasisPoints must be a whole number from 0 to 10000
fractional basis points are an error 10, 1, 1, 0.025% → error: wastageBasisPoints must be a whole number from 0 to 10000

More from the author

1. The area and the pack coverage are each taken to the nearest whole square centimetre (via `math.round-float` to 4 decimal places). 2. `area × coats × (1 + waste)` is worked out in integers and rounded **up** to a whole square centimetre: that is `areaToCover`. 3. Packs are `areaToCover ÷ coverage`, rounded **up** (`math.round-div`). 4. `surplus` is `packs × coverage − areaToCover`.

The point of the integer detour is step 3. Dividing the floats and taking the ceiling is the naive way and it is wrong in ordinary cases: 8.64 m² of wall with 2.88 m² plasterboard sheets is exactly three sheets, but `8.64 / 2.88` is 3.0000000000000004, and `ceil` buys a fourth. The same happens with 2.1 / 0.3 and many other everyday figures.

## Using it

- **Tiles, flooring:** coverage is the m² per box as printed on it; coats 1. Typical waste is 5% for plain layouts and 10-15% for diagonal or herringbone, but that is the caller's decision. - **Paint:** coverage is litres per tin × the manufacturer's m² per litre; coats is the number of coats. - **Plasterboard:** coverage is one sheet, e.g. 2.88 for 2400 × 1200.

It does not deduct openings (subtract them from the area first), or work out tile layout and cuts: it is a quantity, not a setting-out plan.

Limits: area 0 to 1,000,000 m²; coverage 0.0001 to 100,000 m²; 1 to 10 coats; waste 0 to 10,000 basis points (0 to 100%). A zero area needs zero packs.

Files

PathBytes
README.md1,747
impl/python.py2,450
impl/rust.rs2,888
impl/typescript.ts2,344
vectors.json3,838