use super::funejson::Value; use super::math_round_div::round_div; const MAX_SAFE: i128 = 9_007_199_254_740_991; /// Weighted average of integers, rounded once at the end. /// /// The numerator and denominator are exact integers, so the rounding mode the /// caller names is the only rounding that ever happens. /// /// # Panics /// Panics on mismatched or empty lists, a negative weight, all-zero weights, /// decimals outside 0..=9, or a weighted sum too large to hold exactly. pub fn weighted_average(values: &[i64], weights: &[i64], decimals: i64, mode: &str) -> f64 { if values.len() != weights.len() { panic!( "values and weights must be the same length, received {} and {}", values.len(), weights.len() ); } if values.is_empty() { panic!("values must not be empty"); } if !(0..=9).contains(&decimals) { panic!("decimals must be a whole number from 0 to 9, received {}", decimals); } let mut numerator: i128 = 0; let mut denominator: i128 = 0; for (&v, &w) in values.iter().zip(weights) { if (v as i128).abs() > MAX_SAFE { panic!("values must be integers, received {}", v); } if (w as i128).abs() > MAX_SAFE { panic!("weights must be integers, received {}", w); } if w < 0 { panic!("weights must not be negative, received {}", w); } numerator += v as i128 * w as i128; denominator += w as i128; } if denominator == 0 { panic!("weights must not all be zero"); } let scale = 10i128.pow(decimals as u32); let scaled = numerator * scale; if scaled.abs() > MAX_SAFE || denominator > MAX_SAFE { panic!("weighted sum is too large to average exactly; lower decimals or rescale the values"); } round_div(scaled as i64, denominator as i64, mode) as f64 / scale as f64 + 0.0 } pub fn fune_vector(args: &[Value]) -> Value { // Refuse what the typed signature cannot hold, with the wording TypeScript // and Python use, rather than let the conversion below quietly change it. for v in args[0].as_arr() { if !matches!(v, Value::Int(_)) && !matches!(v, Value::Float(f) if f.fract() == 0.0) { panic!("values must be integers, received {:?}", v); } } for v in args[1].as_arr() { if !matches!(v, Value::Int(_)) && !matches!(v, Value::Float(f) if f.fract() == 0.0) { panic!("weights must be integers, received {:?}", v); } } let values: Vec = args[0].as_arr().iter().map(|v| v.as_i64()).collect(); let weights: Vec = args[1].as_arr().iter().map(|v| v.as_i64()).collect(); Value::Float(weighted_average( &values, &weights, args[2].as_i64(), args[3].as_str(), )) }