import re from typing import Sequence from .property_floor_area_types import FloorArea, FloorAreaUnit, RoomDimensions from .units_convert import convert_units _DIMENSION = re.compile(r"[0-9]{1,3}(\.[0-9]{1,3})?") _MAX_ROOMS = 1000 def _thousandths(room: str, field: str, text: str) -> int: """A dimension as an integer number of thousandths.""" if not isinstance(text, str) or not _DIMENSION.fullmatch(text): raise ValueError( 'room "%s": %s must be a decimal below 1000 with at most 3 decimal places, received "%s"' % (room, field, text) ) whole, _, fraction = text.partition(".") value = int(whole) * 1000 + int(fraction.ljust(3, "0")) if value == 0: raise ValueError('room "%s": %s must be greater than zero' % (room, field)) return value def _decimal(millionths: int) -> str: """An integer number of millionths as the shortest exact decimal.""" whole, rest = divmod(millionths, 1_000_000) fraction = str(rest).rjust(6, "0").rstrip("0") return str(whole) if fraction == "" else "%d.%s" % (whole, fraction) def floor_area(rooms: Sequence[RoomDimensions], length_unit: FloorAreaUnit, decimals: int) -> FloorArea: """Room areas and their total, exact, from length x width of each room, and the total in square metres and square feet rounded once at the end.""" if length_unit not in ("m", "ft"): raise ValueError('unknown length unit "%s": use m or ft' % (length_unit,)) if len(rooms) == 0: raise ValueError("rooms must not be empty") if len(rooms) > _MAX_ROOMS: raise ValueError("at most %d rooms, received %d" % (_MAX_ROOMS, len(rooms))) areas = [_thousandths(r.name, "length", r.length) * _thousandths(r.name, "width", r.width) for r in rooms] total = _decimal(sum(areas)) source = "m2" if length_unit == "m" else "ft2" return FloorArea( room_areas=[_decimal(a) for a in areas], total=total, square_metres=convert_units(total, source, "m2", decimals), square_feet=convert_units(total, source, "ft2", decimals), )