import math from typing import Sequence from .agri_gross_margin_types import GrossMargin, MarginBasis, MarginLine from .math_round_div import round_div from .money_add import subtract_money from .money_amount import Money, money from .money_sum import sum_money MAX_MINOR = 900000000000 def gross_margin( outputs: Sequence[MarginLine], variable_costs: Sequence[MarginLine], per: MarginBasis, units: float, currency: str, ) -> GrossMargin: """Gross margin: enterprise output less its variable costs, per hectare or head. Each per-unit figure is its own total divided once, half up. Taking output per head less costs per head instead can be a penny out: 3 head with 10000 output and 5000 costs is 3333 - 1667 = 1666 that way, but 5000 / 3 = 1667. """ if per == "hectare": if isinstance(units, bool) or not isinstance(units, (int, float)) or not math.isfinite(units) or units <= 0 or units > 10000000: raise ValueError("units must be a finite number of hectares greater than 0 and at most 10000000, received %r" % (units,)) divisor = _round_half_away(units * 10000) if divisor < 1: raise ValueError("units must be at least 0.0001 hectares, received %r" % (units,)) scale = 10000 elif per == "head": if isinstance(units, bool) or not isinstance(units, int) or units < 1 or units > 10000000: raise ValueError("units must be a whole number of head from 1 to 10000000, received %r" % (units,)) divisor = units scale = 1 else: raise ValueError('unknown basis "%s": use hectare or head' % (per,)) output = sum_money([line.amount for line in outputs], currency) costs = sum_money([line.amount for line in variable_costs], currency) margin = subtract_money(output, costs) for m in (output, costs, margin): if abs(m.minor) > MAX_MINOR: raise ValueError("totals must be within 900000000000 minor units either side of zero") def per_unit(m: Money) -> Money: return money(round_div(m.minor * scale, divisor, "half-up"), currency) return GrossMargin( output=output, variable_costs=costs, gross_margin=margin, output_per_unit=per_unit(output), variable_costs_per_unit=per_unit(costs), gross_margin_per_unit=per_unit(margin), margin_basis_points=None if output.minor == 0 else round_div(margin.minor * 10000, output.minor, "half-up"), ) def _round_half_away(x: float) -> int: # Math.round and f64::round agree with this for positive numbers. return int(math.floor(x + 0.5))