use super::funejson::Value; use super::math_round_div::round_div; use super::math_round_float::round_float; const NUTRIENTS: [&str; 3] = ["nitrogen", "phosphate", "potash"]; fn tenths_of_percent(name: &str, value: f64) -> i64 { if !value.is_finite() || value < 0.0 || value > 100.0 { panic!("analysis {} must be a finite percentage from 0 to 100, received {}", name, value); } (round_float(value, 1) * 10.0).round() as i64 } /// Product rate for a fertiliser, set by the limiting nutrient. /// /// For each targeted nutrient the product needed is target / analysis; the /// rate is the smallest of those, so no nutrient is applied beyond its target, /// and the others show as a shortfall. The arithmetic is exact in tenths (of a /// percent and of a kilogram) and the rate is rounded once, half up. /// /// # Panics /// Panics on an analysis, target or area out of range, no target, or a /// product with none of the targeted nutrients. pub fn fertiliser_application(analysis: &NutrientAnalysis, targets: &NutrientTargets, area_hectares: f64) -> FertiliserPlan { let a = [ tenths_of_percent("nitrogen", analysis.nitrogen), tenths_of_percent("phosphate", analysis.phosphate), tenths_of_percent("potash", analysis.potash), ]; if a[0] + a[1] + a[2] > 1000 { panic!("analysis must not total more than 100%"); } let t = [targets.nitrogen, targets.phosphate, targets.potash]; for (i, v) in t.iter().enumerate() { if *v < 0 || *v > 1000 { panic!("target {} must be a whole number of kg/ha from 0 to 1000, received {}", NUTRIENTS[i], v); } } if !area_hectares.is_finite() || area_hectares <= 0.0 || area_hectares > 10000.0 { panic!("areaHectares must be a finite number greater than 0 and at most 10000, 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); } if t[0] + t[1] + t[2] == 0 { panic!("at least one target must be above zero"); } // Least product: target_i / a_i smallest, compared by cross-multiplying. let mut limit: Option = None; for i in 0..3 { if t[i] == 0 || a[i] == 0 { continue; } match limit { Some(l) if t[i] * a[l] >= t[l] * a[i] => {} _ => limit = Some(i), } } let limit = match limit { Some(l) => l, None => panic!("the product contains none of the targeted nutrients"), }; // kg/ha in tenths = target x 100 / (a / 10) x 10. let rate = round_div(t[limit] * 10000, a[limit], "half-up"); let s: Vec = a.iter().map(|ai| round_div(rate * ai, 1000, "half-up")).collect(); FertiliserPlan { limiting_nutrient: NUTRIENTS[limit].to_string(), product_kg_per_hectare: rate as f64 / 10.0, total_product_kg: round_div(rate * area, 10000, "half-up") as f64 / 10.0, supplied: NutrientSupply { nitrogen: s[0] as f64 / 10.0, phosphate: s[1] as f64 / 10.0, potash: s[2] as f64 / 10.0, }, balance: NutrientSupply { nitrogen: (s[0] - t[0] * 10) as f64 / 10.0, phosphate: (s[1] - t[1] * 10) as f64 / 10.0, potash: (s[2] - t[2] * 10) as f64 / 10.0, }, } } pub fn nutrient_supply_to_value(s: &NutrientSupply) -> Value { Value::obj(vec![ ("nitrogen", Value::Float(s.nitrogen)), ("phosphate", Value::Float(s.phosphate)), ("potash", Value::Float(s.potash)), ]) } pub fn fertiliser_plan_to_value(p: &FertiliserPlan) -> Value { Value::obj(vec![ ("limitingNutrient", Value::str(&p.limiting_nutrient)), ("productKgPerHectare", Value::Float(p.product_kg_per_hectare)), ("totalProductKg", Value::Float(p.total_product_kg)), ("supplied", nutrient_supply_to_value(&p.supplied)), ("balance", nutrient_supply_to_value(&p.balance)), ]) } fn target(v: &Value, name: &str) -> i64 { match v { Value::Int(i) => *i, other => panic!("target {} must be a whole number of kg/ha from 0 to 1000, received {:?}", name, other), } } pub fn fune_vector(args: &[Value]) -> Value { let analysis = NutrientAnalysis { nitrogen: args[0].get("nitrogen").as_f64(), phosphate: args[0].get("phosphate").as_f64(), potash: args[0].get("potash").as_f64(), }; let targets = NutrientTargets { nitrogen: target(args[1].get("nitrogen"), "nitrogen"), phosphate: target(args[1].get("phosphate"), "phosphate"), potash: target(args[1].get("potash"), "potash"), }; fertiliser_plan_to_value(&fertiliser_application(&analysis, &targets, args[2].as_f64())) }