Functional Weave
Code in Rust

inventory.stock-turnover

Stock turnover ratio, days inventory outstanding and days of cover for a period, computed exactly.

1.0.0 · published 2026-10-03 by charlie · Anterra

Pinned by 13 tests, run in TypeScript, Python and Rust.

What it does

Three measures of how quickly stock moves over a period, all at cost:

- **turns** (stock turnover ratio) = cost of sales / average stock, where average stock is (opening + closing) / 2. Six turns a year means the average holding is sold six times over. - **daysInventoryOutstanding** = average stock / cost of sales x periodDays: how many days the average holding represents; periodDays / turns. - **daysOfCover** = closing stock / cost of sales x periodDays: how many days the stock on hand now would last at this period's rate of use. This is the figure a warehouse plans with; it looks forward from the closing stock.

For example

  • stock_turnover(£500.00, £300.00, £2,400.00, 365) → average stock £400.00, turns 6, days inventory outstanding 60.8, days of cover 45.6 a year: average 400.00 against 2400.00 cost of sales is 6 turns, 60.8 days, 45.6 days of cover
  • stock_turnover(£123.45, £234.56, £1,000.00, 30) → average stock £179.01, turns 5.59, days inventory outstanding 5.4, days of cover 7 awkward figures: 5.59 turns, 5.4 days, 7.0 days of cover over 30 days; the average 179.005 shows as 179.01
  • stock_turnover(£2.00, £2.00, £0.01, 1) → average stock £2.00, turns 0.01, days inventory outstanding 200, days of cover 200 an exact half of a hundredth, 0.005 turns, rounds half-up to 0.01

The function

The same function in TypeScript, Python and Rust, pinned by the same tests. Pick your language; the choice follows you around the registry.

pub fn stock_turnover(opening_stock: &Money, closing_stock: &Money, cost_of_sales: &Money, period_days: i64) -> StockTurnover
opening_stockMoneystock value at cost at the start of the period
closing_stockMoneystock value at cost at the end of the period
cost_of_salesMoneycost of the stock sold or used in the period
period_daysintlength of the period: 365 for a year, 28 for four weeks
returnsStockTurnover

The type it declares, generated into your project

/// How fast stock moves, three ways.
#[derive(Debug, Clone, PartialEq)]
pub struct StockTurnover {
    /// (opening + closing) / 2, half-up to minor units
    pub average_stock: Money,
    /// cost of sales / average stock, to 2 decimal places; null when average stock is zero
    pub turns: Option<f64>,
    /// average stock / cost of sales x periodDays, to 1 decimal place; null when cost of sales is zero
    pub days_inventory_outstanding: Option<f64>,
    /// closing stock / cost of sales x periodDays, to 1 decimal place; null when cost of sales is zero
    pub days_of_cover: Option<f64>,
}

Your code names it in one line, in the file that uses it

fune!(inventory.stock-turnover@^1);  // then call stock_turnover(…)
impl/rust.rs · 69 lines · open · 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(),
    ))
}

Install

fune build

With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and its 2 dependencies, pins them in fune.lock, downloads only the Rust package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. A crate’s build.rs runs it before every compile. Or pin a range in fune.project and build in one step:

fune add inventory.stock-turnover
Download for Rust inventory.stock-turnover-1.0.0-rust.fune · 11,385 bytes sha256 adb098f9e326cef572b5f1942b78b94fe49cb7346302bf559e46a5b49b90ecbb

The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./inventory.stock-turnover-1.0.0-rust.fune, or fetch it from a terminal with fune pull inventory.stock-turnover@1.0.0:rust.

The whole function, every language, is one file too: inventory.stock-turnover-1.0.0.fune, 14,658 bytes, sha256 9155bb345cfebcc9171de07f32cb2dbca138e4b611c2e5f980f793cbbc978314. It installs into a project of any language.

Customise it in your app

The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.

before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.

// fune: before inventory.stock-turnover

after — your function gets the result and the arguments, and returns the final result.

// fune: after inventory.stock-turnover

replace — inside this capability’s code only, calls to a dependency go to your function, with the same signature. Other capabilities that use it are unaffected; write in * to replace it everywhere.

// fune: replace math.round-div in inventory.stock-turnover
// fune: replace money.amount in inventory.stock-turnover

step — your function runs at a numbered point inside the function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show inventory.stock-turnover --steps.

// fune: step inventory.stock-turnover after <n|label>

Tests

A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.

CaseArgumentsExpected
a year: average 400.00 against 2400.00 cost of sales is 6 turns, 60.8 days, 45.6 days of cover £500.00, £300.00, £2,400.00, 365 → average stock £400.00, turns 6, days inventory outstanding 60.8, days of cover 45.6
awkward figures: 5.59 turns, 5.4 days, 7.0 days of cover over 30 days; the average 179.005 shows as 179.01 £123.45, £234.56, £1,000.00, 30 → average stock £179.01, turns 5.59, days inventory outstanding 5.4, days of cover 7
an exact half of a hundredth, 0.005 turns, rounds half-up to 0.01 £2.00, £2.00, £0.01, 1 → average stock £2.00, turns 0.01, days inventory outstanding 200, days of cover 200
a week: 0.7 turns and ten days' cover £10.00, £10.00, £7.00, 7 → average stock £10.00, turns 0.7, days inventory outstanding 10, days of cover 10
stock ran out by the end: no cover left, though the average held some £100.00, £0.00, £200.00, 30 → average stock £50.00, turns 4, days inventory outstanding 7.5, days of cover 0
nothing sold: zero turns, and the day counts have no answer £100.00, £100.00, £0.00, 30 → average stock £100.00, turns 0, days inventory outstanding —, days of cover —
no stock held but cost of sales (drop-shipped): turns has no answer £0.00, £0.00, £50.00, 30 → average stock £0.00, turns —, days inventory outstanding 0, days of cover 0
nothing at all: every ratio is null €0.00, €0.00, €0.00, 30 → average stock €0.00, turns —, days inventory outstanding —, days of cover —
amounts in two currencies are an error £1.00, €1.00, £1.00, 30 → error: currency mismatch
negative stock is an error -£0.01, £1.00, £1.00, 30 → error: openingStock must not be negative
Show the other 3 tests
CaseArgumentsExpected
negative cost of sales is an error £1.00, £1.00, -£1.00, 30 → error: costOfSales must not be negative
a period of zero days is an error £1.00, £1.00, £1.00, 0 → error: periodDays must be a whole number of days, at least 1
a fractional period is an error £1.00, £1.00, £1.00, 7.5 → error: periodDays must be a whole number of days, at least 1

More from the author

**Exact, then rounded once.** Each ratio is a single division of whole minor units, rounded half-up with `math.round-div` to hundredths (turns) or tenths (days), and only then turned into a float. So 0.005 turns is 0.01 in every language, where `Math.round(x * 100) / 100` and Python's `round(x, 2)` disagree. `averageStock` is rounded half-up to a minor unit for display; the ratios use the exact sum, not the rounded average.

**No answer is null, not zero or infinity.** Turns is null when average stock is zero; the two day counts are null when cost of sales is zero (stock that does not move has no cover horizon).

Use cost values throughout: turnover on sales value against stock at cost overstates it by the margin. Stock values and cost of sales may not be negative, the three amounts must share a currency, and periodDays must be at least 1.

Source: the standard definitions, e.g. CIMA Official Terminology (inventory turnover, inventory days).

Files

PathBytes
README.md1,617
impl/python.py1,607
impl/rust.rs2,640
impl/typescript.ts1,527
vectors.json3,797