Functional Weave
Code in Python

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

  • materials_area(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
  • materials_area(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
  • materials_area(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.

def materials_area(area_square_metres: float, coverage_per_pack: float, coats: int, wastage_basis_points: int) -> MaterialsQuantity
area_square_metresfloatthe area to cover, 0 to 1,000,000; taken to the nearest square centimetre
coverage_per_packfloatsquare 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
wastage_basis_pointsintcuts and breakage, 1000 = 10%; 0 to 10000
returnsMaterialsQuantity

The type it declares, generated into your project

@dataclass(frozen=True)
class MaterialsQuantity:
    """How much to buy, and how much of it will be left over."""

    #: square metres including coats and waste, rounded up to the square centimetre
    area_to_cover: float
    #: whole packs to buy
    packs: int
    #: square metres the packs cover beyond areaToCover
    surplus: float

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

from fune.construction.materials_area import materials_area  # construction.materials-area@^1
impl/python.py · 51 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 math

from .construction_materials_area_types import MaterialsQuantity
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

CM2_PER_M2 = 10000


def _square_centimetres(m2: float) -> int:
    # The nearest whole square centimetre to the double given, so 2.88 is
    # 28800, not the 28799.999... that 2.88 * 10000 might suggest.
    return int(round(round_float(m2, 4) * CM2_PER_M2))


def _is_number(value) -> bool:
    return isinstance(value, (int, float)) and not isinstance(value, bool) and math.isfinite(value)


def _is_int(value) -> bool:
    return isinstance(value, int) and not isinstance(value, bool)


def materials_area(area_square_metres: float, coverage_per_pack: float, coats: int, wastage_basis_points: int) -> MaterialsQuantity:
    """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.
    """
    if not _is_number(area_square_metres) or not (0 <= area_square_metres <= 1000000):
        raise ValueError("areaSquareMetres must be a finite number from 0 to 1000000, received %r" % (area_square_metres,))
    if not _is_number(coverage_per_pack) or coverage_per_pack > 100000:
        raise ValueError("coveragePerPack must be at least 0.0001 and at most 100000 square metres, received %r" % (coverage_per_pack,))
    cover = _square_centimetres(coverage_per_pack)
    if cover < 1:
        raise ValueError("coveragePerPack must be at least 0.0001 and at most 100000 square metres, received %r" % (coverage_per_pack,))
    if not _is_int(coats) or not (1 <= coats <= 10):
        raise ValueError("coats must be a whole number from 1 to 10, received %r" % (coats,))
    if not _is_int(wastage_basis_points) or not (0 <= wastage_basis_points <= 10000):
        raise ValueError("wastageBasisPoints must be a whole number from 0 to 10000, received %r" % (wastage_basis_points,))

    area = _square_centimetres(area_square_metres)
    needed = round_div(area * coats * (10000 + wastage_basis_points), 10000, "up")
    packs = round_div(needed, cover, "up")
    return MaterialsQuantity(
        area_to_cover=needed / CM2_PER_M2,
        packs=packs,
        surplus=(packs * cover - needed) / CM2_PER_M2,
    )

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.materials-area
Download for Python construction.materials-area-1.0.0-python.fune · 11,112 bytes sha256 bcc7e449c832c38da0458a3a7ed80e3bb001243e036ca3801ef971e3e9488819

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

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