Functional Weave
Code in Rust

construction.concrete-volume@1.0.0

impl/python.py

3,891 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 List, Optional, Sequence

from .construction_concrete_volume_types import ConcreteElement, ConcreteVolume
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

MAX_SAFE = 9007199254740991
MM3_PER_LITRE = 1000000
PI = 3.141592653589793


def _millimetres(element: int, name: str, value: Optional[float]) -> int:
    if (
        value is None
        or isinstance(value, bool)
        or not isinstance(value, (int, float))
        or not math.isfinite(value)
        or value <= 0
        or value > 1000
    ):
        raise ValueError(
            "element %d: %s must be a finite number greater than 0 and at most 1000 metres, received %r" % (element, name, value)
        )
    mm = int(round(round_float(value, 3) * 1000))
    if mm < 1:
        raise ValueError("element %d: %s must be at least 1 millimetre, received %r" % (element, name, value))
    return mm


def _too_large() -> None:
    raise ValueError("the total volume is too large (more than 9 million cubic metres)")


def concrete_volume(elements: Sequence[ConcreteElement], wastage_basis_points: int) -> ConcreteVolume:
    """Concrete volume for a set of pours, and the quantity to order.

    Dimensions are taken to the nearest millimetre and rectangular volumes are
    exact integers of cubic millimetres; a round column is pi d^2 / 4 h in
    floating point, rounded to a whole cubic millimetre. Working in metres and
    rounding up the float is the naive way: 3 x 1 x 0.1 is 0.30000000000000004,
    which orders 0.4 m3.
    """
    if len(elements) == 0:
        raise ValueError("elements must not be empty")
    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,))

    volumes: List[float] = []
    total = 0
    for i, el in enumerate(elements):
        n = i + 1
        if isinstance(el.count, bool) or not isinstance(el.count, int) or el.count < 1:
            raise ValueError("element %d: count must be a whole number of at least 1, received %r" % (n, el.count))
        if el.kind in ("slab", "strip-footing", "pad"):
            if el.length is None or el.width is None or el.depth is None:
                raise ValueError("element %d: %s needs length, width and depth" % (n, el.kind))
            one = _millimetres(n, "length", el.length) * _millimetres(n, "width", el.width) * _millimetres(n, "depth", el.depth)
            # Python could carry on, but TypeScript cannot, and all three must agree.
            if one > MAX_SAFE:
                _too_large()
        elif el.kind == "column":
            if el.diameter is None or el.height is None:
                raise ValueError("element %d: column needs diameter and height" % (n,))
            d = _millimetres(n, "diameter", el.diameter)
            h = _millimetres(n, "height", el.height)
            one = int(round_float(((PI * d * d) / 4) * h, 0))
            if one > MAX_SAFE:
                _too_large()
        else:
            raise ValueError('element %d: unknown kind "%s"' % (n, el.kind))
        volume = one * el.count
        if volume > MAX_SAFE:
            _too_large()
        total += volume
        if total > MAX_SAFE:
            _too_large()
        volumes.append(round_div(volume, MM3_PER_LITRE, "half-up") / 1000)

    # Up to the whole litre, add the wastage, then up to the next 100 litres.
    litres_up = round_div(total, MM3_PER_LITRE, "up")
    tenths = round_div(litres_up * (10000 + wastage_basis_points), 100 * 10000, "up")
    return ConcreteVolume(
        element_cubic_metres=volumes,
        cubic_metres=round_div(total, MM3_PER_LITRE, "half-up") / 1000,
        order_cubic_metres=tenths / 10,
    )