construction.materials-area@1.0.0
impl/typescript.ts
2,344 bytes · the TypeScript implementation · view 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,
};
}