use super::funejson::Value; /// The grade a mark earns from a boundary table, and the gap to the next grade. /// /// Rows are sorted by min_mark here because published tables run highest /// grade first, and a lookup that assumes ascending order hands out the wrong /// grade. /// /// # Panics /// Panics on a mark outside 0 to max_mark, an empty or ambiguous table, or a /// boundary outside 0 to max_mark. pub fn grade_for_mark(mark: i64, max_mark: i64, boundaries: &[GradeBoundary]) -> GradeResult { if max_mark < 1 { panic!("maxMark must be a positive whole number, received {}", max_mark); } if mark < 0 || mark > max_mark { panic!("mark must be a whole number from 0 to maxMark ({}), received {}", max_mark, mark); } if boundaries.is_empty() { panic!("boundaries must not be empty"); } let mut seen: Vec<&str> = Vec::new(); for b in boundaries { if b.grade.is_empty() { panic!("every boundary needs a non-empty grade"); } if b.min_mark < 0 || b.min_mark > max_mark { panic!( "boundary minMark must be a whole number from 0 to maxMark ({}), received {} for grade {}", max_mark, b.min_mark, b.grade ); } if seen.contains(&b.grade.as_str()) { panic!("grade {} appears twice in boundaries", b.grade); } seen.push(&b.grade); } let mut sorted: Vec<&GradeBoundary> = boundaries.iter().collect(); sorted.sort_by_key(|b| b.min_mark); for i in 1..sorted.len() { if sorted[i].min_mark == sorted[i - 1].min_mark { panic!( "grades {} and {} share the boundary {}", sorted[i - 1].grade, sorted[i].grade, sorted[i].min_mark ); } } let mut index: i64 = -1; for (i, b) in sorted.iter().enumerate() { if b.min_mark <= mark { index = i as i64; } } let awarded = if index >= 0 { Some(sorted[index as usize]) } else { None }; let next_index = (index + 1) as usize; let next = if next_index < sorted.len() { Some(sorted[next_index]) } else { None }; GradeResult { grade: awarded.map(|b| b.grade.clone()), min_mark: awarded.map(|b| b.min_mark), next_grade: next.map(|b| b.grade.clone()), marks_to_next: next.map(|b| b.min_mark - mark), } } fn whole(value: &Value, message: &str) -> i64 { match value { Value::Float(f) if f.fract() != 0.0 => panic!("{}, received {}", message, f), Value::Int(_) | Value::Float(_) => value.as_i64(), _ => panic!("{}, received a non-number", message), } } fn opt_str(value: &Option) -> Value { match value { Some(s) => Value::str(s), None => Value::Null, } } fn opt_int(value: Option) -> Value { match value { Some(n) => Value::Int(n), None => Value::Null, } } pub fn grade_result_to_value(r: &GradeResult) -> Value { Value::obj(vec![ ("grade", opt_str(&r.grade)), ("minMark", opt_int(r.min_mark)), ("nextGrade", opt_str(&r.next_grade)), ("marksToNext", opt_int(r.marks_to_next)), ]) } pub fn fune_vector(args: &[Value]) -> Value { let max_mark = whole(&args[1], "maxMark must be a positive whole number"); let mark = whole(&args[0], &format!("mark must be a whole number from 0 to maxMark ({})", max_mark)); let boundaries: Vec = args[2] .as_arr() .iter() .map(|v| GradeBoundary { grade: v.get("grade").as_str().to_string(), min_mark: whole( v.get("minMark"), &format!("boundary minMark must be a whole number from 0 to maxMark ({})", max_mark), ), }) .collect(); grade_result_to_value(&grade_for_mark(mark, max_mark, &boundaries)) }