Functional Weave
Code in Python

agri.yield-per-hectare@1.0.1

impl/rust.rs

5,308 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::units_convert::convert_units;  ← from units.convert ^1.0.0 · built alongside by fune

// digits / 10^scale
#[derive(Clone, Copy)]
struct Decimal {
    digits: i128,
    scale: u32,
}

fn is_decimal(text: &str) -> bool {
    let mut parts = text.splitn(2, '.');
    let digits = |s: &str| !s.is_empty() && s.bytes().all(|b| b.is_ascii_digit());
    digits(parts.next().unwrap_or("")) && parts.next().map_or(true, digits)
}

fn parse(name: &str, text: &str, limited: bool) -> Decimal {
    if !is_decimal(text) {
        panic!("{} must be plain non-negative decimal text like \"12.5\", received \"{}\"", name, text);
    }
    let mut parts = text.splitn(2, '.');
    let whole = parts.next().unwrap_or("");
    let fraction = parts.next().unwrap_or("").trim_end_matches('0');
    let joined = format!("{}{}", whole, fraction);
    let significant = joined.trim_start_matches('0');
    if limited && (significant.len() > 12 || fraction.len() > 6) {
        panic!("{} \"{}\" has too many digits: at most 12 significant digits and 6 decimal places", name, text);
    }
    if significant.len() > 30 {
        panic!("{} \"{}\" has too many digits", name, text);
    }
    Decimal {
        digits: if significant.is_empty() { 0 } else { significant.parse::<i128>().unwrap() },
        scale: fraction.len() as u32,
    }
}

// Inputs are capped at 12 significant digits and 6 places and the largest
// unit factor is the square mile (2589988110336 / 10^6 m²), so after the
// powers of ten cancel nothing here passes about 1e33; the checks make any
// breach loud instead of wrapping.
fn mul(a: i128, b: i128) -> i128 {
    a.checked_mul(b).unwrap_or_else(|| panic!("value too large to divide exactly"))
}

fn ratio(top: &[Decimal], bottom: &[Decimal]) -> (i128, i128) {
    let (mut num, mut den, mut exponent) = (1i128, 1i128, 0i64);
    for d in top {
        num = mul(num, d.digits);
        exponent -= d.scale as i64;
    }
    for d in bottom {
        den = mul(den, d.digits);
        exponent += d.scale as i64;
    }
    if exponent > 0 {
        num = mul(num, 10i128.pow(exponent as u32));
    } else {
        den = mul(den, 10i128.pow((-exponent) as u32));
    }
    (num, den)
}

// Long division, one digit at a time, rounded half up, trailing zeros trimmed.
fn to_text((num, den): (i128, i128), decimals: i64) -> String {
    let mut whole = num / den;
    let mut remainder = num % den;
    let mut fraction: i128 = 0;
    for _ in 0..decimals {
        remainder = mul(remainder, 10);
        fraction = fraction * 10 + remainder / den;
        remainder %= den;
    }
    if mul(remainder, 2) >= den {
        fraction += 1;
        if fraction == 10i128.pow(decimals as u32) {
            fraction = 0;
            whole += 1;
        }
    }
    let mut text = whole.to_string();
    if decimals > 0 {
        let padded = format!("{:0>width$}", fraction, width = decimals as usize);
        let shown = padded.trim_end_matches('0');
        if !shown.is_empty() {
            text.push('.');
            text.push_str(shown);
        }
    }
    text
}

/// Yield per hectare and per acre, exactly.
///
/// The area unit's size comes from units.convert as exact decimal text
/// (1 acre = 4046.8564224 m²), so every answer is one exact fraction rounded
/// once. In floats, 1.005 t from 1 ha is 1.00 t/ha to two places; here it is
/// 1.01.
///
/// # Panics
/// Panics on malformed or over-long decimals, a zero area, a unit that is not
/// an area, or `decimals` outside 0..=12.
pub fn yield_per_hectare(quantity: &str, area: &str, area_unit: &str, decimals: i64) -> YieldPerArea {
    let q = parse("quantity", quantity, true);
    let a = parse("area", area, true);
    if !(0..=12).contains(&decimals) {
        panic!("decimals must be a whole number from 0 to 12, received {}", decimals);
    }
    let unit = parse("unit size", &convert_units("1", area_unit, "m2", 12), false);
    if a.digits == 0 {
        panic!("area must be greater than zero");
    }
    let acre = parse("acre", &convert_units("1", "acre", "m2", 12), false);
    let hectare = Decimal { digits: 10000, scale: 0 };
    YieldPerArea {
        per_hectare: to_text(ratio(&[q, hectare], &[a, unit]), decimals),
        per_acre: to_text(ratio(&[q, acre], &[a, unit]), decimals),
        hectares: to_text(ratio(&[a, unit], &[hectare]), decimals),
        acres: to_text(ratio(&[a, unit], &[acre]), decimals),
    }
}

pub fn yield_per_area_to_value(y: &YieldPerArea) -> Value {
    Value::obj(vec![
        ("perHectare", Value::str(&y.per_hectare)),
        ("perAcre", Value::str(&y.per_acre)),
        ("hectares", Value::str(&y.hectares)),
        ("acres", Value::str(&y.acres)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    let text = |v: &Value, name: &str| -> String {
        match v {
            Value::Str(s) => s.clone(),
            other => panic!("{} must be plain non-negative decimal text like \"12.5\", received {:?}", name, other),
        }
    };
    let decimals = match &args[3] {
        Value::Int(i) => *i,
        other => panic!("decimals must be a whole number from 0 to 12, received {:?}", other),
    };
    yield_per_area_to_value(&yield_per_hectare(
        &text(&args[0], "quantity"),
        &text(&args[1], "area"),
        args[2].as_str(),
        decimals,
    ))
}