Functional Weave
Code in TypeScript

agri.fertiliser-application@1.0.0

impl/rust.rs

4,857 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::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()))
}