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::math_round_float::round_float; ← from math.round-float ^1.0.0 · built alongside by fune
const NUTRIENTS: [&str; 3] = ["nitrogen", "phosphate", "potash"];
fn tenths_of_percent(name: &str, value: f64) -> i64 {
if !value.is_finite() || value < 0.0 || value > 100.0 {
panic!("analysis {} must be a finite percentage from 0 to 100, received {}", name, value);
}
(round_float(value, 1) * 10.0).round() as i64
}
/// Product rate for a fertiliser, set by the limiting nutrient.
///
/// For each targeted nutrient the product needed is target / analysis; the
/// rate is the smallest of those, so no nutrient is applied beyond its target,
/// and the others show as a shortfall. The arithmetic is exact in tenths (of a
/// percent and of a kilogram) and the rate is rounded once, half up.
///
/// # Panics
/// Panics on an analysis, target or area out of range, no target, or a
/// product with none of the targeted nutrients.
pub fn fertiliser_application(analysis: &NutrientAnalysis, targets: &NutrientTargets, area_hectares: f64) -> FertiliserPlan {
let a = [
tenths_of_percent("nitrogen", analysis.nitrogen),
tenths_of_percent("phosphate", analysis.phosphate),
tenths_of_percent("potash", analysis.potash),
];
if a[0] + a[1] + a[2] > 1000 {
panic!("analysis must not total more than 100%");
}
let t = [targets.nitrogen, targets.phosphate, targets.potash];
for (i, v) in t.iter().enumerate() {
if *v < 0 || *v > 1000 {
panic!("target {} must be a whole number of kg/ha from 0 to 1000, received {}", NUTRIENTS[i], v);
}
}
if !area_hectares.is_finite() || area_hectares <= 0.0 || area_hectares > 10000.0 {
panic!("areaHectares must be a finite number greater than 0 and at most 10000, received {}", area_hectares);
}
let area = (round_float(area_hectares, 4) * 10000.0).round() as i64;
if area < 1 {
panic!("areaHectares must be at least 0.0001 hectares, received {}", area_hectares);
}
if t[0] + t[1] + t[2] == 0 {
panic!("at least one target must be above zero");
}
// Least product: target_i / a_i smallest, compared by cross-multiplying.
let mut limit: Option<usize> = None;
for i in 0..3 {
if t[i] == 0 || a[i] == 0 {
continue;
}
match limit {
Some(l) if t[i] * a[l] >= t[l] * a[i] => {}
_ => limit = Some(i),
}
}
let limit = match limit {
Some(l) => l,
None => panic!("the product contains none of the targeted nutrients"),
};
// kg/ha in tenths = target x 100 / (a / 10) x 10.
let rate = round_div(t[limit] * 10000, a[limit], "half-up");
let s: Vec<i64> = a.iter().map(|ai| round_div(rate * ai, 1000, "half-up")).collect();
FertiliserPlan {
limiting_nutrient: NUTRIENTS[limit].to_string(),
product_kg_per_hectare: rate as f64 / 10.0,
total_product_kg: round_div(rate * area, 10000, "half-up") as f64 / 10.0,
supplied: NutrientSupply {
nitrogen: s[0] as f64 / 10.0,
phosphate: s[1] as f64 / 10.0,
potash: s[2] as f64 / 10.0,
},
balance: NutrientSupply {
nitrogen: (s[0] - t[0] * 10) as f64 / 10.0,
phosphate: (s[1] - t[1] * 10) as f64 / 10.0,
potash: (s[2] - t[2] * 10) as f64 / 10.0,
},
}
}
pub fn nutrient_supply_to_value(s: &NutrientSupply) -> Value {
Value::obj(vec![
("nitrogen", Value::Float(s.nitrogen)),
("phosphate", Value::Float(s.phosphate)),
("potash", Value::Float(s.potash)),
])
}
pub fn fertiliser_plan_to_value(p: &FertiliserPlan) -> Value {
Value::obj(vec![
("limitingNutrient", Value::str(&p.limiting_nutrient)),
("productKgPerHectare", Value::Float(p.product_kg_per_hectare)),
("totalProductKg", Value::Float(p.total_product_kg)),
("supplied", nutrient_supply_to_value(&p.supplied)),
("balance", nutrient_supply_to_value(&p.balance)),
])
}
fn target(v: &Value, name: &str) -> i64 {
match v {
Value::Int(i) => *i,
other => panic!("target {} must be a whole number of kg/ha from 0 to 1000, received {:?}", name, other),
}
}
pub fn fune_vector(args: &[Value]) -> Value {
let analysis = NutrientAnalysis {
nitrogen: args[0].get("nitrogen").as_f64(),
phosphate: args[0].get("phosphate").as_f64(),
potash: args[0].get("potash").as_f64(),
};
let targets = NutrientTargets {
nitrogen: target(args[1].get("nitrogen"), "nitrogen"),
phosphate: target(args[1].get("phosphate"), "phosphate"),
potash: target(args[1].get("potash"), "potash"),
};
fertiliser_plan_to_value(&fertiliser_application(&analysis, &targets, args[2].as_f64()))
}