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
milkPrice(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,750milkPrice(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 tiermilkPrice(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.
export function milkPrice(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
/** A milk buyer's price schedule. Every price is thousandths of a minor unit per litre. */
export interface MilkContract {
readonly currency: string;
/** for milk at the standard constituents */
readonly basePrice: number;
/** 400 = 4.00% butterfat */
readonly standardButterfatBasisPoints: number;
/** per 0.1 percentage point above or below standard */
readonly butterfatValue: number;
/** 330 = 3.30% protein */
readonly standardProteinBasisPoints: number;
/** per 0.1 percentage point above or below standard */
readonly proteinValue: number;
/** ascending by minimumLitres; the highest reached applies */
readonly volumeBonuses: readonly VolumeBonus[];
/** 12 monthly adjustments, January first, or empty for none */
readonly seasonality: readonly number[];
}
/** A volume tier. */
export interface VolumeBonus {
/** litres in the period at which this tier starts */
readonly minimumLitres: number;
/** thousandths of a minor unit per litre */
readonly bonus: number;
}
/** One period's milk. */
export interface MilkSupply {
/** 0 or more */
readonly litres: number;
/** average butterfat, 412 = 4.12% */
readonly butterfatBasisPoints: number;
/** average protein, 335 = 3.35% */
readonly proteinBasisPoints: number;
/** 1 to 12, picks the seasonal adjustment */
readonly month: number;
}
/** Each component per litre, the price per litre, and the payment. */
export interface MilkPrice {
readonly basePrice: number;
/** pro rata, rounded half away from zero to a thousandth */
readonly butterfatAdjustment: number;
/** pro rata, rounded half away from zero to a thousandth */
readonly proteinAdjustment: number;
readonly volumeBonus: number;
readonly seasonalAdjustment: number;
/** the sum of the five, thousandths of a minor unit per litre */
readonly pricePerLitre: number;
/** litres x pricePerLitre, rounded half up to the minor unit */
readonly total: Money;
}
Your code names it in one line, in the file that uses it
import { milkPrice } from "#fune/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.
import { money } from "./money_amount.ts"; ← from money.amount ^1.0.0 · built alongside by fune
import { roundDiv } from "./math_round_div.ts"; ← from math.round-div ^1.0.0 · built alongside by fune
import { type MilkContract, type MilkPrice, type MilkSupply } from "./agri_milk_price_types.ts";
const MAX_PRICE = 1000000;
const MAX_LITRES = 1000000000;
function whole(value: number, name: string, min: number, max: number): number {
if (typeof value !== "number" || !Number.isInteger(value) || value < min || value > max) {
throw new RangeError(`${name} must be a whole number from ${min} to ${max}, received ${value}`);
}
return value;
}
/**
* 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.
*/
export function milkPrice(contract: MilkContract, supply: MilkSupply): MilkPrice {
money(0, contract.currency);
const base = whole(contract.basePrice, "basePrice", 0, MAX_PRICE);
const stdFat = whole(contract.standardButterfatBasisPoints, "standardButterfatBasisPoints", 0, 10000);
const fatValue = whole(contract.butterfatValue, "butterfatValue", -MAX_PRICE, MAX_PRICE);
const stdProtein = whole(contract.standardProteinBasisPoints, "standardProteinBasisPoints", 0, 10000);
const proteinValue = whole(contract.proteinValue, "proteinValue", -MAX_PRICE, MAX_PRICE);
const litres = whole(supply.litres, "litres", 0, MAX_LITRES);
const fat = whole(supply.butterfatBasisPoints, "butterfatBasisPoints", 0, 10000);
const protein = whole(supply.proteinBasisPoints, "proteinBasisPoints", 0, 10000);
const month = whole(supply.month, "month", 1, 12);
// 0.1 percentage point is 10 basis points.
const fatAdjustment = roundDiv((fat - stdFat) * fatValue, 10, "half-up");
const proteinAdjustment = roundDiv((protein - stdProtein) * proteinValue, 10, "half-up");
let volumeBonus = 0;
let previous = -1;
contract.volumeBonuses.forEach((tier, i) => {
const minimum = whole(tier.minimumLitres, `volumeBonuses[${i}].minimumLitres`, 0, MAX_LITRES);
const bonus = whole(tier.bonus, `volumeBonuses[${i}].bonus`, -MAX_PRICE, MAX_PRICE);
if (minimum <= previous) throw new RangeError("volumeBonuses must be in strictly ascending order of minimumLitres");
previous = minimum;
if (litres >= minimum) volumeBonus = bonus;
});
let seasonal = 0;
if (contract.seasonality.length === 12) {
contract.seasonality.forEach((v, i) => whole(v, `seasonality[${i}]`, -MAX_PRICE, MAX_PRICE));
seasonal = contract.seasonality[month - 1];
} else if (contract.seasonality.length !== 0) {
throw new RangeError(`seasonality must have 12 monthly values or none, received ${contract.seasonality.length}`);
}
const price = base + fatAdjustment + proteinAdjustment + volumeBonus + seasonal;
if (price < 0) throw new RangeError(`price per litre must not be negative, received ${price}`);
if (price > MAX_PRICE) throw new RangeError(`price per litre must be at most ${MAX_PRICE}, received ${price}`);
return {
basePrice: base,
butterfatAdjustment: fatAdjustment,
proteinAdjustment,
volumeBonus,
seasonalAdjustment: seasonal,
pricePerLitre: price,
total: money(roundDiv(price * litres, 1000, "half-up"), contract.currency),
};
}Install
fune build
With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and its 2 dependencies, pins them in fune.lock, downloads only the TypeScript 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 TypeScript implementation. Install it without the registry with fune add ./agri.milk-price-1.0.0-typescript.fune, or fetch it from a terminal with fune pull agri.milk-price@1.0.0:typescript.
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 |