Functional Weave
Code in Rust

inventory.valuation-weighted-average@1.0.0

impl/rust.rs

4,400 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_add_days::epoch_day_from_iso;  ← from dates.add-days ^1.0.0 · built alongside by fune
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

/// Perpetual weighted average cost. The stock's value is held exactly and each
/// issue takes value x issued / on hand, rounded once, so the last issue takes
/// exactly what is left and an empty item is worth nothing.
///
/// # Panics
/// Panics on a ledger out of date order, a zero quantity, a receipt without a
/// unit cost or with a negative one, an issue with a unit cost, a cost in
/// another currency, an issue larger than the stock on hand, or an unknown mode.
pub fn weighted_average_valuation(movements: &[StockMovement], currency: &str, mode: &str) -> AverageCostValuation {
    let zero = money(0, currency);
    round_div(0, 1, mode); // an unknown mode fails even on a ledger with no issues
    let mut quantity: i64 = 0;
    let mut value: i64 = 0;
    let mut cost_of_sales: i64 = 0;
    let mut issue_costs = Vec::new();
    let mut previous: Option<(i64, &str)> = None;
    for line in movements {
        let day = epoch_day_from_iso(&line.date);
        if let Some((previous_day, previous_date)) = previous {
            if day < previous_day {
                panic!("movements must be in date order: {} comes after {}", line.date, previous_date);
            }
        }
        previous = Some((day, &line.date));
        if line.quantity == 0 {
            panic!("quantity must be a non-zero whole number, received 0 on {}", line.date);
        }
        if line.quantity > 0 {
            let unit_cost = match &line.unit_cost {
                Some(c) => c,
                None => panic!("a receipt needs a unitCost: {} on {}", line.quantity, line.date),
            };
            assert_same_currency(&zero, unit_cost);
            if unit_cost.minor < 0 {
                panic!("unitCost must not be negative, received {} on {}", unit_cost.minor, line.date);
            }
            quantity += line.quantity;
            value += line.quantity * unit_cost.minor;
            continue;
        }
        if line.unit_cost.is_some() {
            panic!(
                "an issue takes its cost from stock, so its unitCost must be null: {} on {}",
                line.quantity, line.date
            );
        }
        let issued = -line.quantity;
        if issued > quantity {
            panic!(
                "insufficient stock: an issue of {} on {} exceeds the {} on hand",
                issued, line.date, quantity
            );
        }
        let cost = round_div(value * issued, quantity, mode);
        issue_costs.push(money(cost, currency));
        cost_of_sales += cost;
        value -= cost;
        quantity -= issued;
    }
    AverageCostValuation {
        closing_quantity: quantity,
        closing_value: money(value, currency),
        cost_of_sales: money(cost_of_sales, currency),
        issue_costs,
        average_unit_cost: money(if quantity == 0 { 0 } else { round_div(value, quantity, mode) }, currency),
    }
}

pub fn stock_movement_from_value(v: &Value) -> StockMovement {
    let cost = v.get("unitCost");
    StockMovement {
        date: v.get("date").as_str().to_string(),
        quantity: v.get("quantity").as_i64(),
        unit_cost: if cost.is_null() { None } else { Some(money_from_value(cost)) },
    }
}

pub fn average_cost_valuation_to_value(result: &AverageCostValuation) -> Value {
    Value::obj(vec![
        ("closingQuantity", Value::Int(result.closing_quantity)),
        ("closingValue", money_to_value(&result.closing_value)),
        ("costOfSales", money_to_value(&result.cost_of_sales)),
        ("issueCosts", Value::Arr(result.issue_costs.iter().map(money_to_value).collect())),
        ("averageUnitCost", money_to_value(&result.average_unit_cost)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    let movements: Vec<StockMovement> = args[0]
        .as_arr()
        .iter()
        .map(|v| {
            if let Value::Float(f) = v.get("quantity") {
                panic!("quantity must be a non-zero whole number, received {} on {}", f, v.get("date").as_str());
            }
            stock_movement_from_value(v)
        })
        .collect();
    average_cost_valuation_to_value(&weighted_average_valuation(&movements, args[1].as_str(), args[2].as_str()))
}