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 wastematerialsArea(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 givesmaterialsArea(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
| areaSquareMetres | float | the area to cover, 0 to 1,000,000; taken to the nearest square centimetre |
| coveragePerPack | float | square metres one pack covers in one coat: a box of tiles, a tin of paint, a sheet of board |
| coats | int | 1 for tiles, board and flooring; 2 or more for paint |
| wastageBasisPoints | int | cuts and breakage, 1000 = 10%; 0 to 10000 |
| returns | MaterialsQuantity |
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";
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
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.
| Case | Arguments | Expected | |
|---|---|---|---|
| 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
| Case | Arguments | Expected | |
|---|---|---|---|
| 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
| Path | Bytes |
|---|---|
| README.md | 1,747 |
| impl/python.py | 2,450 |
| impl/rust.rs | 2,888 |
| impl/typescript.ts | 2,344 |
| vectors.json | 3,838 |