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