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 wastematerials_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 givesmaterials_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_metres | float | the area to cover, 0 to 1,000,000; taken to the nearest square centimetre |
| coverage_per_pack | 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 |
| wastage_basis_points | 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.
#[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(…)
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
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.
| 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 |