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.
pub fn 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
/// A milk buyer's price schedule. Every price is thousandths of a minor unit per litre.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MilkContract {
pub currency: String,
/// for milk at the standard constituents
pub base_price: i64,
/// 400 = 4.00% butterfat
pub standard_butterfat_basis_points: i64,
/// per 0.1 percentage point above or below standard
pub butterfat_value: i64,
/// 330 = 3.30% protein
pub standard_protein_basis_points: i64,
/// per 0.1 percentage point above or below standard
pub protein_value: i64,
/// ascending by minimumLitres; the highest reached applies
pub volume_bonuses: Vec<VolumeBonus>,
/// 12 monthly adjustments, January first, or empty for none
pub seasonality: Vec<i64>,
}
/// A volume tier.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct VolumeBonus {
/// litres in the period at which this tier starts
pub minimum_litres: i64,
/// thousandths of a minor unit per litre
pub bonus: i64,
}
/// One period's milk.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MilkSupply {
/// 0 or more
pub litres: i64,
/// average butterfat, 412 = 4.12%
pub butterfat_basis_points: i64,
/// average protein, 335 = 3.35%
pub protein_basis_points: i64,
/// 1 to 12, picks the seasonal adjustment
pub month: i64,
}
/// Each component per litre, the price per litre, and the payment.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MilkPrice {
pub base_price: i64,
/// pro rata, rounded half away from zero to a thousandth
pub butterfat_adjustment: i64,
/// pro rata, rounded half away from zero to a thousandth
pub protein_adjustment: i64,
pub volume_bonus: i64,
pub seasonal_adjustment: i64,
/// the sum of the five, thousandths of a minor unit per litre
pub price_per_litre: i64,
/// litres x pricePerLitre, rounded half up to the minor unit
pub total: Money,
}
Your code names it in one line, in the file that uses it
fune!(agri.milk-price@^1); // then call milk_price(…)
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
use super::funejson::Value; ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::math_round_div::round_div; ← from math.round-div ^1.0.0 · built alongside by fune
use super::money_amount::{money, money_to_value}; ← from money.amount ^1.0.0 · built alongside by fune
const MAX_PRICE: i64 = 1_000_000;
const MAX_LITRES: i64 = 1_000_000_000;
fn whole(value: i64, name: &str, low: i64, high: i64) -> i64 {
if value < low || value > high {
panic!("{} must be a whole number from {} to {}, received {}", name, low, high, value);
}
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.
///
/// # Panics
/// Panics on any figure out of range, volume tiers out of order, a
/// seasonality list that is not 12 long, or a negative price.
pub fn milk_price(contract: &MilkContract, supply: &MilkSupply) -> MilkPrice {
money(0, &contract.currency);
let base = whole(contract.base_price, "basePrice", 0, MAX_PRICE);
let std_fat = whole(contract.standard_butterfat_basis_points, "standardButterfatBasisPoints", 0, 10000);
let fat_value = whole(contract.butterfat_value, "butterfatValue", -MAX_PRICE, MAX_PRICE);
let std_protein = whole(contract.standard_protein_basis_points, "standardProteinBasisPoints", 0, 10000);
let protein_value = whole(contract.protein_value, "proteinValue", -MAX_PRICE, MAX_PRICE);
let litres = whole(supply.litres, "litres", 0, MAX_LITRES);
let fat = whole(supply.butterfat_basis_points, "butterfatBasisPoints", 0, 10000);
let protein = whole(supply.protein_basis_points, "proteinBasisPoints", 0, 10000);
let month = whole(supply.month, "month", 1, 12);
// 0.1 percentage point is 10 basis points.
let fat_adjustment = round_div((fat - std_fat) * fat_value, 10, "half-up");
let protein_adjustment = round_div((protein - std_protein) * protein_value, 10, "half-up");
let mut volume_bonus = 0;
let mut previous = -1;
for (i, tier) in contract.volume_bonuses.iter().enumerate() {
let minimum = whole(tier.minimum_litres, &format!("volumeBonuses[{}].minimumLitres", i), 0, MAX_LITRES);
let bonus = whole(tier.bonus, &format!("volumeBonuses[{}].bonus", i), -MAX_PRICE, MAX_PRICE);
if minimum <= previous {
panic!("volumeBonuses must be in strictly ascending order of minimumLitres");
}
previous = minimum;
if litres >= minimum {
volume_bonus = bonus;
}
}
let seasonal = match contract.seasonality.len() {
12 => {
for (i, v) in contract.seasonality.iter().enumerate() {
whole(*v, &format!("seasonality[{}]", i), -MAX_PRICE, MAX_PRICE);
}
contract.seasonality[(month - 1) as usize]
}
0 => 0,
n => panic!("seasonality must have 12 monthly values or none, received {}", n),
};
let price = base + fat_adjustment + protein_adjustment + volume_bonus + seasonal;
if price < 0 {
panic!("price per litre must not be negative, received {}", price);
}
if price > MAX_PRICE {
panic!("price per litre must be at most {}, received {}", MAX_PRICE, price);
}
MilkPrice {
base_price: base,
butterfat_adjustment: fat_adjustment,
protein_adjustment,
volume_bonus,
seasonal_adjustment: seasonal,
price_per_litre: price,
total: money(round_div(price * litres, 1000, "half-up"), &contract.currency),
}
}
pub fn milk_price_to_value(p: &MilkPrice) -> Value {
Value::obj(vec![
("basePrice", Value::Int(p.base_price)),
("butterfatAdjustment", Value::Int(p.butterfat_adjustment)),
("proteinAdjustment", Value::Int(p.protein_adjustment)),
("volumeBonus", Value::Int(p.volume_bonus)),
("seasonalAdjustment", Value::Int(p.seasonal_adjustment)),
("pricePerLitre", Value::Int(p.price_per_litre)),
("total", money_to_value(&p.total)),
])
}
// The vectors' integers must really be integers: 4.12 is not 412 basis points.
fn int(v: &Value, name: &str) -> i64 {
match v {
Value::Int(i) => *i,
other => panic!("{} must be a whole number, received {:?}", name, other),
}
}
pub fn fune_vector(args: &[Value]) -> Value {
let c = &args[0];
let s = &args[1];
let contract = MilkContract {
currency: c.get("currency").as_str().to_string(),
base_price: int(c.get("basePrice"), "basePrice"),
standard_butterfat_basis_points: int(c.get("standardButterfatBasisPoints"), "standardButterfatBasisPoints"),
butterfat_value: int(c.get("butterfatValue"), "butterfatValue"),
standard_protein_basis_points: int(c.get("standardProteinBasisPoints"), "standardProteinBasisPoints"),
protein_value: int(c.get("proteinValue"), "proteinValue"),
volume_bonuses: c
.get("volumeBonuses")
.as_arr()
.iter()
.map(|t| VolumeBonus {
minimum_litres: int(t.get("minimumLitres"), "minimumLitres"),
bonus: int(t.get("bonus"), "bonus"),
})
.collect(),
seasonality: c.get("seasonality").as_arr().iter().map(|v| int(v, "seasonality")).collect(),
};
let supply = MilkSupply {
litres: int(s.get("litres"), "litres"),
butterfat_basis_points: int(s.get("butterfatBasisPoints"), "butterfatBasisPoints"),
protein_basis_points: int(s.get("proteinBasisPoints"), "proteinBasisPoints"),
month: int(s.get("month"), "month"),
};
milk_price_to_value(&milk_price(&contract, &supply))
}Install
fune build
With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and its 2 dependencies, pins them in fune.lock, downloads only the Rust 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. A crate’s build.rs runs it before every compile. 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 Rust implementation. Install it without the registry with fune add ./agri.milk-price-1.0.0-rust.fune, or fetch it from a terminal with fune pull agri.milk-price@1.0.0:rust.
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 |