Functional Weave
Code in TypeScript

agri.milk-price@1.0.0

impl/rust.rs

5,747 bytes · the Rust implementation · view raw

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