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,
))
}