use super::agri_stocking_rate_data::{LivestockUnitCoefficient, LIVESTOCK_UNITS}; use super::funejson::Value; use super::math_round_div::round_div; use super::math_round_float::round_float; const MAX_COUNT: i64 = 10_000_000; /// Livestock units and the stocking rate per hectare. /// /// Coefficients are held in thousandths of a livestock unit, so a herd's total /// is an exact integer and only the rate per hectare is rounded. Adding float /// coefficients drifts: 3 x 0.7 is 2.0999999999999996, which puts 3 yearlings /// on 12 ha at 0.17 LU/ha instead of 0.18 (0.175 rounded half up). /// /// # Panics /// Panics on an unknown scheme or category, a count out of range, or an area /// that is not positive. pub fn stocking_rate(scheme: &str, animals: &[LivestockCount], area_hectares: f64) -> StockingRate { let table: Vec<&LivestockUnitCoefficient> = LIVESTOCK_UNITS.iter().filter(|r| r.scheme == scheme).collect(); if table.is_empty() { panic!("unknown scheme \"{}\": use eurostat or uk-countryside-stewardship", scheme); } if !area_hectares.is_finite() || area_hectares <= 0.0 || area_hectares > 1_000_000.0 { panic!("areaHectares must be a finite number greater than 0 and at most 1000000, received {}", area_hectares); } let area = (round_float(area_hectares, 4) * 10000.0).round() as i64; if area < 1 { panic!("areaHectares must be at least 0.0001 hectares, received {}", area_hectares); } let mut total: i64 = 0; let mut lines: Vec = Vec::with_capacity(animals.len()); for (i, animal) in animals.iter().enumerate() { let row = match table.iter().find(|r| r.category == animal.category) { Some(r) => r, None => panic!("unknown category \"{}\" in scheme {}", animal.category, scheme), }; if animal.count < 0 || animal.count > MAX_COUNT { panic!("animal {}: count must be a whole number from 0 to 10000000, received {}", i + 1, animal.count); } let thousandths = animal.count * row.thousandths; total += thousandths; lines.push(LivestockUnitLine { category: animal.category.clone(), count: animal.count, coefficient: row.thousandths as f64 / 1000.0, livestock_units: thousandths as f64 / 1000.0, }); } // LU/ha in hundredths = (total / 1000) / (area / 10000) x 100. StockingRate { livestock_units: total as f64 / 1000.0, units_per_hectare: round_div(total * 1000, area, "half-up") as f64 / 100.0, lines, } } pub fn stocking_rate_to_value(s: &StockingRate) -> Value { let lines = s .lines .iter() .map(|l| { Value::obj(vec![ ("category", Value::str(&l.category)), ("count", Value::Int(l.count)), ("coefficient", Value::Float(l.coefficient)), ("livestockUnits", Value::Float(l.livestock_units)), ]) }) .collect(); Value::obj(vec![ ("livestockUnits", Value::Float(s.livestock_units)), ("unitsPerHectare", Value::Float(s.units_per_hectare)), ("lines", Value::Arr(lines)), ]) } pub fn fune_vector(args: &[Value]) -> Value { let animals: Vec = args[1] .as_arr() .iter() .enumerate() .map(|(i, v)| { let count = match v.get("count") { Value::Int(n) => *n, other => panic!("animal {}: count must be a whole number from 0 to 10000000, received {:?}", i + 1, other), }; LivestockCount { category: v.get("category").as_str().to_string(), count, } }) .collect(); stocking_rate_to_value(&stocking_rate(args[0].as_str(), &animals, args[2].as_f64())) }