use super::funejson::Value; use super::math_rational::{compare_rational, divide_rational, multiply_rational, rational, rational_to_integer}; const MAX_SAFE: i64 = (1i64 << 53) - 1; fn minutes(what: &str, value: i64) { if !(0..=MAX_SAFE).contains(&value) { panic!("{} must be a whole number of minutes, not negative, received {}", what, value); } } /// Capacity, load and overload per work centre. Capacity is kept as an exact /// fraction, so a load a fraction of a minute over it is still an overload. /// /// # Panics /// Panics on a duplicate or unknown work centre, negative times, negative /// efficiency, or utilisation outside 0-10000. pub fn work_centre_load(work_centres: &[WorkCentre], loads: &[RoutedLoad]) -> Vec { let mut totals: Vec<(&str, i64)> = Vec::new(); for wc in work_centres { if totals.iter().any(|(id, _)| *id == wc.id) { panic!("duplicate work centre \"{}\"", wc.id); } minutes(&format!("availableMinutes of \"{}\"", wc.id), wc.available_minutes); if !(0..=MAX_SAFE).contains(&wc.efficiency_basis_points) { panic!( "efficiencyBasisPoints of \"{}\" must be a whole number, not negative, received {}", wc.id, wc.efficiency_basis_points ); } if !(0..=10000).contains(&wc.utilisation_basis_points) { panic!( "utilisationBasisPoints of \"{}\" must be a whole number from 0 to 10000, received {}", wc.id, wc.utilisation_basis_points ); } totals.push((&wc.id, 0)); } for load in loads { let slot = match totals.iter_mut().find(|(id, _)| *id == load.work_centre) { Some(slot) => slot, None => panic!("a load names work centre \"{}\", which is not in the list", load.work_centre), }; minutes(&format!("a load on \"{}\"", load.work_centre), load.minutes); let sum = slot.1 + load.minutes; if sum > MAX_SAFE { panic!("the load on \"{}\" exceeds 2^53 - 1 minutes", load.work_centre); } slot.1 = sum; } work_centres .iter() .zip(totals.iter()) .map(|(wc, (_, load_minutes))| { let capacity = multiply_rational( &rational(wc.available_minutes, 1), &multiply_rational( &rational(wc.efficiency_basis_points, 10000), &rational(wc.utilisation_basis_points, 10000), ), ); let load = rational(*load_minutes, 1); let capacity_minutes = rational_to_integer(&capacity, "down"); CapacityLoad { work_centre: wc.id.clone(), capacity_minutes, load_minutes: *load_minutes, spare_minutes: capacity_minutes - load_minutes, load_basis_points: if capacity.numerator == 0 { None } else { Some(rational_to_integer( &multiply_rational(÷_rational(&load, &capacity), &rational(10000, 1)), "half-up", )) }, overloaded: compare_rational(&load, &capacity) > 0, } }) .collect() } pub fn capacity_load_to_value(c: &CapacityLoad) -> Value { Value::obj(vec![ ("workCentre", Value::str(&c.work_centre)), ("capacityMinutes", Value::Int(c.capacity_minutes)), ("loadMinutes", Value::Int(c.load_minutes)), ("spareMinutes", Value::Int(c.spare_minutes)), ("loadBasisPoints", c.load_basis_points.map(Value::Int).unwrap_or(Value::Null)), ("overloaded", Value::Bool(c.overloaded)), ]) } pub fn fune_vector(args: &[Value]) -> Value { let centres: Vec = args[0] .as_arr() .iter() .map(|v| { let id = v.get("id").as_str(); if let Value::Float(f) = v.get("availableMinutes") { panic!("availableMinutes of \"{}\" must be a whole number of minutes, not negative, received {}", id, f); } if let Value::Float(f) = v.get("efficiencyBasisPoints") { panic!("efficiencyBasisPoints of \"{}\" must be a whole number, not negative, received {}", id, f); } if let Value::Float(f) = v.get("utilisationBasisPoints") { panic!("utilisationBasisPoints of \"{}\" must be a whole number from 0 to 10000, received {}", id, f); } WorkCentre { id: id.to_string(), available_minutes: v.get("availableMinutes").as_i64(), efficiency_basis_points: v.get("efficiencyBasisPoints").as_i64(), utilisation_basis_points: v.get("utilisationBasisPoints").as_i64(), } }) .collect(); let loads: Vec = args[1] .as_arr() .iter() .map(|v| { let wc = v.get("workCentre").as_str(); if let Value::Float(f) = v.get("minutes") { panic!("a load on \"{}\" must be a whole number of minutes, not negative, received {}", wc, f); } RoutedLoad { work_centre: wc.to_string(), minutes: v.get("minutes").as_i64(), } }) .collect(); Value::Arr(work_centre_load(¢res, &loads).iter().map(capacity_load_to_value).collect()) }