from typing import List, Sequence from .math_gcd_lcm import lcm from .money_allocate import allocate from .money_amount import Money from .property_service_charge_apportion_types import ApportionBasis, ServiceChargeLine, ServiceChargeUnit _MAX_SAFE = 2**53 - 1 def _is_int(value: object) -> bool: return isinstance(value, int) and not isinstance(value, bool) def apportion_service_charge( total: Money, units: Sequence[ServiceChargeUnit], basis: ApportionBasis ) -> List[ServiceChargeLine]: """Share a service charge between units by lease fraction, floor area or fixed percentage. Every share becomes an integer weight, and money.allocate does the one split, so the lines always add up to the total exactly.""" if basis not in ("fraction", "floor-area", "percentage"): raise ValueError('unknown basis "%s": use fraction, floor-area or percentage' % (basis,)) if len(units) == 0: raise ValueError("units must not be empty") for u in units: if not _is_int(u.share) or u.share < 0 or u.share > _MAX_SAFE: raise ValueError('unit "%s": share must be a whole number, zero or more, received %r' % (u.unit, u.share)) if basis == "fraction": if u.of is None or not _is_int(u.of) or u.of <= 0 or u.of > _MAX_SAFE: raise ValueError('unit "%s": a fraction needs a denominator "of" greater than zero' % (u.unit,)) elif u.of is not None: raise ValueError('unit "%s": "of" is only for the fraction basis' % (u.unit,)) if basis == "fraction": common = 1 for u in units: common = lcm(common, u.of) weights = [u.share * (common // u.of) for u in units] total_weight = sum(weights) if total_weight > _MAX_SAFE: raise ValueError("the fractions are too fine to apportion exactly") if total_weight != common: raise ValueError("the fractions add up to %d/%d, not exactly 1" % (total_weight, common)) else: weights = [u.share for u in units] total_weight = sum(weights) if basis == "percentage" and total_weight != 10000: raise ValueError("the percentages add up to %d basis points, not 10000" % (total_weight,)) if total_weight == 0: raise ValueError("the floor areas add up to zero") if total_weight > _MAX_SAFE or abs(total.minor) * total_weight > _MAX_SAFE: raise ValueError("the fractions are too fine to apportion exactly") parts = allocate(total, weights) return [ServiceChargeLine(unit=u.unit, charge=parts[i]) for i, u in enumerate(units)]