use std::collections::HashSet; use super::funejson::Value; use super::dates_add_days::{epoch_day_from_iso, iso_from_epoch_day}; use super::dates_day_of_week::day_of_week; /// Spread each item's counts evenly over the working days of a period: a class /// counted f times gets f even windows, and its items are spread evenly within /// each window. /// /// # Panics /// Panics on an unknown class, a duplicate SKU, a negative frequency, a class /// counted more often than there are working days, a period that ends before it /// starts, or a malformed date. pub fn cycle_count_schedule( items: &[CountItem], counts_per_period: &[(String, i64)], start_date: &str, end_date: &str, holidays: &[String], ) -> Vec { let first = epoch_day_from_iso(start_date); let last = epoch_day_from_iso(end_date); if last < first { panic!("endDate {} is before startDate {}", end_date, start_date); } let closed: HashSet = holidays.iter().map(|h| epoch_day_from_iso(h)).collect(); let mut days: Vec = Vec::new(); for day in first..=last { let iso = iso_from_epoch_day(day); if day_of_week(&iso) <= 5 && !closed.contains(&day) { days.push(iso); } } let mut classes: Vec<&(String, i64)> = counts_per_period.iter().collect(); classes.sort_by(|a, b| a.0.cmp(&b.0)); for (cls, f) in &classes { if *f < 0 { panic!( "countsPerPeriod for class \"{}\" must be a whole number, not negative, received {}", cls, f ); } } let mut seen: HashSet<&str> = HashSet::new(); for item in items { if !seen.insert(item.sku.as_str()) { panic!("duplicate sku \"{}\"", item.sku); } if !counts_per_period.iter().any(|(c, _)| *c == item.abc_class) { panic!("no count frequency for class \"{}\" (sku \"{}\")", item.abc_class, item.sku); } } let n = days.len() as i64; let mut schedule: Vec> = vec![Vec::new(); days.len()]; for (cls, f) in classes { let f = *f; let mut skus: Vec<&String> = items.iter().filter(|i| i.abc_class == *cls).map(|i| &i.sku).collect(); skus.sort(); if skus.is_empty() || f == 0 { continue; } if f > n { panic!( "class \"{}\" is counted {} times but the period has only {} working days", cls, f, n ); } let m = skus.len() as i64; for k in 0..f { let start = k * n / f; let length = (k + 1) * n / f - start; for (j, sku) in skus.iter().enumerate() { schedule[(start + j as i64 * length / m) as usize].push((*sku).clone()); } } } days.into_iter() .zip(schedule) .map(|(date, skus)| CountDay { date, skus }) .collect() } pub fn count_item_from_value(v: &Value) -> CountItem { CountItem { sku: v.get("sku").as_str().to_string(), abc_class: v.get("abcClass").as_str().to_string(), } } pub fn count_day_to_value(day: &CountDay) -> Value { Value::obj(vec![ ("date", Value::str(&day.date)), ("skus", Value::Arr(day.skus.iter().map(|s| Value::str(s)).collect())), ]) } pub fn fune_vector(args: &[Value]) -> Value { let items: Vec = args[0].as_arr().iter().map(count_item_from_value).collect(); let counts: Vec<(String, i64)> = match &args[1] { Value::Obj(pairs) => pairs .iter() .map(|(k, v)| { if let Value::Float(f) = v { panic!( "countsPerPeriod for class \"{}\" must be a whole number, not negative, received {}", k, f ); } (k.clone(), v.as_i64()) }) .collect(), _ => Vec::new(), }; let holidays: Vec = args[4].as_arr().iter().map(|v| v.as_str().to_string()).collect(); Value::Arr( cycle_count_schedule(&items, &counts, args[2].as_str(), args[3].as_str(), &holidays) .iter() .map(count_day_to_value) .collect(), ) }