use super::funejson::Value; use super::inventory_pallet_fit_data::PALLETS; /// Cartons per layer on a `length` x `width` deck, base `a` x `b`, as the best /// two-block pattern: one split across the length or the width, each block a /// grid with the carton one way round. fn cartons_per_layer(length: i64, width: i64, a: i64, b: i64) -> i64 { let mut best = 0; let mut i = 0; while i * a <= length { best = best.max(i * (width / b) + ((length - i * a) / b) * (width / a)); i += 1; } let mut j = 0; while j * b <= width { best = best.max(j * (length / a) + ((width - j * b) / a) * (length / b)); j += 1; } best } fn positive(name: &str, value: i64) { if value <= 0 { panic!("{} must be a whole number greater than zero, received {}", name, value); } } /// The best stack of identical cartons on a UK or EUR pallet within the limits. /// /// # Panics /// Panics on a dimension that is not positive, a negative weight or weight /// limit, or an unknown pallet. pub fn pallet_fit(carton: &Carton, pallet: &str, max_load_height_mm: i64, max_load_weight_grams: Option, upright: bool) -> PalletFit { positive("lengthMm", carton.length_mm); positive("widthMm", carton.width_mm); positive("heightMm", carton.height_mm); if carton.weight_grams < 0 { panic!("weightGrams must be a whole number, not negative, received {}", carton.weight_grams); } positive("maxLoadHeightMm", max_load_height_mm); if let Some(mw) = max_load_weight_grams { if mw < 0 { panic!("maxLoadWeightGrams must be a whole number, not negative, received {}", mw); } } let deck = match PALLETS.iter().find(|p| p.code == pallet) { Some(d) => d, None => panic!("unknown pallet \"{}\": expected uk or euro", pallet), }; let (l, w, h) = (carton.length_mm, carton.width_mm, carton.height_mm); let choices: Vec<(i64, i64, i64)> = if upright { vec![(h, l, w)] } else { vec![(h, l, w), (w, l, h), (l, w, h)] }; let mut best: Option = None; for (vertical, a, b) in choices { let per_layer = cartons_per_layer(deck.length_mm, deck.width_mm, a, b); let layers = max_load_height_mm / vertical; let mut per_pallet = per_layer * layers; let mut limited_by = if per_layer == 0 { "footprint" } else { "height" }; if let Some(mw) = max_load_weight_grams { if per_layer > 0 && carton.weight_grams > 0 { let by_weight = mw / carton.weight_grams; if by_weight < per_pallet { per_pallet = by_weight; limited_by = "weight"; } } } let fit = PalletFit { pallet_length_mm: deck.length_mm, pallet_width_mm: deck.width_mm, carton_height_mm: vertical, per_layer, layers, per_pallet, limited_by: limited_by.to_string(), load_height_mm: if per_layer == 0 { 0 } else { (per_pallet + per_layer - 1) / per_layer * vertical }, load_weight_grams: per_pallet * carton.weight_grams, }; if best.as_ref().map_or(true, |b| fit.per_pallet > b.per_pallet) { best = Some(fit); } } best.unwrap() } pub fn carton_from_value(v: &Value) -> Carton { Carton { length_mm: v.get("lengthMm").as_i64(), width_mm: v.get("widthMm").as_i64(), height_mm: v.get("heightMm").as_i64(), weight_grams: v.get("weightGrams").as_i64(), } } pub fn pallet_fit_to_value(fit: &PalletFit) -> Value { Value::obj(vec![ ("palletLengthMm", Value::Int(fit.pallet_length_mm)), ("palletWidthMm", Value::Int(fit.pallet_width_mm)), ("cartonHeightMm", Value::Int(fit.carton_height_mm)), ("perLayer", Value::Int(fit.per_layer)), ("layers", Value::Int(fit.layers)), ("perPallet", Value::Int(fit.per_pallet)), ("limitedBy", Value::str(&fit.limited_by)), ("loadHeightMm", Value::Int(fit.load_height_mm)), ("loadWeightGrams", Value::Int(fit.load_weight_grams)), ]) } pub fn fune_vector(args: &[Value]) -> Value { for key in ["lengthMm", "widthMm", "heightMm"] { if let Value::Float(f) = args[0].get(key) { panic!("{} must be a whole number greater than zero, received {}", key, f); } } let max_weight = if args[3].is_null() { None } else { Some(args[3].as_i64()) }; pallet_fit_to_value(&pallet_fit( &carton_from_value(&args[0]), args[1].as_str(), args[2].as_i64(), max_weight, args[4].as_bool(), )) }