Functional Weave
Code in Rust

agri.feed-ration@1.0.0

impl/rust.rs

5,533 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_float::round_float;  ← from math.round-float ^1.0.0 · built alongside by fune
use super::money_amount::{money, money_from_value, money_to_value};  ← from money.amount ^1.0.0 · built alongside by fune

const MAX_SAFE: i128 = 9_007_199_254_740_991;

// Half up for a non-negative numerator and a positive denominator.
fn half_up(numerator: i128, denominator: i128) -> i64 {
    let q = (2 * numerator + denominator) / (2 * denominator);
    if q > MAX_SAFE {
        panic!("total cost is too large");
    }
    q as i64
}

/// Ration cost and dry matter intake.
///
/// Weights are grams and prices minor units per tonne, so a feed's daily cost
/// is exactly grams x price / 1,000,000 and every total is rounded once from
/// the exact sum. Costing the group from a rounded daily cost multiplies the
/// rounding: 467.5p a head a day is 1,402,500p for 100 cows over 30 days, not
/// 468 x 3000 = 1,404,000p.
///
/// # Panics
/// Panics on an out-of-range quantity, DM, price, head or days, a price in
/// another currency, or a total beyond 2^53 - 1 minor units.
pub fn feed_ration(feeds: &[RationFeed], head: i64, days: i64, currency: &str) -> RationCost {
    if !(1..=1_000_000).contains(&head) {
        panic!("head must be a whole number from 1 to 1000000, received {}", head);
    }
    if !(1..=3660).contains(&days) {
        panic!("days must be a whole number from 1 to 3660, received {}", days);
    }
    money(0, currency);

    let mut grams: i128 = 0;
    let mut dry_matter: i128 = 0;
    let mut cost: i128 = 0;
    let mut lines: Vec<RationLine> = Vec::with_capacity(feeds.len());
    for (i, feed) in feeds.iter().enumerate() {
        let n = i + 1;
        let kg = feed.fresh_kg_per_head;
        if !kg.is_finite() || kg < 0.0 || kg > 1000.0 {
            panic!("feed {}: freshKgPerHead must be a finite number from 0 to 1000, received {}", n, kg);
        }
        let g = (round_float(kg, 3) * 1000.0).round() as i128;
        let bp = feed.dry_matter_basis_points;
        if !(0..=10000).contains(&bp) {
            panic!("feed {}: dryMatterBasisPoints must be a whole number from 0 to 10000, received {}", n, bp);
        }
        let price = &feed.price_per_tonne;
        if price.currency != currency {
            panic!("feed {}: currency mismatch: {} and {}", n, price.currency, currency);
        }
        if !(0..=100_000_000).contains(&price.minor) {
            panic!("feed {}: pricePerTonne must be from 0 to 100000000 minor units, received {}", n, price.minor);
        }
        let line_dm = g * bp as i128;
        let line_cost = g * price.minor as i128;
        grams += g;
        dry_matter += line_dm;
        cost += line_cost;
        lines.push(RationLine {
            name: feed.name.clone(),
            fresh_kg_per_head: g as f64 / 1000.0,
            dry_matter_kg_per_head: half_up(line_dm, 10000) as f64 / 1000.0,
            cost_per_head: money(half_up(line_cost, 1_000_000), currency),
        });
    }

    RationCost {
        lines,
        fresh_kg_per_head: grams as f64 / 1000.0,
        dry_matter_kg_per_head: half_up(dry_matter, 10000) as f64 / 1000.0,
        dry_matter_basis_points: if grams == 0 { None } else { Some(half_up(dry_matter, grams)) },
        cost_per_head_per_day: money(half_up(cost, 1_000_000), currency),
        total_cost: money(half_up(cost * head as i128 * days as i128, 1_000_000), currency),
        cost_per_tonne_dry_matter: if dry_matter == 0 {
            None
        } else {
            Some(money(half_up(cost * 10000, dry_matter), currency))
        },
    }
}

pub fn ration_cost_to_value(r: &RationCost) -> Value {
    let lines = r
        .lines
        .iter()
        .map(|l| {
            Value::obj(vec![
                ("name", Value::str(&l.name)),
                ("freshKgPerHead", Value::Float(l.fresh_kg_per_head)),
                ("dryMatterKgPerHead", Value::Float(l.dry_matter_kg_per_head)),
                ("costPerHead", money_to_value(&l.cost_per_head)),
            ])
        })
        .collect();
    Value::obj(vec![
        ("lines", Value::Arr(lines)),
        ("freshKgPerHead", Value::Float(r.fresh_kg_per_head)),
        ("dryMatterKgPerHead", Value::Float(r.dry_matter_kg_per_head)),
        ("dryMatterBasisPoints", r.dry_matter_basis_points.map_or(Value::Null, Value::Int)),
        ("costPerHeadPerDay", money_to_value(&r.cost_per_head_per_day)),
        ("totalCost", money_to_value(&r.total_cost)),
        ("costPerTonneDryMatter", r.cost_per_tonne_dry_matter.as_ref().map_or(Value::Null, money_to_value)),
    ])
}

fn whole(v: &Value, message: String) -> i64 {
    match v {
        Value::Int(i) => *i,
        _ => panic!("{}, received {:?}", message, v),
    }
}

pub fn fune_vector(args: &[Value]) -> Value {
    let feeds: Vec<RationFeed> = args[0]
        .as_arr()
        .iter()
        .enumerate()
        .map(|(i, v)| RationFeed {
            name: v.get("name").as_str().to_string(),
            fresh_kg_per_head: v.get("freshKgPerHead").as_f64(),
            dry_matter_basis_points: whole(
                v.get("dryMatterBasisPoints"),
                format!("feed {}: dryMatterBasisPoints must be a whole number from 0 to 10000", i + 1),
            ),
            price_per_tonne: money_from_value(v.get("pricePerTonne")),
        })
        .collect();
    let head = whole(&args[1], "head must be a whole number from 1 to 1000000".to_string());
    let days = whole(&args[2], "days must be a whole number from 1 to 3660".to_string());
    ration_cost_to_value(&feed_ration(&feeds, head, days, args[3].as_str()))
}