use super::funejson::Value; use super::math_round_div::round_div; use super::money_amount::{money, money_to_value}; 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)) }