use super::funejson::Value; use super::inventory_safety_stock_data::Z_SCORES; fn amount(name: &str, value: Option, 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::>() .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 { 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]))) }