Functional Weave
Code in TypeScript

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,
  };
}