use super::funejson::Value; use super::math_round_div::round_div; use super::math_round_float::round_float; 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, 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 = 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 = 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())) }