Functional Weave
Code in Rust

inventory.abc-classification@1.0.0

impl/rust.rs

4,348 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::math_round_div::round_div;  ← from math.round-div ^1.0.0 · built alongside by fune
use super::money_amount::{assert_same_currency, money, money_from_value, money_to_value};  ← from money.amount ^1.0.0 · built alongside by fune

const LETTERS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ";
const MAX_SAFE: i128 = (1i128 << 53) - 1;

/// Rank items by annual consumption value and class them by the band of the
/// cumulative share each one starts in, so the item that crosses a cut-off
/// stays in the higher class.
///
/// # Panics
/// Panics on bad cut-offs, a duplicate SKU, a negative quantity or cost, mixed
/// currencies, or a total too large to scale by 10000.
pub fn abc_classification(items: &[ConsumptionItem], cutoff_basis_points: &[i64]) -> Vec<AbcItem> {
    if cutoff_basis_points.is_empty() || cutoff_basis_points.len() > 25 {
        panic!("cutoffBasisPoints needs 1 to 25 cut-offs, received {}", cutoff_basis_points.len());
    }
    let mut previous = 0;
    for &cutoff in cutoff_basis_points {
        if cutoff <= previous || cutoff >= 10000 {
            panic!(
                "cutoffBasisPoints must ascend strictly within 1..9999: received {} after {}",
                cutoff, previous
            );
        }
        previous = cutoff;
    }
    let mut seen: HashSet<&str> = HashSet::new();
    let mut valued: Vec<(String, i64, String)> = Vec::new();
    for item in items {
        if !seen.insert(item.sku.as_str()) {
            panic!("duplicate sku \"{}\"", item.sku);
        }
        if item.annual_quantity < 0 {
            panic!(
                "annualQuantity must be a whole number, not negative, received {} for \"{}\"",
                item.annual_quantity, item.sku
            );
        }
        if item.unit_cost.minor < 0 {
            panic!("unitCost must not be negative, received {} for \"{}\"", item.unit_cost.minor, item.sku);
        }
        assert_same_currency(&items[0].unit_cost, &item.unit_cost);
        valued.push((item.sku.clone(), item.annual_quantity * item.unit_cost.minor, item.unit_cost.currency.clone()));
    }
    let total: i64 = valued.iter().map(|v| v.1).sum();
    if total as i128 * 10000 > MAX_SAFE {
        panic!("the total annual value is too large: it must stay within (2^53 - 1) / 10000 minor units");
    }
    valued.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
    let mut before: i64 = 0;
    let mut result = Vec::with_capacity(valued.len());
    for (index, (sku, value, currency)) in valued.into_iter().enumerate() {
        let band = cutoff_basis_points
            .iter()
            .position(|&cutoff| (before as i128) * 10000 < (cutoff as i128) * (total as i128))
            .unwrap_or(cutoff_basis_points.len());
        before += value;
        result.push(AbcItem {
            sku,
            annual_value: money(value, &currency),
            rank: index as i64 + 1,
            cumulative_basis_points: if total == 0 { 0 } else { round_div(before * 10000, total, "half-up") },
            abc_class: (LETTERS[band] as char).to_string(),
        });
    }
    result
}

pub fn consumption_item_from_value(v: &Value) -> ConsumptionItem {
    ConsumptionItem {
        sku: v.get("sku").as_str().to_string(),
        annual_quantity: v.get("annualQuantity").as_i64(),
        unit_cost: money_from_value(v.get("unitCost")),
    }
}

pub fn abc_item_to_value(item: &AbcItem) -> Value {
    Value::obj(vec![
        ("sku", Value::str(&item.sku)),
        ("annualValue", money_to_value(&item.annual_value)),
        ("rank", Value::Int(item.rank)),
        ("cumulativeBasisPoints", Value::Int(item.cumulative_basis_points)),
        ("abcClass", Value::str(&item.abc_class)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    let items: Vec<ConsumptionItem> = args[0]
        .as_arr()
        .iter()
        .map(|v| {
            if let Value::Float(f) = v.get("annualQuantity") {
                panic!(
                    "annualQuantity must be a whole number, not negative, received {} for \"{}\"",
                    f,
                    v.get("sku").as_str()
                );
            }
            consumption_item_from_value(v)
        })
        .collect();
    let cutoffs: Vec<i64> = args[1].as_arr().iter().map(|v| v.as_i64()).collect();
    Value::Arr(abc_classification(&items, &cutoffs).iter().map(abc_item_to_value).collect())
}