use super::funejson::Value; const MAX_SAFE: i128 = 9_007_199_254_740_991; fn gcd(a: i64, b: i64) -> i64 { let (mut a, mut b) = (a.abs(), b.abs()); while b != 0 { let t = a % b; a = b; b = t; } a } fn fraction(numerator: i64, denominator: i64) -> Fraction { let g = match gcd(numerator, denominator) { 0 => 1, g => g, }; Fraction { numerator: numerator / g, denominator: denominator / g, } } /// Mean, median and mode of a list of integers. /// /// Mean and median are exact fractions; their float forms come from a single /// correctly rounded division, so every language returns the same double. /// /// # Panics /// Panics on an empty list, a value outside the safe integer range, or a sum /// or median outside it. pub fn mean_median_mode(values: &[i64]) -> CentralTendency { if values.is_empty() { panic!("values must not be empty"); } let mut total: i128 = 0; for &v in values { if (v as i128).abs() > MAX_SAFE { panic!( "values must be safe integers (magnitude at most 9007199254740991), received {}", v ); } total += v as i128; } if total.abs() > MAX_SAFE { panic!("sum exceeds the safe integer range"); } let sum = total as i64; let count = values.len(); let mut sorted = values.to_vec(); sorted.sort_unstable(); let median = if count % 2 == 1 { Fraction { numerator: sorted[(count - 1) / 2], denominator: 1, } } else { let pair = sorted[count / 2 - 1] as i128 + sorted[count / 2] as i128; if pair % 2 == 0 { Fraction { numerator: (pair / 2) as i64, denominator: 1, } } else { if pair.abs() > MAX_SAFE { panic!("median exceeds the safe integer range"); } Fraction { numerator: pair as i64, denominator: 2, } } }; // Runs of equal values in the sorted copy give frequencies in ascending // value order, so the modes come out sorted without a second sort. let mut modes: Vec = Vec::new(); let mut mode_frequency = 0usize; let mut i = 0usize; while i < count { let mut j = i; while j < count && sorted[j] == sorted[i] { j += 1; } let run = j - i; if run > mode_frequency { mode_frequency = run; modes = vec![sorted[i]]; } else if run == mode_frequency { modes.push(sorted[i]); } i = j; } CentralTendency { count: count as i64, sum, mean: fraction(sum, count as i64), mean_value: sum as f64 / count as f64 + 0.0, median_value: median.numerator as f64 / median.denominator as f64 + 0.0, median, modes, mode_frequency: mode_frequency as i64, } } pub fn fraction_to_value(f: &Fraction) -> Value { Value::obj(vec![ ("numerator", Value::Int(f.numerator)), ("denominator", Value::Int(f.denominator)), ]) } pub fn central_tendency_to_value(c: &CentralTendency) -> Value { Value::obj(vec![ ("count", Value::Int(c.count)), ("sum", Value::Int(c.sum)), ("mean", fraction_to_value(&c.mean)), ("meanValue", Value::Float(c.mean_value)), ("median", fraction_to_value(&c.median)), ("medianValue", Value::Float(c.median_value)), ("modes", Value::Arr(c.modes.iter().map(|m| Value::Int(*m)).collect())), ("modeFrequency", Value::Int(c.mode_frequency)), ]) } 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); } } let values: Vec = args[0].as_arr().iter().map(|v| v.as_i64()).collect(); central_tendency_to_value(&mean_median_mode(&values)) }