use super::funejson::Value; use super::math_integer_sqrt::integer_sqrt; use super::money_amount::{assert_same_currency, money_from_value, Money}; const MAX_SAFE: i64 = (1i64 << 53) - 1; /// Economic batch quantity, sqrt(2DSP / (H(P - D))), in whole units. /// /// The root is never taken in floating point: the floor is an exact integer /// square root and the rounding decision compares whole numbers. /// /// # Panics /// Panics on negative demand or set-up cost, a production rate not above /// demand, a holding cost that is not positive, mixed currencies, an unknown /// mode, or 8DSP beyond 2^63 - 1. pub fn economic_batch_quantity( annual_demand: i64, annual_production_rate: i64, setup_cost: &Money, holding_cost: &Money, mode: &str, ) -> i64 { if !(0..=MAX_SAFE).contains(&annual_demand) { panic!("annualDemand must be a whole number of units, not negative, received {}", annual_demand); } if annual_production_rate <= annual_demand || annual_production_rate > MAX_SAFE { panic!( "annualProductionRate must be a whole number greater than annualDemand, received {}", annual_production_rate ); } assert_same_currency(setup_cost, holding_cost); if setup_cost.minor < 0 { panic!("setupCost must not be negative, received {}", setup_cost.minor); } if holding_cost.minor <= 0 { panic!("holdingCost must be greater than zero, received {}", holding_cost.minor); } let d = annual_demand as i128; let p = annual_production_rate as i128; let s = setup_cost.minor as i128; let eight = (8 * d) .checked_mul(s) .and_then(|x| x.checked_mul(p)) .filter(|x| *x <= i64::MAX as i128) .unwrap_or_else(|| { panic!("annualDemand, setupCost and annualProductionRate are too large: 8 x D x S x P must stay within 2^63 - 1") }); let two = eight / 4; let m = holding_cost.minor as i128 * (p - d); let n = integer_sqrt((two / m) as i64) as i128; let odd = 2 * n + 1; let result = match mode { "down" => n, "up" => { if n * n * m == two { n } else { n + 1 } } "half-up" => { if odd * odd * m <= eight { n + 1 } else { n } } "half-even" => { let half = odd * odd * m; if half < eight || (half == eight && n % 2 == 1) { n + 1 } else { n } } other => panic!("unknown rounding mode \"{}\"", other), }; result as i64 } pub fn fune_vector(args: &[Value]) -> Value { if let Value::Float(f) = &args[0] { panic!("annualDemand must be a whole number of units, not negative, received {}", f); } if let Value::Float(f) = &args[1] { panic!("annualProductionRate must be a whole number greater than annualDemand, received {}", f); } Value::Int(economic_batch_quantity( args[0].as_i64(), args[1].as_i64(), &money_from_value(&args[2]), &money_from_value(&args[3]), args[4].as_str(), )) }