use super::funejson::Value; use super::math_rational::{add_rational, multiply_rational, rational, rational_to_integer, rational_to_value}; /// Weighted percentage across components, summed exactly and rounded once. /// /// Each component adds mark x weight / (out_of x 100) percent: mark/out_of of /// the component, times weight/10000 of the whole, times 100 for a percentage. /// /// # Panics /// Panics on an empty list, a mark outside 0 to out_of, weights that do not /// total 10000, decimals outside 0 to 6, or an unknown rounding mode. pub fn weighted_grade(components: &[GradeComponent], decimals: i64, mode: &str) -> WeightedGrade { if components.is_empty() { panic!("components must not be empty"); } if !(0..=6).contains(&decimals) { panic!("decimals must be a whole number from 0 to 6, received {}", decimals); } let mut total = rational(0, 1); let mut weights: i64 = 0; for c in components { if c.out_of < 1 { panic!("outOf must be a whole number of 1 or more, received {} for {}", c.out_of, c.name); } if c.mark < 0 || c.mark > c.out_of { panic!( "mark must be a whole number from 0 to outOf ({}), received {} for {}", c.out_of, c.mark, c.name ); } if c.weight_basis_points < 0 { panic!( "weightBasisPoints must be a whole number of 0 or more, received {} for {}", c.weight_basis_points, c.name ); } weights += c.weight_basis_points; total = add_rational(&total, &rational(c.mark * c.weight_basis_points, c.out_of * 100)); } if weights != 10000 { panic!("weights must add up to 10000 basis points (100%), received {}", weights); } let scaled = rational_to_integer(&multiply_rational(&total, &rational(10i64.pow(decimals as u32), 1)), mode); WeightedGrade { percent: total, scaled, decimals } } fn whole(value: &Value, message: &str) -> i64 { match value { Value::Float(f) if f.fract() != 0.0 => panic!("{}, received {}", message, f), _ => value.as_i64(), } } pub fn weighted_grade_to_value(g: &WeightedGrade) -> Value { Value::obj(vec![ ("percent", rational_to_value(&g.percent)), ("scaled", Value::Int(g.scaled)), ("decimals", Value::Int(g.decimals)), ]) } pub fn fune_vector(args: &[Value]) -> Value { let components: Vec = args[0] .as_arr() .iter() .map(|v| { let out_of = whole(v.get("outOf"), "outOf must be a whole number of 1 or more"); GradeComponent { name: v.get("name").as_str().to_string(), mark: whole(v.get("mark"), &format!("mark must be a whole number from 0 to outOf ({})", out_of)), out_of, weight_basis_points: whole(v.get("weightBasisPoints"), "weightBasisPoints must be a whole number of 0 or more"), } }) .collect(); let decimals = whole(&args[1], "decimals must be a whole number from 0 to 6"); weighted_grade_to_value(&weighted_grade(&components, decimals, args[2].as_str())) }