use super::funejson::Value; use super::units_convert::convert_units; // 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::().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, )) }