Functional Weave
Code in TypeScript

agri.spray-mix@1.0.1

impl/rust.rs

4,283 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

fn scaled(name: &str, value: f64, decimals: i64, low: i64, high: i64) -> i64 {
    if !value.is_finite() || value < low as f64 || value > high as f64 {
        panic!("{} must be a finite number from {} to {}, received {}", name, low, high, value);
    }
    (round_float(value, decimals) * 10f64.powi(decimals as i32)).round() as i64
}

/// Tank mix for a sprayer.
///
/// Area (0.0001 ha), water volume (0.1 L/ha), dose (0.001/ha) and tank
/// (0.1 L) are held as integers, so the job's spray volume is exact and the
/// count of full tanks cannot be thrown by a float: 129.2 ha at 250 L/ha is
/// exactly 19 tanks of 1700 L, where 129.2 x 250 in floating point is
/// 32299.999999999996 and makes it 18 and a sliver.
///
/// # Panics
/// Panics on an argument out of range, an unknown dose unit, or a liquid dose
/// that is not less than the water volume.
pub fn spray_mix(
    area_hectares: f64,
    water_volume_litres_per_hectare: f64,
    product_dose_per_hectare: f64,
    dose_unit: &str,
    tank_litres: f64,
) -> SprayMix {
    let area = scaled("areaHectares", area_hectares, 4, 0, 100000);
    let rate = scaled("waterVolumeLitresPerHectare", water_volume_litres_per_hectare, 1, 0, 10000);
    let dose = scaled("productDosePerHectare", product_dose_per_hectare, 3, 0, 100);
    let tank = scaled("tankLitres", tank_litres, 1, 0, 100000);
    if area < 1 {
        panic!("areaHectares must be at least 0.0001 hectares");
    }
    if rate < 1 {
        panic!("waterVolumeLitresPerHectare must be at least 0.1 litres");
    }
    if tank < 1 {
        panic!("tankLitres must be at least 0.1 litres");
    }
    if dose_unit != "litres" && dose_unit != "kilograms" {
        panic!("unknown doseUnit \"{}\": use litres or kilograms", dose_unit);
    }
    // A litre of product per litre of spray would leave no room for water.
    if dose_unit == "litres" && dose >= rate * 100 {
        panic!("productDosePerHectare must be less than waterVolumeLitresPerHectare for a liquid product");
    }

    let spray = area * rate; // litres x 10^5
    let tank_spray = tank * 10000; // litres x 10^5
    let full_tanks = spray / tank_spray;
    let remainder = spray - full_tanks * tank_spray;

    // A load from its spray volume in litres x 10^5: area = spray / rate,
    // product = spray x dose / (rate x 10^4) in thousandths.
    let load = |s: i64| -> TankLoad {
        let spray_ml = round_div(s, 100, "half-up");
        let product = round_div(s * dose, rate * 10000, "half-up");
        TankLoad {
            area_hectares: round_div(s, rate, "half-up") as f64 / 10000.0,
            spray_litres: spray_ml as f64 / 1000.0,
            product: product as f64 / 1000.0,
            water_litres: (if dose_unit == "litres" { spray_ml - product } else { spray_ml }) as f64 / 1000.0,
        }
    };
    SprayMix {
        tank_fills: full_tanks + if remainder > 0 { 1 } else { 0 },
        full_tanks,
        full_tank: load(tank_spray),
        partial_tank: if remainder > 0 { Some(load(remainder)) } else { None },
        total_spray_litres: round_div(spray, 100, "half-up") as f64 / 1000.0,
        total_product: round_div(area * dose, 10000, "half-up") as f64 / 1000.0,
    }
}

pub fn tank_load_to_value(t: &TankLoad) -> Value {
    Value::obj(vec![
        ("areaHectares", Value::Float(t.area_hectares)),
        ("sprayLitres", Value::Float(t.spray_litres)),
        ("product", Value::Float(t.product)),
        ("waterLitres", Value::Float(t.water_litres)),
    ])
}

pub fn spray_mix_to_value(m: &SprayMix) -> Value {
    Value::obj(vec![
        ("tankFills", Value::Int(m.tank_fills)),
        ("fullTanks", Value::Int(m.full_tanks)),
        ("fullTank", tank_load_to_value(&m.full_tank)),
        ("partialTank", m.partial_tank.as_ref().map_or(Value::Null, tank_load_to_value)),
        ("totalSprayLitres", Value::Float(m.total_spray_litres)),
        ("totalProduct", Value::Float(m.total_product)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    spray_mix_to_value(&spray_mix(
        args[0].as_f64(),
        args[1].as_f64(),
        args[2].as_f64(),
        args[3].as_str(),
        args[4].as_f64(),
    ))
}