import math from typing import List, Optional, Sequence from .agri_feed_ration_types import RationCost, RationFeed, RationLine from .math_round_float import round_float from .money_amount import Money, money MAX_SAFE = 9007199254740991 def _half_up(numerator: int, denominator: int) -> int: # Half up for a non-negative numerator and a positive denominator. q = (2 * numerator + denominator) // (2 * denominator) if q > MAX_SAFE: raise ValueError("total cost is too large") return q def _whole(value: object) -> bool: return isinstance(value, int) and not isinstance(value, bool) def feed_ration(feeds: Sequence[RationFeed], head: int, days: int, currency: str) -> RationCost: """Ration cost and dry matter intake. Weights are grams and prices minor units per tonne, so a feed's daily cost is exactly grams x price / 1,000,000 and every total is rounded once from the exact sum. Costing the group from a rounded daily cost multiplies the rounding: 467.5p a head a day is 1,402,500p for 100 cows over 30 days, not 468 x 3000 = 1,404,000p. """ if not _whole(head) or head < 1 or head > 1000000: raise ValueError("head must be a whole number from 1 to 1000000, received %r" % (head,)) if not _whole(days) or days < 1 or days > 3660: raise ValueError("days must be a whole number from 1 to 3660, received %r" % (days,)) money(0, currency) grams = 0 dry_matter = 0 # grams x basis points cost = 0 # grams x minor units per tonne lines: List[RationLine] = [] for i, feed in enumerate(feeds): n = i + 1 kg = feed.fresh_kg_per_head if isinstance(kg, bool) or not isinstance(kg, (int, float)) or not math.isfinite(kg) or kg < 0 or kg > 1000: raise ValueError("feed %d: freshKgPerHead must be a finite number from 0 to 1000, received %r" % (n, kg)) g = int(round(round_float(float(kg), 3) * 1000)) bp = feed.dry_matter_basis_points if not _whole(bp) or bp < 0 or bp > 10000: raise ValueError("feed %d: dryMatterBasisPoints must be a whole number from 0 to 10000, received %r" % (n, bp)) price = feed.price_per_tonne if price.currency != currency: raise ValueError("feed %d: currency mismatch: %s and %s" % (n, price.currency, currency)) if not _whole(price.minor) or price.minor < 0 or price.minor > 100000000: raise ValueError("feed %d: pricePerTonne must be from 0 to 100000000 minor units, received %r" % (n, price.minor)) line_dm = g * bp line_cost = g * price.minor grams += g dry_matter += line_dm cost += line_cost lines.append( RationLine( name=feed.name, fresh_kg_per_head=g / 1000, dry_matter_kg_per_head=_half_up(line_dm, 10000) / 1000, cost_per_head=money(_half_up(line_cost, 1000000), currency), ) ) per_tonne_dm: Optional[Money] = None if dry_matter == 0 else money(_half_up(cost * 10000, dry_matter), currency) return RationCost( lines=lines, fresh_kg_per_head=grams / 1000, dry_matter_kg_per_head=_half_up(dry_matter, 10000) / 1000, dry_matter_basis_points=None if grams == 0 else _half_up(dry_matter, grams), cost_per_head_per_day=money(_half_up(cost, 1000000), currency), total_cost=money(_half_up(cost * head * days, 1000000), currency), cost_per_tonne_dry_matter=per_tonne_dm, )