Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
use super::funejson::Value; ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
/// 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<String>) -> Value {
match value {
Some(s) => Value::str(s),
None => Value::Null,
}
}
fn opt_int(value: Option<i64>) -> 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<GradeBoundary> = 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))
}