Functional Weave
Code in TypeScript

education.grade-boundaries@1.0.0

impl/rust.rs

3,903 bytes · the Rust implementation · view raw

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))
}