Functional Weave
Code in TypeScript

inventory.expiry-fefo@1.0.0

impl/rust.rs

3,195 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 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_days_between::days_between;  ← from dates.days-between ^1.0.0 · built alongside by fune

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<StockBatch> = 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()))
}