Functional Weave
Code in Rust

construction.materials-area@1.0.0

impl/python.py

2,450 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 .construction_materials_area_types import MaterialsQuantity
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

CM2_PER_M2 = 10000


def _square_centimetres(m2: float) -> int:
    # The nearest whole square centimetre to the double given, so 2.88 is
    # 28800, not the 28799.999... that 2.88 * 10000 might suggest.
    return int(round(round_float(m2, 4) * CM2_PER_M2))


def _is_number(value) -> bool:
    return isinstance(value, (int, float)) and not isinstance(value, bool) and math.isfinite(value)


def _is_int(value) -> bool:
    return isinstance(value, int) and not isinstance(value, bool)


def materials_area(area_square_metres: float, coverage_per_pack: float, coats: int, wastage_basis_points: int) -> MaterialsQuantity:
    """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.
    """
    if not _is_number(area_square_metres) or not (0 <= area_square_metres <= 1000000):
        raise ValueError("areaSquareMetres must be a finite number from 0 to 1000000, received %r" % (area_square_metres,))
    if not _is_number(coverage_per_pack) or coverage_per_pack > 100000:
        raise ValueError("coveragePerPack must be at least 0.0001 and at most 100000 square metres, received %r" % (coverage_per_pack,))
    cover = _square_centimetres(coverage_per_pack)
    if cover < 1:
        raise ValueError("coveragePerPack must be at least 0.0001 and at most 100000 square metres, received %r" % (coverage_per_pack,))
    if not _is_int(coats) or not (1 <= coats <= 10):
        raise ValueError("coats must be a whole number from 1 to 10, received %r" % (coats,))
    if not _is_int(wastage_basis_points) or not (0 <= wastage_basis_points <= 10000):
        raise ValueError("wastageBasisPoints must be a whole number from 0 to 10000, received %r" % (wastage_basis_points,))

    area = _square_centimetres(area_square_metres)
    needed = round_div(area * coats * (10000 + wastage_basis_points), 10000, "up")
    packs = round_div(needed, cover, "up")
    return MaterialsQuantity(
        area_to_cover=needed / CM2_PER_M2,
        packs=packs,
        surplus=(packs * cover - needed) / CM2_PER_M2,
    )