Functional Weave
Code in Rust

health.dose-weight-based@1.0.1

impl/rust.rs

5,178 bytes · the Rust implementation · view raw

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