Functional Weave
Code in Python

construction.brick-count@1.0.0

impl/typescript.ts

2,842 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 BrickCount, type Opening } from "./construction_brick_count_types.ts";

// A 215 x 65 mm brick face plus a 10 mm joint is 225 x 75 mm of wall.
const FACE_MM2 = 225 * 75;
// Mortar per mm2 of wall face, in mm3: each skin fills the joints around a
// brick, (16875 - 215 x 65) / 16875 of the face, 102.5 mm deep, which is
// 2378/135 mm; each collar joint between skins is 10 mm = 1350/135 mm.
const SKIN_MORTAR = 2378;
const COLLAR_MORTAR = 1350;
const MORTAR_DIVISOR = 135;
const MM3_PER_LITRE = 1000000;

function millimetres(value: number, message: string, max: number): number {
  if (typeof value !== "number" || !Number.isFinite(value) || value <= 0 || value > max) {
    throw new RangeError(`${message} must be a finite number greater than 0 and at most ${max} metres, received ${value}`);
  }
  return Math.round(roundFloat(value, 3) * 1000);
}

/**
 * Bricks and mortar for a wall of UK standard bricks, openings subtracted.
 *
 * The count is the net face area over one brick's 225 x 75 mm share of it,
 * 59.26 per m2 per skin (the trade's "60 a metre"), times the skins, plus
 * wastage, rounded up. Mortar is the joint volume that geometry implies.
 * Everything is whole millimetres and integer arithmetic.
 */
export function brickCount(
  wallLength: number,
  wallHeight: number,
  openings: readonly Opening[],
  skins: number,
  wastageBasisPoints: number
): BrickCount {
  const length = millimetres(wallLength, "wallLength", 1000);
  const height = millimetres(wallHeight, "wallHeight", 100);
  if (!Number.isInteger(skins) || skins < 1 || skins > 4) {
    throw new RangeError(`skins must be a whole number from 1 to 4, received ${skins}`);
  }
  if (!Number.isInteger(wastageBasisPoints) || wastageBasisPoints < 0 || wastageBasisPoints > 10000) {
    throw new RangeError(`wastageBasisPoints must be a whole number from 0 to 10000, received ${wastageBasisPoints}`);
  }
  let net = length * height;
  openings.forEach((opening, i) => {
    const w = millimetres(opening.width, `opening ${i + 1}: width`, 1000);
    const h = millimetres(opening.height, `opening ${i + 1}: height`, 1000);
    net -= w * h;
  });
  if (net < 0) {
    throw new RangeError("openings are larger than the wall");
  }

  const bricks = roundDiv(net * skins * (10000 + wastageBasisPoints), FACE_MM2 * 10000, "up");
  const perMm2 = skins * SKIN_MORTAR + (skins - 1) * COLLAR_MORTAR;
  // Up to the whole litre first, then the wastage, so no product passes 2^53.
  const litres = roundDiv(net * perMm2, MORTAR_DIVISOR * MM3_PER_LITRE, "up");
  const mortar = roundDiv(litres * (10000 + wastageBasisPoints), 10000, "up");
  return {
    netAreaSquareMetres: net / 1000000,
    bricks,
    mortarCubicMetres: mortar / 1000,
  };
}