use super::funejson::Value; fn check_range(name: &str, value: i64, low: i64, high: i64) { if value < low || value > high { panic!("{} must be a whole number from {} to {}, received {}", name, low, high, value); } } /// num / den rounded half-up, for num >= 0 and den > 0. fn half_up(num: i128, den: i128) -> i128 { (2 * num + den) / (2 * den) } fn half_away_from_zero(num: i128, den: i128) -> i128 { let magnitude = half_up(num.abs(), den); if num < 0 { -magnitude } else { magnitude } } /// A weight-based dose, capped at the maximum single dose and turned into a /// volume the measuring device can show. Every quantity is a whole number of /// micrograms, microlitres or grams and every step is an exact fraction (i128), /// so a 0.1 mL step never drifts. The volume is the nearest whole number of /// increments, a tie going down, and never one that would deliver more than /// the maximum single dose. /// /// # Panics /// Panics on an argument out of range, or a dose smaller than one increment. pub fn weight_based_dose( weight_grams: i64, dose_per_kg_micrograms: i64, max_single_dose_micrograms: Option, strength_micrograms: i64, strength_volume_microlitres: i64, measuring_increment_microlitres: i64, ) -> WeightBasedDose { check_range("weightGrams", weight_grams, 200, 500000); check_range("dosePerKgMicrograms", dose_per_kg_micrograms, 1, 1000000); if let Some(max) = max_single_dose_micrograms { check_range("maxSingleDoseMicrograms", max, 1, 100000000); } check_range("strengthMicrograms", strength_micrograms, 1, 100000000); check_range("strengthVolumeMicrolitres", strength_volume_microlitres, 1, 1000000); check_range("measuringIncrementMicrolitres", measuring_increment_microlitres, 1, 100000); let per_kg = weight_grams as i128 * dose_per_kg_micrograms as i128; // micrograms x 1000 let max = max_single_dose_micrograms.map(|m| m as i128); let capped = matches!(max, Some(m) if per_kg > m * 1000); // The target dose as the exact fraction target_num / target_den micrograms. let (target_num, target_den) = if capped { (max.unwrap(), 1) } else { (per_kg, 1000) }; let strength = strength_micrograms as i128; let volume = strength_volume_microlitres as i128; let increment = measuring_increment_microlitres as i128; // Increments needed: target x volume / (strength x increment), nearest, a tie down. let steps_num = target_num * volume; let steps_den = target_den * strength * increment; let mut steps = steps_num / steps_den; if 2 * (steps_num % steps_den) > steps_den { steps += 1; } // Rounding up to the nearest step must not take the dose over the maximum. if let Some(m) = max { if steps * increment * strength > m * volume { steps -= 1; } } if steps == 0 { panic!("dose is less than one measuring increment; use a more dilute product or a finer measure"); } let delivered_num = steps * increment * strength; // micrograms x volume WeightBasedDose { calculated_micrograms: half_up(per_kg, 1000) as i64, capped_at_maximum: capped, target_micrograms: half_up(target_num, target_den) as i64, volume_microlitres: (steps * increment) as i64, delivered_micrograms: half_up(delivered_num, volume) as i64, difference_basis_points: half_away_from_zero( (delivered_num * target_den - target_num * volume) * 10000, target_num * volume, ) as i64, } } pub fn weight_based_dose_to_value(result: &WeightBasedDose) -> Value { Value::obj(vec![ ("calculatedMicrograms", Value::Int(result.calculated_micrograms)), ("cappedAtMaximum", Value::Bool(result.capped_at_maximum)), ("targetMicrograms", Value::Int(result.target_micrograms)), ("volumeMicrolitres", Value::Int(result.volume_microlitres)), ("deliveredMicrograms", Value::Int(result.delivered_micrograms)), ("differenceBasisPoints", Value::Int(result.difference_basis_points)), ]) } const RANGES: [(&str, i64, i64); 6] = [ ("weightGrams", 200, 500000), ("dosePerKgMicrograms", 1, 1000000), ("maxSingleDoseMicrograms", 1, 100000000), ("strengthMicrograms", 1, 100000000), ("strengthVolumeMicrolitres", 1, 1000000), ("measuringIncrementMicrolitres", 1, 100000), ]; /// Refuse what an i64 cannot hold (a fraction, a string) with the wording /// TypeScript and Python use, rather than let the conversion change it. fn int_arg(args: &[Value], index: usize) -> i64 { let (name, low, high) = RANGES[index]; match &args[index] { Value::Int(v) => *v, other => panic!("{} must be a whole number from {} to {}, received {:?}", name, low, high, other), } } pub fn fune_vector(args: &[Value]) -> Value { let max = if args[2].is_null() { None } else { Some(int_arg(args, 2)) }; weight_based_dose_to_value(&weight_based_dose( int_arg(args, 0), int_arg(args, 1), max, int_arg(args, 3), int_arg(args, 4), int_arg(args, 5), )) }