Functional Weave
Code in Python

inventory.safety-stock@1.0.0

impl/rust.rs

4,045 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
use super::inventory_safety_stock_data::Z_SCORES;  ← this capability’s own data, compiled from data/z-scores.json into the same file by fune build

fn amount(name: &str, value: Option<f64>, method: &str) -> f64 {
    match value {
        None => panic!("{} needs {}", method, name),
        Some(v) if !v.is_finite() || v < 0.0 => {
            panic!("{} must be a finite number, not negative, received {}", name, v)
        }
        Some(v) => v,
    }
}

// Six decimal places, half away from zero, then up to whole units: float noise
// such as 55.00000000000001 must not cost a whole extra unit.
fn whole_units_up(x: f64) -> i64 {
    let y = x * 1e6;
    let mut micro = y.floor();
    if y - micro >= 0.5 {
        micro += 1.0;
    }
    (micro as i64 + 999_999).div_euclid(1_000_000)
}

/// Safety stock in whole units, by the service-level or max-minus-average method.
///
/// # Panics
/// Panics on an unknown method or service level, a missing or negative input,
/// or a worst case below the average case.
pub fn safety_stock(input: &SafetyStockInput) -> i64 {
    let method = input.method.as_str();
    let lead_time = amount("leadTime", Some(input.lead_time), method);
    match method {
        "service-level" => {
            let level = match input.service_level_basis_points {
                Some(l) => l,
                None => panic!("service-level needs serviceLevelBasisPoints"),
            };
            let row = match Z_SCORES.iter().find(|r| r.service_level_basis_points == level) {
                Some(r) => r,
                None => panic!(
                    "no z-score for a service level of {} basis points: use one of {}",
                    level,
                    Z_SCORES
                        .iter()
                        .map(|r| r.service_level_basis_points.to_string())
                        .collect::<Vec<_>>()
                        .join(", ")
                ),
            };
            let sd = amount("demandStdDev", input.demand_std_dev, method);
            let mut variance = lead_time * sd * sd;
            let sl = amount("leadTimeStdDev", Some(input.lead_time_std_dev.unwrap_or(0.0)), method);
            if sl > 0.0 {
                let d = amount("averageDemand", input.average_demand, "service-level with a varying lead time");
                variance = variance + d * d * sl * sl;
            }
            whole_units_up((row.z_ten_thousandths as f64 / 10000.0) * variance.sqrt())
        }
        "max-minus-average" => {
            let max_demand = amount("maxDemand", input.max_demand, method);
            let max_lead_time = amount("maxLeadTime", input.max_lead_time, method);
            let average_demand = amount("averageDemand", input.average_demand, method);
            let worst = max_demand * max_lead_time;
            let usual = average_demand * lead_time;
            if worst < usual {
                panic!(
                    "maxDemand x maxLeadTime ({}) must not be less than averageDemand x leadTime ({})",
                    worst, usual
                );
            }
            whole_units_up(worst - usual)
        }
        other => panic!("unknown safety stock method \"{}\"", other),
    }
}

fn optional_f64(v: &Value) -> Option<f64> {
    if v.is_null() {
        None
    } else {
        Some(v.as_f64())
    }
}

pub fn safety_stock_input_from_value(v: &Value) -> SafetyStockInput {
    let level = v.get("serviceLevelBasisPoints");
    SafetyStockInput {
        method: v.get("method").as_str().to_string(),
        service_level_basis_points: if level.is_null() { None } else { Some(level.as_i64()) },
        demand_std_dev: optional_f64(v.get("demandStdDev")),
        lead_time: v.get("leadTime").as_f64(),
        average_demand: optional_f64(v.get("averageDemand")),
        lead_time_std_dev: optional_f64(v.get("leadTimeStdDev")),
        max_demand: optional_f64(v.get("maxDemand")),
        max_lead_time: optional_f64(v.get("maxLeadTime")),
    }
}

pub fn fune_vector(args: &[Value]) -> Value {
    Value::Int(safety_stock(&safety_stock_input_from_value(&args[0])))
}