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(°ree_classification(&modules, &weights, decimals, args[3].as_str()))
}