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::dates_add_days::parse_iso_date; ← from dates.add-days ^1.0.0 · built alongside by fune
use super::education_attendance_rate_data::{
ABSENCE_THRESHOLDS, ABSENCE_THRESHOLDS_HISTORY, ABSENCE_THRESHOLDS_HORIZON,
};
use super::math_round_div::round_div; ← from math.round-div ^1.0.0 · built alongside by fune
fn threshold_on(measure: &str, on_date: &str) -> Option<i64> {
ABSENCE_THRESHOLDS
.iter()
.find(|r| r.measure == measure && on_date >= r.valid_from && r.valid_to.map_or(true, |to| on_date <= to))
.map(|r| r.basis_points)
}
/// Attendance and absence in basis points, and the DfE absence flags.
///
/// The flags compare missed x 10000 with possible x threshold, exactly: a
/// rate of 9.996% rounds to 10.00% but is not persistent absence.
///
/// # Panics
/// Panics on no possible sessions, missed sessions outside 0 to possible, a
/// malformed date, or a date before the persistent absence definition.
pub fn attendance_rate(possible_sessions: i64, missed_sessions: i64, on_date: &str) -> AttendanceRate {
if possible_sessions < 1 {
panic!("possibleSessions must be a whole number of 1 or more, received {}", possible_sessions);
}
if missed_sessions < 0 || missed_sessions > possible_sessions {
panic!(
"missedSessions must be a whole number from 0 to possibleSessions ({}), received {}",
possible_sessions, missed_sessions
);
}
parse_iso_date(on_date);
let persistent = match threshold_on("persistent", on_date) {
Some(bp) => bp,
None => {
if let Some(horizon) = ABSENCE_THRESHOLDS_HORIZON {
if ABSENCE_THRESHOLDS_HISTORY != "full" && on_date < horizon {
panic!(
"no persistent absence definition on {}: this build was installed with history={}, so it only carries rules from {}. Reinstall with history=full for earlier years.",
on_date, ABSENCE_THRESHOLDS_HISTORY, horizon
);
}
}
panic!("no persistent absence definition on {}", on_date)
}
};
let severe = threshold_on("severe", on_date);
let missed = missed_sessions as i128 * 10000;
let possible = possible_sessions as i128;
let absence = round_div(missed_sessions * 10000, possible_sessions, "half-up");
AttendanceRate {
attendance_basis_points: 10000 - absence,
absence_basis_points: absence,
persistently_absent: missed >= possible * persistent as i128,
severely_absent: severe.map(|bp| missed >= possible * bp as i128),
}
}
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 attendance_rate_to_value(r: &AttendanceRate) -> Value {
Value::obj(vec![
("attendanceBasisPoints", Value::Int(r.attendance_basis_points)),
("absenceBasisPoints", Value::Int(r.absence_basis_points)),
("persistentlyAbsent", Value::Bool(r.persistently_absent)),
("severelyAbsent", match r.severely_absent { Some(b) => Value::Bool(b), None => Value::Null }),
])
}
pub fn fune_vector(args: &[Value]) -> Value {
let possible = whole(&args[0], "possibleSessions must be a whole number of 1 or more");
let missed = whole(
&args[1],
&format!("missedSessions must be a whole number from 0 to possibleSessions ({})", possible),
);
attendance_rate_to_value(&attendance_rate(possible, missed, args[2].as_str()))
}