agri.milk-price
Milk price per litre and payment from a contract's base price, butterfat/protein adjustments and bonuses.
1.0.0 · published 2026-10-03 by charlie · Anterra
Pinned by 18 tests, run in TypeScript, Python and Rust.
What it does
The price a dairy farmer is paid for one period's milk under a buyer's contract: a base price for milk at standard constituents, adjusted for the butterfat and protein actually delivered, plus a volume bonus and a seasonal adjustment, and the payment for the litres collected.
Contracts differ between buyers and change often, so every term is supplied by the caller in a `MilkContract`; nothing about any buyer is built in.
For example
milk_price(currency GBP, base price 38,000, standard butterfat basis points 4%, butterfat value 250, standard protein basis points 3.3%, protein value 400, volume bonuses ×2, seasonality 0, …)→ base price 38,000, butterfat adjustment 500, protein adjustment 400, volume bonus 200, seasonal adjustment 1,000, price per litre 40,100, total £300,750.00 September, good constituents, middle volume tier: 40.100 ppl on 750,000 litres is £300,750milk_price(currency GBP, base price 38,000, standard butterfat basis points 4%, butterfat value 250, standard protein basis points 3.3%, protein value 400, volume bonuses ×2, seasonality 0, …)→ base price 38,000, butterfat adjustment -375, protein adjustment -320, volume bonus 400, seasonal adjustment -1,500, price per litre 36,205, total £434,460.00 May spring flush with low constituents on the top volume tiermilk_price(currency GBP, base price 30,000, standard butterfat basis points 4%, butterfat value 255, standard protein basis points 3.3%, protein value 0, volume bonuses , seasonality , litre…)→ base price 30,000, butterfat adjustment 77, protein adjustment 0, volume bonus 0, seasonal adjustment 0, price per litre 30,077, total £300.77 a pro-rata butterfat adjustment of 76.5 thousandths rounds up to 77
The function
The same function in TypeScript, Python and Rust, pinned by the same tests. Pick your language; the choice follows you around the registry.
def milk_price(contract: MilkContract, supply: MilkSupply) -> MilkPrice
| contract | MilkContract | the buyer's terms; prices in thousandths of a minor unit per litre (38.25 ppl is 38250) |
| supply | MilkSupply | the month's collection and its average constituents |
| returns | MilkPrice |
The types it declares, generated into your project
@dataclass(frozen=True)
class MilkContract:
"""A milk buyer's price schedule. Every price is thousandths of a minor unit per litre."""
currency: str
#: for milk at the standard constituents
base_price: int
#: 400 = 4.00% butterfat
standard_butterfat_basis_points: int
#: per 0.1 percentage point above or below standard
butterfat_value: int
#: 330 = 3.30% protein
standard_protein_basis_points: int
#: per 0.1 percentage point above or below standard
protein_value: int
#: ascending by minimumLitres; the highest reached applies
volume_bonuses: List[VolumeBonus]
#: 12 monthly adjustments, January first, or empty for none
seasonality: List[int]
@dataclass(frozen=True)
class VolumeBonus:
"""A volume tier."""
#: litres in the period at which this tier starts
minimum_litres: int
#: thousandths of a minor unit per litre
bonus: int
@dataclass(frozen=True)
class MilkSupply:
"""One period's milk."""
#: 0 or more
litres: int
#: average butterfat, 412 = 4.12%
butterfat_basis_points: int
#: average protein, 335 = 3.35%
protein_basis_points: int
#: 1 to 12, picks the seasonal adjustment
month: int
@dataclass(frozen=True)
class MilkPrice:
"""Each component per litre, the price per litre, and the payment."""
base_price: int
#: pro rata, rounded half away from zero to a thousandth
butterfat_adjustment: int
#: pro rata, rounded half away from zero to a thousandth
protein_adjustment: int
volume_bonus: int
seasonal_adjustment: int
#: the sum of the five, thousandths of a minor unit per litre
price_per_litre: int
#: litres x pricePerLitre, rounded half up to the minor unit
total: Money
Your code names it in one line, in the file that uses it
from fune.agri.milk_price import milk_price # agri.milk-price@^1
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
from .agri_milk_price_types import MilkContract, MilkPrice, MilkSupply
from .math_round_div import round_div ← from math.round-div ^1.0.0 · built alongside by fune
from .money_amount import money ← from money.amount ^1.0.0 · built alongside by fune
MAX_PRICE = 1000000
MAX_LITRES = 1000000000
def _whole(value: int, name: str, low: int, high: int) -> int:
if isinstance(value, bool) or not isinstance(value, int) or value < low or value > high:
raise ValueError("%s must be a whole number from %d to %d, received %r" % (name, low, high, value))
return value
def milk_price(contract: MilkContract, supply: MilkSupply) -> MilkPrice:
"""Milk price for one period under a buyer's contract.
Prices are thousandths of a penny (or cent) per litre, because contracts
quote to 0.01 or 0.001 ppl. Constituent adjustments are pro rata per 0.1
percentage point and are each rounded to a thousandth, half away from
zero; the payment is litres x price, rounded once to the penny.
"""
money(0, contract.currency)
base = _whole(contract.base_price, "basePrice", 0, MAX_PRICE)
std_fat = _whole(contract.standard_butterfat_basis_points, "standardButterfatBasisPoints", 0, 10000)
fat_value = _whole(contract.butterfat_value, "butterfatValue", -MAX_PRICE, MAX_PRICE)
std_protein = _whole(contract.standard_protein_basis_points, "standardProteinBasisPoints", 0, 10000)
protein_value = _whole(contract.protein_value, "proteinValue", -MAX_PRICE, MAX_PRICE)
litres = _whole(supply.litres, "litres", 0, MAX_LITRES)
fat = _whole(supply.butterfat_basis_points, "butterfatBasisPoints", 0, 10000)
protein = _whole(supply.protein_basis_points, "proteinBasisPoints", 0, 10000)
month = _whole(supply.month, "month", 1, 12)
# 0.1 percentage point is 10 basis points.
fat_adjustment = round_div((fat - std_fat) * fat_value, 10, "half-up")
protein_adjustment = round_div((protein - std_protein) * protein_value, 10, "half-up")
volume_bonus = 0
previous = -1
for i, tier in enumerate(contract.volume_bonuses):
minimum = _whole(tier.minimum_litres, "volumeBonuses[%d].minimumLitres" % i, 0, MAX_LITRES)
bonus = _whole(tier.bonus, "volumeBonuses[%d].bonus" % i, -MAX_PRICE, MAX_PRICE)
if minimum <= previous:
raise ValueError("volumeBonuses must be in strictly ascending order of minimumLitres")
previous = minimum
if litres >= minimum:
volume_bonus = bonus
seasonal = 0
if len(contract.seasonality) == 12:
for i, v in enumerate(contract.seasonality):
_whole(v, "seasonality[%d]" % i, -MAX_PRICE, MAX_PRICE)
seasonal = contract.seasonality[month - 1]
elif len(contract.seasonality) != 0:
raise ValueError("seasonality must have 12 monthly values or none, received %d" % len(contract.seasonality))
price = base + fat_adjustment + protein_adjustment + volume_bonus + seasonal
if price < 0:
raise ValueError("price per litre must not be negative, received %d" % price)
if price > MAX_PRICE:
raise ValueError("price per litre must be at most %d, received %d" % (MAX_PRICE, price))
return MilkPrice(
base_price=base,
butterfat_adjustment=fat_adjustment,
protein_adjustment=protein_adjustment,
volume_bonus=volume_bonus,
seasonal_adjustment=seasonal,
price_per_litre=price,
total=money(round_div(price * litres, 1000, "half-up"), contract.currency),
)Install
fune build
With that line in your source, in a Python project (language python in fune.project), fune build resolves it and its 2 dependencies, pins them in fune.lock, downloads only the Python package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. Or pin a range in fune.project and build in one step:
fune add agri.milk-price
The manifest, vectors and README with only the Python implementation. Install it without the registry with fune add ./agri.milk-price-1.0.0-python.fune, or fetch it from a terminal with fune pull agri.milk-price@1.0.0:python.
The whole function, every language, is one file too: agri.milk-price-1.0.0.fune, 39,277 bytes, sha256 c11c5977dbfc8fb8c2181f4bb679e4b08fd9394b0a95e65f64565c17ab8a23d2. It installs into a project of any language.
Customise it in your app
The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.
before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.
# fune: before agri.milk-price
after — your function gets the result and the arguments, and returns the final result.
# fune: after agri.milk-price
replace — inside this capability’s code only, calls to a dependency go to your function, with the same signature. Other capabilities that use it are unaffected; write in * to replace it everywhere.
# fune: replace math.round-div in agri.milk-price
# fune: replace money.amount in agri.milk-price
step — your function runs at a numbered point inside the function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show agri.milk-price --steps.
# fune: step agri.milk-price after <n|label>
Tests
A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.
| Case | Arguments | Expected | |
|---|---|---|---|
| September, good constituents, middle volume tier: 40.100 ppl on 750,000 litres is £300,750 | currency GBP, base price 38,000, standard butterfat basis points 4%, butterfat value 250, standard protein basis points 3.3%, protein value 400, volume bonuses ×2, seasonality 0, … | → | base price 38,000, butterfat adjustment 500, protein adjustment 400, volume bonus 200, seasonal adjustment 1,000, price per litre 40,100, total £300,750.00 |
| May spring flush with low constituents on the top volume tier | currency GBP, base price 38,000, standard butterfat basis points 4%, butterfat value 250, standard protein basis points 3.3%, protein value 400, volume bonuses ×2, seasonality 0, … | → | base price 38,000, butterfat adjustment -375, protein adjustment -320, volume bonus 400, seasonal adjustment -1,500, price per litre 36,205, total £434,460.00 |
| a pro-rata butterfat adjustment of 76.5 thousandths rounds up to 77 | currency GBP, base price 30,000, standard butterfat basis points 4%, butterfat value 255, standard protein basis points 3.3%, protein value 0, volume bonuses , seasonality , litre… | → | base price 30,000, butterfat adjustment 77, protein adjustment 0, volume bonus 0, seasonal adjustment 0, price per litre 30,077, total £300.77 |
| below standard it rounds away from zero, to -77 | currency GBP, base price 30,000, standard butterfat basis points 4%, butterfat value 255, standard protein basis points 3.3%, protein value 0, volume bonuses , seasonality , litre… | → | base price 30,000, butterfat adjustment -77, protein adjustment 0, volume bonus 0, seasonal adjustment 0, price per litre 29,923, total £299.23 |
| the payment is rounded once: 333 litres at 39.900 ppl is 13,286.7p | currency GBP, base price 38,000, standard butterfat basis points 4%, butterfat value 250, standard protein basis points 3.3%, protein value 400, volume bonuses ×2, seasonality 0, … | → | base price 38,000, butterfat adjustment 500, protein adjustment 400, volume bonus 0, seasonal adjustment 1,000, price per litre 39,900, total £132.87 |
| the payment rounds half up: 5 litres at 30.100 ppl is 150.5p, paid as 151p | currency GBP, base price 30,100, standard butterfat basis points 4%, butterfat value 0, standard protein basis points 3.3%, protein value 0, volume bonuses , seasonality , litres … | → | base price 30,100, butterfat adjustment 0, protein adjustment 0, volume bonus 0, seasonal adjustment 0, price per litre 30,100, total £1.51 |
| a volume tier starts at exactly its minimum | currency GBP, base price 38,000, standard butterfat basis points 4%, butterfat value 250, standard protein basis points 3.3%, protein value 400, volume bonuses ×2, seasonality 0, … | → | base price 38,000, butterfat adjustment 0, protein adjustment 0, volume bonus 200, seasonal adjustment 0, price per litre 38,200, total £191,000.00 |
| one litre short of a tier gets nothing | currency GBP, base price 38,000, standard butterfat basis points 4%, butterfat value 250, standard protein basis points 3.3%, protein value 400, volume bonuses ×2, seasonality 0, … | → | base price 38,000, butterfat adjustment 0, protein adjustment 0, volume bonus 0, seasonal adjustment 0, price per litre 38,000, total £189,999.62 |
| December uses the last seasonal value | currency GBP, base price 38,000, standard butterfat basis points 4%, butterfat value 250, standard protein basis points 3.3%, protein value 400, volume bonuses ×2, seasonality 0, … | → | base price 38,000, butterfat adjustment 0, protein adjustment 0, volume bonus 0, seasonal adjustment 0, price per litre 38,000, total £38,000.00 |
| no milk is no payment | currency GBP, base price 38,000, standard butterfat basis points 4%, butterfat value 250, standard protein basis points 3.3%, protein value 400, volume bonuses ×2, seasonality 0, … | → | base price 38,000, butterfat adjustment 0, protein adjustment 0, volume bonus 0, seasonal adjustment -1,000, price per litre 37,000, total £0.00 |
Show the other 8 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| a euro contract in cents | currency EUR, base price 45,000, standard butterfat basis points 4%, butterfat value 300, standard protein basis points 3.3%, protein value 500, volume bonuses , seasonality , lit… | → | base price 45,000, butterfat adjustment 1,500, protein adjustment 1,000, volume bonus 0, seasonal adjustment 0, price per litre 47,500, total €9,500.00 |
| a seasonality list that is not 12 long is an error | currency GBP, base price 38,000, standard butterfat basis points 4%, butterfat value 250, standard protein basis points 3.3%, protein value 400, volume bonuses ×2, seasonality 0, … | → | error: seasonality must have 12 monthly values or none |
| month 13 is an error | currency GBP, base price 38,000, standard butterfat basis points 4%, butterfat value 250, standard protein basis points 3.3%, protein value 400, volume bonuses ×2, seasonality 0, … | → | error: month must be a whole number from 1 to 12 |
| volume tiers out of order are an error | currency GBP, base price 38,000, standard butterfat basis points 4%, butterfat value 250, standard protein basis points 3.3%, protein value 400, volume bonuses ×2, seasonality 0, … | → | error: volumeBonuses must be in strictly ascending order of minimumLitres |
| negative litres are an error | currency GBP, base price 38,000, standard butterfat basis points 4%, butterfat value 250, standard protein basis points 3.3%, protein value 400, volume bonuses ×2, seasonality 0, … | → | error: litres must be a whole number from 0 to 1000000000 |
| butterfat given as a percentage is an error | currency GBP, base price 38,000, standard butterfat basis points 4%, butterfat value 250, standard protein basis points 3.3%, protein value 400, volume bonuses ×2, seasonality 0, … | → | error: butterfatBasisPoints must be a whole number |
| a price that goes negative is an error | currency GBP, base price 1,000, standard butterfat basis points 4%, butterfat value 1,000, standard protein basis points 3.3%, protein value 0, volume bonuses , seasonality , litr… | → | error: price per litre must not be negative |
| a bad currency code is an error | currency gbp, base price 38,000, standard butterfat basis points 4%, butterfat value 250, standard protein basis points 3.3%, protein value 400, volume bonuses ×2, seasonality 0, … | → | error: is not an uppercase ISO 4217 currency code |
More from the author
## Units
Prices are integers in **thousandths of a minor unit per litre**: 38.25 pence per litre (ppl) is `38250`, 0.035 ppl is `35`. Buyers quote to 0.01 or 0.001 ppl, which a whole-penny price cannot hold and a float cannot hold exactly. Constituents are in basis points of the milk: 4.12% butterfat is `412`.
## How it is worked out
1. **Butterfat and protein**: (actual - standard) in 0.1 percentage points, times the contract's value per 0.1%, pro rata. 4.20% butterfat against a 4.00% standard at 0.25 ppl per 0.1% is +0.500 ppl. Each adjustment is rounded to a thousandth of a penny, half away from zero, so a deficit costs exactly what the same surplus earns. 2. **Volume bonus**: the tiers are in ascending order of `minimumLitres`; the highest tier the period's litres reach applies (not a sum of tiers). No tier reached, or no tiers, is 0. 3. **Seasonality**: twelve monthly adjustments, January first, and `month` picks one; an empty list means none. 4. The price per litre is the sum of the five components, and the payment is litres x price, rounded once, half up, to the minor unit.
A price that comes out negative is refused rather than paid.
## What it does not do
It does not apply hygiene or cell-count penalties, deductions (levies, haulage, membership), a pence-per-kg-of-solids (A+B-C) schedule, or retrospective price changes; model those as separate lines. Some contracts adjust per complete 0.1% step rather than pro rata; this one is pro rata.
## Limits
Prices and adjustments within ±1,000,000 (±10 major units per litre), the base and the final price from 0; constituents 0 to 10,000 basis points; litres 0 to 1,000,000,000; month 1 to 12.
Files
| Path | Bytes |
|---|---|
| README.md | 2,148 |
| impl/python.py | 3,418 |
| impl/rust.rs | 5,747 |
| impl/typescript.ts | 3,398 |
| vectors.json | 17,215 |