import math from .construction_materials_area_types import MaterialsQuantity from .math_round_div import round_div from .math_round_float import round_float 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, )