Functional Weave
Code in Rust

inventory.stock-turnover@1.0.0

impl/rust.rs

2,640 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::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, Money};  ← from money.amount ^1.0.0 · built alongside by fune

fn not_negative(name: &str, amount: &Money) {
    if amount.minor < 0 {
        panic!("{} must not be negative, received {}", name, amount.minor);
    }
}

/// Turns, days inventory outstanding and days of cover. Each is one exact
/// division rounded half-up, then made a float, so every language agrees.
///
/// # Panics
/// Panics on mixed currencies, a negative amount, or a period under one day.
pub fn stock_turnover(opening_stock: &Money, closing_stock: &Money, cost_of_sales: &Money, period_days: i64) -> StockTurnover {
    assert_same_currency(opening_stock, closing_stock);
    assert_same_currency(opening_stock, cost_of_sales);
    not_negative("openingStock", opening_stock);
    not_negative("closingStock", closing_stock);
    not_negative("costOfSales", cost_of_sales);
    if period_days < 1 {
        panic!("periodDays must be a whole number of days, at least 1, received {}", period_days);
    }
    let both = opening_stock.minor + closing_stock.minor;
    let cogs = cost_of_sales.minor;
    StockTurnover {
        average_stock: money(round_div(both, 2, "half-up"), &opening_stock.currency),
        turns: if both == 0 { None } else { Some(round_div(cogs * 200, both, "half-up") as f64 / 100.0) },
        days_inventory_outstanding: if cogs == 0 {
            None
        } else {
            Some(round_div(both * period_days * 10, cogs * 2, "half-up") as f64 / 10.0)
        },
        days_of_cover: if cogs == 0 {
            None
        } else {
            Some(round_div(closing_stock.minor * period_days * 10, cogs, "half-up") as f64 / 10.0)
        },
    }
}

fn optional_float(value: Option<f64>) -> Value {
    match value {
        Some(v) => Value::Float(v),
        None => Value::Null,
    }
}

pub fn stock_turnover_to_value(result: &StockTurnover) -> Value {
    Value::obj(vec![
        ("averageStock", money_to_value(&result.average_stock)),
        ("turns", optional_float(result.turns)),
        ("daysInventoryOutstanding", optional_float(result.days_inventory_outstanding)),
        ("daysOfCover", optional_float(result.days_of_cover)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    if let Value::Float(f) = &args[3] {
        panic!("periodDays must be a whole number of days, at least 1, received {}", f);
    }
    stock_turnover_to_value(&stock_turnover(
        &money_from_value(&args[0]),
        &money_from_value(&args[1]),
        &money_from_value(&args[2]),
        args[3].as_i64(),
    ))
}