Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
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 math.round-float ^1.0.0 · built alongside by fune
from .money_amount import Money, money ← from money.amount ^1.0.0 · built alongside by fune
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,
)