Functional Weave
Code in Rust

inventory.cycle-count-schedule@1.0.0

impl/rust.rs

4,274 bytes · the Rust implementation · view raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

use std::collections::HashSet;

use super::funejson::Value;  ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::dates_add_days::{epoch_day_from_iso, iso_from_epoch_day};  ← from dates.add-days ^1.0.0 · built alongside by fune
use super::dates_day_of_week::day_of_week;  ← from dates.day-of-week ^1.0.0 · built alongside by fune

/// 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<CountDay> {
    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<i64> = holidays.iter().map(|h| epoch_day_from_iso(h)).collect();
    let mut days: Vec<String> = 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<String>> = 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<CountItem> = 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<String> = 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(),
    )
}