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))
}