Functional Weave
Code in Python

construction.brick-count@1.0.0

impl/python.py

3,006 bytes · the Python 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 math
from typing import Sequence

from .construction_brick_count_types import BrickCount, Opening
from .math_round_div import round_div  ← from math.round-div ^1.0.0 · built alongside by fune
from .math_round_float import round_float  ← from math.round-float ^1.0.0 · built alongside by fune

# A 215 x 65 mm brick face plus a 10 mm joint is 225 x 75 mm of wall.
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.
SKIN_MORTAR = 2378
COLLAR_MORTAR = 1350
MORTAR_DIVISOR = 135
MM3_PER_LITRE = 1000000


def _millimetres(value: float, message: str, maximum: int) -> int:
    if (
        isinstance(value, bool)
        or not isinstance(value, (int, float))
        or not math.isfinite(value)
        or value <= 0
        or value > maximum
    ):
        raise ValueError(
            "%s must be a finite number greater than 0 and at most %d metres, received %r" % (message, maximum, value)
        )
    return int(round(round_float(value, 3) * 1000))


def brick_count(
    wall_length: float, wall_height: float, openings: Sequence[Opening], skins: int, wastage_basis_points: int
) -> BrickCount:
    """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.
    """
    length = _millimetres(wall_length, "wallLength", 1000)
    height = _millimetres(wall_height, "wallHeight", 100)
    if isinstance(skins, bool) or not isinstance(skins, int) or not (1 <= skins <= 4):
        raise ValueError("skins must be a whole number from 1 to 4, received %r" % (skins,))
    if (
        isinstance(wastage_basis_points, bool)
        or not isinstance(wastage_basis_points, int)
        or not (0 <= wastage_basis_points <= 10000)
    ):
        raise ValueError("wastageBasisPoints must be a whole number from 0 to 10000, received %r" % (wastage_basis_points,))
    net = length * height
    for i, opening in enumerate(openings):
        w = _millimetres(opening.width, "opening %d: width" % (i + 1), 1000)
        h = _millimetres(opening.height, "opening %d: height" % (i + 1), 1000)
        net -= w * h
    if net < 0:
        raise ValueError("openings are larger than the wall")

    bricks = round_div(net * skins * (10000 + wastage_basis_points), FACE_MM2 * 10000, "up")
    per_mm2 = skins * SKIN_MORTAR + (skins - 1) * COLLAR_MORTAR
    # Up to the whole litre first, then the wastage, so no product passes 2^53.
    litres = round_div(net * per_mm2, MORTAR_DIVISOR * MM3_PER_LITRE, "up")
    mortar = round_div(litres * (10000 + wastage_basis_points), 10000, "up")
    return BrickCount(
        net_area_square_metres=net / 1000000,
        bricks=bricks,
        mortar_cubic_metres=mortar / 1000,
    )