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
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<i64>,
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),
))
}