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_float import round_float 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, )