use super::funejson::Value; use super::dates_days_between::days_between; fn whole_units(name: &str, value: i64) { if value < 0 { panic!("{} must be a whole number, not negative, received {}", name, value); } } /// Pick `quantity` units earliest expiry first, from batches with at least /// `min_shelf_life_days` of life left on `as_at`. Short stock is a shortfall, /// not an error. /// /// # Panics /// Panics on a negative quantity or shelf life, or a malformed date. pub fn fefo_pick(batches: &[StockBatch], quantity: i64, as_at: &str, min_shelf_life_days: i64) -> FefoPick { whole_units("quantity", quantity); whole_units("minShelfLifeDays", min_shelf_life_days); let mut dated: Vec<(i64, &StockBatch)> = batches .iter() .map(|batch| { whole_units(&format!("quantity of batch \"{}\"", batch.batch), batch.quantity); (days_between(as_at, &batch.expiry), batch) }) .collect(); // sort_by_key is stable, so equal expiry dates keep their order. dated.sort_by_key(|pair| pair.0); let mut lines = Vec::new(); let mut skipped = Vec::new(); let mut remaining = quantity; for (life, batch) in dated { if batch.quantity == 0 { continue; } if life < min_shelf_life_days { skipped.push(batch.batch.clone()); continue; } if remaining == 0 { continue; } let take = remaining.min(batch.quantity); lines.push(PickLine { batch: batch.batch.clone(), expiry: batch.expiry.clone(), quantity: take, }); remaining -= take; } FefoPick { lines, picked: quantity - remaining, shortfall: remaining, skipped, } } pub fn stock_batch_from_value(v: &Value) -> StockBatch { StockBatch { batch: v.get("batch").as_str().to_string(), expiry: v.get("expiry").as_str().to_string(), quantity: v.get("quantity").as_i64(), } } pub fn fefo_pick_to_value(result: &FefoPick) -> Value { Value::obj(vec![ ( "lines", Value::Arr( result .lines .iter() .map(|l| { Value::obj(vec![ ("batch", Value::str(&l.batch)), ("expiry", Value::str(&l.expiry)), ("quantity", Value::Int(l.quantity)), ]) }) .collect(), ), ), ("picked", Value::Int(result.picked)), ("shortfall", Value::Int(result.shortfall)), ("skipped", Value::Arr(result.skipped.iter().map(|s| Value::str(s)).collect())), ]) } pub fn fune_vector(args: &[Value]) -> Value { if let Value::Float(f) = &args[1] { panic!("quantity must be a whole number, not negative, received {}", f); } let batches: Vec = args[0].as_arr().iter().map(stock_batch_from_value).collect(); fefo_pick_to_value(&fefo_pick(&batches, args[1].as_i64(), args[2].as_str(), args[3].as_i64())) }