use super::funejson::Value; use super::education_gpa_data::DEFAULT_GRADE_POINTS; use super::math_round_div::round_div; /// Credit-weighted grade point average in hundredths, rounded once. /// /// Quality points are summed exactly in integers (hundredths x credits), so /// the rounding mode the caller names is the only rounding that happens. /// /// # Panics /// Panics on an empty or duplicated table, a grade not in the table, negative /// credits or points, or an unknown rounding mode. pub fn gpa(courses: &[GpaCourse], grade_points: Option<&[GradePoint]>, mode: &str) -> GpaResult { let table: Vec<(String, Option)> = match grade_points { Some(rows) => rows.iter().map(|r| (r.grade.clone(), r.points)).collect(), None => DEFAULT_GRADE_POINTS.iter().map(|r| (r.grade.to_string(), r.points)).collect(), }; if table.is_empty() { panic!("gradePoints must not be empty; pass null for the default table"); } for (i, (grade, points)) in table.iter().enumerate() { if table[..i].iter().any(|(g, _)| g == grade) { panic!("grade \"{}\" appears twice in gradePoints", grade); } if let Some(p) = points { if *p < 0 { panic!("points must be whole hundredths of 0 or more, received {} for grade \"{}\"", p, grade); } } } let mut quality_points: i64 = 0; let mut gpa_credits: i64 = 0; let mut excluded_credits: i64 = 0; for course in courses { if course.credits < 0 { panic!("credits must be a whole number of 0 or more, received {}", course.credits); } let row = table.iter().find(|(g, _)| *g == course.grade); match row { None => panic!("grade \"{}\" is not in the grade-point table", course.grade), Some((_, None)) => excluded_credits += course.credits, Some((_, Some(p))) => { quality_points += p * course.credits; gpa_credits += course.credits; } } } // Validate the mode even when there is nothing to divide. round_div(0, 1, mode); GpaResult { gpa: if gpa_credits == 0 { None } else { Some(round_div(quality_points, gpa_credits, mode)) }, quality_points, gpa_credits, excluded_credits, } } 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 gpa_result_to_value(r: &GpaResult) -> Value { Value::obj(vec![ ("gpa", match r.gpa { Some(n) => Value::Int(n), None => Value::Null }), ("qualityPoints", Value::Int(r.quality_points)), ("gpaCredits", Value::Int(r.gpa_credits)), ("excludedCredits", Value::Int(r.excluded_credits)), ]) } pub fn fune_vector(args: &[Value]) -> Value { let courses: Vec = args[0] .as_arr() .iter() .map(|v| GpaCourse { grade: v.get("grade").as_str().to_string(), credits: whole(v.get("credits"), "credits must be a whole number of 0 or more"), }) .collect(); let table: Option> = if args[1].is_null() { None } else { Some( args[1] .as_arr() .iter() .map(|v| { let p = v.get("points"); GradePoint { grade: v.get("grade").as_str().to_string(), points: if p.is_null() { None } else { Some(whole(p, "points must be whole hundredths of 0 or more")) }, } }) .collect(), ) }; gpa_result_to_value(&gpa(&courses, table.as_deref(), args[2].as_str())) }