use super::funejson::Value; use super::dates_add_days::parse_iso_date; use super::education_attendance_rate_data::{ ABSENCE_THRESHOLDS, ABSENCE_THRESHOLDS_HISTORY, ABSENCE_THRESHOLDS_HORIZON, }; use super::math_round_div::round_div; fn threshold_on(measure: &str, on_date: &str) -> Option { 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())) }