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
stockTurnover(£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 coverstockTurnover(£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.01stockTurnover(£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.
export function stockTurnover(openingStock: Money, closingStock: Money, costOfSales: Money, periodDays: number): StockTurnover
| openingStock | Money | stock value at cost at the start of the period |
| closingStock | Money | stock value at cost at the end of the period |
| costOfSales | Money | cost of the stock sold or used in the period |
| periodDays | int | length of the period: 365 for a year, 28 for four weeks |
| returns | StockTurnover |
The type it declares, generated into your project
/** How fast stock moves, three ways. */
export interface StockTurnover {
/** (opening + closing) / 2, half-up to minor units */
readonly averageStock: Money;
/** cost of sales / average stock, to 2 decimal places; null when average stock is zero */
readonly turns: number | null;
/** average stock / cost of sales x periodDays, to 1 decimal place; null when cost of sales is zero */
readonly daysInventoryOutstanding: number | null;
/** closing stock / cost of sales x periodDays, to 1 decimal place; null when cost of sales is zero */
readonly daysOfCover: number | null;
}
Your code names it in one line, in the file that uses it
import { stockTurnover } from "#fune/inventory.stock-turnover@^1";
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
import { roundDiv } from "./math_round_div.ts"; ← from math.round-div ^1.0.0 · built alongside by fune
import { type Money, assertSameCurrency, money } from "./money_amount.ts"; ← from money.amount ^1.0.0 · built alongside by fune
import { type StockTurnover } from "./inventory_stock_turnover_types.ts";
function notNegative(name: string, amount: Money): void {
if (amount.minor < 0) throw new RangeError(`${name} must not be negative, received ${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.
*/
export function stockTurnover(openingStock: Money, closingStock: Money, costOfSales: Money, periodDays: number): StockTurnover {
assertSameCurrency(openingStock, closingStock);
assertSameCurrency(openingStock, costOfSales);
notNegative("openingStock", openingStock);
notNegative("closingStock", closingStock);
notNegative("costOfSales", costOfSales);
if (!Number.isInteger(periodDays) || periodDays < 1) {
throw new RangeError(`periodDays must be a whole number of days, at least 1, received ${periodDays}`);
}
const both = openingStock.minor + closingStock.minor;
const cogs = costOfSales.minor;
return {
averageStock: money(roundDiv(both, 2, "half-up"), openingStock.currency),
turns: both === 0 ? null : roundDiv(cogs * 200, both, "half-up") / 100,
daysInventoryOutstanding: cogs === 0 ? null : roundDiv(both * periodDays * 10, cogs * 2, "half-up") / 10,
daysOfCover: cogs === 0 ? null : roundDiv(closingStock.minor * periodDays * 10, cogs, "half-up") / 10,
};
}Install
fune build
With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and its 2 dependencies, pins them in fune.lock, downloads only the TypeScript 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. Or pin a range in fune.project and build in one step:
fune add inventory.stock-turnover
The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./inventory.stock-turnover-1.0.0-typescript.fune, or fetch it from a terminal with fune pull inventory.stock-turnover@1.0.0:typescript.
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.
| Case | Arguments | Expected | |
|---|---|---|---|
| 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
| Case | Arguments | Expected | |
|---|---|---|---|
| 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
| Path | Bytes |
|---|---|
| README.md | 1,617 |
| impl/python.py | 1,607 |
| impl/rust.rs | 2,640 |
| impl/typescript.ts | 1,527 |
| vectors.json | 3,797 |