Functional Weave
Code in Rust

agri.growing-degree-days@1.0.0

impl/rust.rs

3,057 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 tenths(value: f64, message: &str) -> i64 {
    if !value.is_finite() || value < -100.0 || value > 100.0 {
        panic!("{}", message);
    }
    (round_float(value, 1) * 10.0).round() as i64
}

/// Growing degree days, McMaster and Wilhelm (1997), in tenths of a degree.
///
/// Each day is (max + min) / 2 - base with the temperatures taken to 0.1 °C,
/// so it is an exact multiple of 0.05 and the total is summed exactly and
/// rounded once. Rounding each day to 0.1 first and adding drifts: three days
/// of 0.05 are 0.15 (0.2 to one place), not 0.3.
///
/// # Panics
/// Panics on a temperature out of range, a day whose min is above its max,
/// an upper cap not above the base, or an unknown method.
pub fn growing_degree_days(days: &[DailyTemperature], base_celsius: f64, upper_celsius: Option<f64>, method: &str) -> DegreeDays {
    let base = tenths(base_celsius, "baseCelsius must be a finite number from -100 to 100");
    let upper = upper_celsius.map(|u| tenths(u, "upperCelsius must be a finite number from -100 to 100"));
    if let Some(u) = upper {
        if u <= base {
            panic!("upperCelsius must be above baseCelsius");
        }
    }
    if method != "average" && method != "clamped" {
        panic!("unknown method \"{}\": use average or clamped", method);
    }
    let cap = |t: i64| match upper {
        Some(u) if t > u => u,
        _ => t,
    };
    let mut daily: Vec<f64> = Vec::with_capacity(days.len());
    let mut total: i64 = 0;
    for (i, day) in days.iter().enumerate() {
        let message = format!("day {}: temperatures must be finite numbers from -100 to 100 °C", i + 1);
        let lo = tenths(day.min, &message);
        let hi = tenths(day.max, &message);
        if lo > hi {
            panic!("day {}: min must not be above max", i + 1);
        }
        let (lo, hi) = (cap(lo), cap(hi));
        let twice = if method == "clamped" {
            lo.max(base) + hi.max(base) - 2 * base
        } else {
            (lo + hi - 2 * base).max(0)
        };
        // twice is 2 x (mean - base) in tenths, so the day is twice / 20 degrees.
        daily.push(twice as f64 / 20.0);
        total += twice;
    }
    DegreeDays {
        total: round_div(total, 2, "half-up") as f64 / 10.0,
        daily,
    }
}

pub fn degree_days_to_value(d: &DegreeDays) -> Value {
    Value::obj(vec![
        ("total", Value::Float(d.total)),
        ("daily", Value::Arr(d.daily.iter().map(|&v| Value::Float(v)).collect())),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    let days: Vec<DailyTemperature> = args[0]
        .as_arr()
        .iter()
        .map(|v| DailyTemperature {
            min: v.get("min").as_f64(),
            max: v.get("max").as_f64(),
        })
        .collect();
    let upper = if args[2].is_null() { None } else { Some(args[2].as_f64()) };
    degree_days_to_value(&growing_degree_days(&days, args[1].as_f64(), upper, args[3].as_str()))
}