Functional Weave
Code in Rust

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.

pub fn materials_area(area_square_metres: f64, coverage_per_pack: f64, coats: i64, wastage_basis_points: i64) -> 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

/// How much to buy, and how much of it will be left over.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct MaterialsQuantity {
    /// square metres including coats and waste, rounded up to the square centimetre
    pub area_to_cover: f64,
    /// whole packs to buy
    pub packs: i64,
    /// square metres the packs cover beyond areaToCover
    pub surplus: f64,
}

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

fune!(construction.materials-area@^1);  // then call materials_area(…)
impl/rust.rs · 66 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.

use super::funejson::Value;  ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::math_round_div::round_div;  ← from math.round-div ^1.0.0 · built alongside by fune
use super::math_round_float::round_float;  ← from math.round-float ^1.0.0 · built alongside by fune

const CM2_PER_M2: f64 = 10000.0;

// The nearest whole square centimetre to the double given, so 2.88 is 28800,
// not the 28799.999... that 2.88 * 10000 might suggest.
fn square_centimetres(m2: f64) -> i64 {
    (round_float(m2, 4) * CM2_PER_M2).round() as i64
}

/// 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.
///
/// # Panics
/// Panics on an area, coverage, coat count or wastage outside its range.
pub fn materials_area(area_square_metres: f64, coverage_per_pack: f64, coats: i64, wastage_basis_points: i64) -> MaterialsQuantity {
    if !area_square_metres.is_finite() || area_square_metres < 0.0 || area_square_metres > 1000000.0 {
        panic!("areaSquareMetres must be a finite number from 0 to 1000000, received {}", area_square_metres);
    }
    if !coverage_per_pack.is_finite() || coverage_per_pack > 100000.0 {
        panic!("coveragePerPack must be at least 0.0001 and at most 100000 square metres, received {}", coverage_per_pack);
    }
    let cover = square_centimetres(coverage_per_pack);
    if cover < 1 {
        panic!("coveragePerPack must be at least 0.0001 and at most 100000 square metres, received {}", coverage_per_pack);
    }
    if !(1..=10).contains(&coats) {
        panic!("coats must be a whole number from 1 to 10, received {}", coats);
    }
    if !(0..=10000).contains(&wastage_basis_points) {
        panic!("wastageBasisPoints must be a whole number from 0 to 10000, received {}", wastage_basis_points);
    }

    let area = square_centimetres(area_square_metres);
    let needed = round_div(area * coats * (10000 + wastage_basis_points), 10000, "up");
    let packs = round_div(needed, cover, "up");
    MaterialsQuantity {
        area_to_cover: needed as f64 / CM2_PER_M2,
        packs,
        surplus: (packs * cover - needed) as f64 / CM2_PER_M2,
    }
}

pub fn materials_quantity_to_value(q: &MaterialsQuantity) -> Value {
    Value::obj(vec![
        ("areaToCover", Value::Float(q.area_to_cover)),
        ("packs", Value::Int(q.packs)),
        ("surplus", Value::Float(q.surplus)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    if let Value::Float(f) = &args[2] {
        panic!("coats must be a whole number from 1 to 10, received {}", f);
    }
    if let Value::Float(f) = &args[3] {
        panic!("wastageBasisPoints must be a whole number from 0 to 10000, received {}", f);
    }
    materials_quantity_to_value(&materials_area(args[0].as_f64(), args[1].as_f64(), args[2].as_i64(), args[3].as_i64()))
}

Install

fune build

With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and its 2 dependencies, pins them in fune.lock, downloads only the Rust 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. A crate’s build.rs runs it before every compile. Or pin a range in fune.project and build in one step:

fune add construction.materials-area
Download for Rust construction.materials-area-1.0.0-rust.fune · 11,565 bytes sha256 f7db13a24c29cebb4f711ba14ad5632d0187ba184d30f18d48d9af7d177e5500

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

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