Functional Weave
Code in Rust

education.degree-classification@1.0.0

impl/rust.rs

4,519 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
use super::education_degree_classification_data::DEGREE_CLASSES;  ← this capability’s own data, compiled from data/degree-classes.json into the same file by fune build
use super::math_rational::{
    add_rational, divide_rational, multiply_rational, rational, rational_to_integer, rational_to_value,
};

/// Honours classification: credit-weighted mean per level, levels combined by
/// their weights, rounded once, then banded.
///
/// # Panics
/// Panics on a mark outside 0 to 100, credits below 1, an empty, duplicated or
/// all-zero weighting, a weighted level with no modules, decimals outside 0 to
/// 6, or an unknown rounding mode.
pub fn degree_classification(
    modules: &[ModuleMark],
    level_weights: &[LevelWeight],
    decimals: i64,
    mode: &str,
) -> DegreeClassification {
    if level_weights.is_empty() {
        panic!("levelWeights must name at least one level");
    }
    if !(0..=6).contains(&decimals) {
        panic!("decimals must be a whole number from 0 to 6, received {}", decimals);
    }
    for m in modules {
        if m.mark < 0 || m.mark > 100 {
            panic!("mark must be a whole number from 0 to 100, received {}", m.mark);
        }
        if m.credits < 1 {
            panic!("credits must be a whole number of 1 or more, received {}", m.credits);
        }
    }
    let mut total_weight: i64 = 0;
    let mut sum = rational(0, 1);
    let mut counted_credits: i64 = 0;
    for (i, lw) in level_weights.iter().enumerate() {
        if lw.weight < 0 {
            panic!("weight must be a whole number of 0 or more, received {} for level {}", lw.weight, lw.level);
        }
        if level_weights[..i].iter().any(|o| o.level == lw.level) {
            panic!("level {} appears twice in levelWeights", lw.level);
        }
        if lw.weight == 0 {
            continue;
        }
        let mut marks: i64 = 0;
        let mut credits: i64 = 0;
        for m in modules.iter().filter(|m| m.level == lw.level) {
            marks += m.mark * m.credits;
            credits += m.credits;
        }
        if credits == 0 {
            panic!("no modules at level {}, which levelWeights counts", lw.level);
        }
        counted_credits += credits;
        total_weight += lw.weight;
        sum = add_rational(&sum, &multiply_rational(&rational(marks, credits), &rational(lw.weight, 1)));
    }
    if total_weight == 0 {
        panic!("levelWeights must not all be zero");
    }
    let average = divide_rational(&sum, &rational(total_weight, 1));
    let scale = 10i64.pow(decimals as u32);
    let scaled = rational_to_integer(&multiply_rational(&average, &rational(scale, 1)), mode);
    let mut bands: Vec<_> = DEGREE_CLASSES.iter().collect();
    bands.sort_by(|a, b| b.min_mark.cmp(&a.min_mark));
    let band = bands
        .iter()
        .find(|b| scaled >= b.min_mark * scale)
        .copied()
        .unwrap_or(bands[bands.len() - 1]);
    DegreeClassification {
        average,
        scaled,
        decimals,
        classification: band.classification.to_string(),
        label: band.label.to_string(),
        counted_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 degree_classification_to_value(d: &DegreeClassification) -> Value {
    Value::obj(vec![
        ("average", rational_to_value(&d.average)),
        ("scaled", Value::Int(d.scaled)),
        ("decimals", Value::Int(d.decimals)),
        ("classification", Value::str(&d.classification)),
        ("label", Value::str(&d.label)),
        ("countedCredits", Value::Int(d.counted_credits)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    let modules: Vec<ModuleMark> = args[0]
        .as_arr()
        .iter()
        .map(|v| ModuleMark {
            level: v.get("level").as_i64(),
            mark: whole(v.get("mark"), "mark must be a whole number from 0 to 100"),
            credits: whole(v.get("credits"), "credits must be a whole number of 1 or more"),
        })
        .collect();
    let weights: Vec<LevelWeight> = args[1]
        .as_arr()
        .iter()
        .map(|v| LevelWeight {
            level: v.get("level").as_i64(),
            weight: whole(v.get("weight"), "weight must be a whole number of 0 or more"),
        })
        .collect();
    let decimals = whole(&args[2], "decimals must be a whole number from 0 to 6");
    degree_classification_to_value(&degree_classification(&modules, &weights, decimals, args[3].as_str()))
}