inventory.eoq
Economic order quantity (Harris/Wilson): the order size that minimises ordering plus holding cost, in whole units.
1.0.0 · published 2026-10-03 by charlie · Anterra
Pinned by 20 tests, run in TypeScript, Python and Rust.
What it does
The economic order quantity of Harris (1913), popularised by Wilson (1934):
EOQ = sqrt(2 * D * S / H)
For example
economic_order_quantity(1,000, £10.00, £0.50, half-up)→ 200 a whole answer: 1000 a year, £10 an order, 50p to hold, is 200economic_order_quantity(1,000, £10.00, £0.50, up)→ 200 a perfect square stays put when rounding upeconomic_order_quantity(10,000, £40.00, £2.00, half-up)→ 632 sqrt(400000) = 632.46 rounds half-up to 632
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 economic_order_quantity(annual_demand: i64, order_cost: &Money, holding_cost: &Money, mode: &str) -> i64
| annual_demand | int | units used per year, not negative |
| order_cost | Money | the fixed cost of placing one order, whatever its size |
| holding_cost | Money | the cost of holding one unit for a year; same currency, more than zero |
| mode | RoundingMode | how sqrt(2DS/H) becomes whole units: half-up, half-even, down or up |
| returns | int | the order quantity in whole units |
Your code names it in one line, in the file that uses it
fune!(inventory.eoq@^1); // then call economic_order_quantity(…)
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_integer_sqrt::integer_sqrt; ← from math.integer-sqrt ^1.0.0 · built alongside by fune
use super::money_amount::{assert_same_currency, money_from_value, Money}; ← from money.amount ^1.0.0 · built alongside by fune
const MAX_SAFE: i128 = (1i128 << 53) - 1;
/// Economic order quantity, sqrt(2DS / H), in whole units.
///
/// The root is never taken in floating point: the floor is an exact integer
/// square root and the rounding decision compares whole numbers, so an exact
/// half (sqrt(6.25) = 2.5) rounds by `mode` and not by the language.
///
/// # Panics
/// Panics on negative demand or order cost, a holding cost that is not
/// positive, mixed currencies, an unknown mode, or 8DS beyond 2^53 - 1.
pub fn economic_order_quantity(annual_demand: i64, order_cost: &Money, holding_cost: &Money, mode: &str) -> i64 {
if annual_demand < 0 {
panic!(
"annualDemand must be a whole number of units, not negative, received {}",
annual_demand
);
}
assert_same_currency(order_cost, holding_cost);
if order_cost.minor < 0 {
panic!("orderCost must not be negative, received {}", order_cost.minor);
}
if holding_cost.minor <= 0 {
panic!("holdingCost must be greater than zero, received {}", holding_cost.minor);
}
let d = annual_demand as i128;
let s = order_cost.minor as i128;
let h = holding_cost.minor as i128;
let eight_ds = 8 * d * s;
if eight_ds > MAX_SAFE {
panic!("annualDemand and orderCost are too large: 8 x demand x order cost must stay within 2^53 - 1");
}
let two_ds = 2 * d * s;
let n = integer_sqrt((two_ds / h) as i64) as i128;
let odd = 2 * n + 1;
let result = match mode {
"down" => n,
"up" => {
if n * n * h == two_ds {
n
} else {
n + 1
}
}
"half-up" => {
if odd * odd * h <= eight_ds {
n + 1
} else {
n
}
}
"half-even" => {
let half = odd * odd * h;
if half < eight_ds || (half == eight_ds && n % 2 == 1) {
n + 1
} else {
n
}
}
other => panic!("unknown rounding mode \"{}\"", other),
};
result as i64
}
pub fn fune_vector(args: &[Value]) -> Value {
if let Value::Float(f) = &args[0] {
panic!("annualDemand must be a whole number of units, not negative, received {}", f);
}
Value::Int(economic_order_quantity(
args[0].as_i64(),
&money_from_value(&args[1]),
&money_from_value(&args[2]),
args[3].as_str(),
))
}Install
fune build
With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and its 3 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.eoq
The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./inventory.eoq-1.0.0-rust.fune, or fetch it from a terminal with fune pull inventory.eoq@1.0.0:rust.
The whole function, every language, is one file too: inventory.eoq-1.0.0.fune, 14,944 bytes, sha256 f6c821a1432734350964c729182dcec1ec2b14422daa6830c0e1a6c227918003. 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.eoq
after — your function gets the result and the arguments, and returns the final result.
// fune: after inventory.eoq
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.integer-sqrt in inventory.eoq
// fune: replace math.round-div in inventory.eoq
// fune: replace money.amount in inventory.eoq
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.eoq --steps.
// fune: step inventory.eoq 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 whole answer: 1000 a year, £10 an order, 50p to hold, is 200 | 1,000, £10.00, £0.50, half-up | → | 200 |
| a perfect square stays put when rounding up | 1,000, £10.00, £0.50, up | → | 200 |
| sqrt(400000) = 632.46 rounds half-up to 632 | 10,000, £40.00, £2.00, half-up | → | 632 |
| sqrt(400000) = 632.46 rounds up to 633 | 10,000, £40.00, £2.00, up | → | 633 |
| a fractional 2DS/H: sqrt(208333.33) = 456.44 rounds to 456 | 5,000, £25.00, £1.20, half-up | → | 456 |
| an exact half, sqrt(6.25) = 2.5, rounds half-up to 3 | 25, £0.01, £0.08, half-up | → | 3 |
| an exact half, sqrt(6.25) = 2.5, rounds half-even to 2 (Math.round would say 3) | 25, £0.01, £0.08, half-even | → | 2 |
| an exact half, sqrt(12.25) = 3.5, rounds half-even to 4 | 49, £0.01, £0.08, half-even | → | 4 |
| just under a half, sqrt(6.24) = 2.498, rounds half-up to 2 | 312, £0.01, £1.00, half-up | → | 2 |
| a hair over a whole number, sqrt(40000.01), rounds up to 201 | 4,000,001, £0.01, £2.00, up | → | 201 |
Show the other 10 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| a hair over a whole number, sqrt(40000.01), rounds down to 200 | 4,000,001, £0.01, £2.00, down | → | 200 |
| no demand orders nothing, even rounding up | 0, €10.00, €0.50, up | → | 0 |
| a free order gives zero | 1,000, €0.00, €0.50, half-up | → | 0 |
| costs in two currencies are an error | 1,000, £10.00, €0.50, half-up | → | error: currency mismatch |
| a zero holding cost is an error | 1,000, £10.00, £0.00, half-up | → | error: holdingCost must be greater than zero |
| negative demand is an error | -1, £10.00, £0.50, half-up | → | error: annualDemand must be a whole number of units, not negative |
| fractional demand is an error | 10.5, £10.00, £0.50, half-up | → | error: annualDemand must be a whole number of units, not negative |
| a negative order cost is an error | 1,000, -£0.01, £0.50, half-up | → | error: orderCost must not be negative |
| 8DS beyond 2^53 - 1 is an error | 1,000,000,000, £100,000.00, £0.50, half-up | → | error: too large |
| an unknown rounding mode is an error | 1,000, £10.00, £0.50, nearest | → | error: unknown rounding mode |
More from the author
where `D` is annual demand in units, `S` the fixed cost of placing an order and `H` the cost of holding one unit in stock for a year. It is the order size at which annual ordering cost (`D / Q * S`) equals annual holding cost (`Q / 2 * H`), and their sum is least.
**Whole units, rounded exactly.** The answer is a square root, so it is rarely whole. Rounding it through floating point is where implementations disagree: `sqrt(6.25)` is exactly 2.5, and JavaScript's `Math.round` makes that 3 while Python's `round` makes it 2. Here the root is never taken in floating point. The floor comes from `math.integer-sqrt` applied to `floor(2DS / H)` (the floor of a square root only changes at whole numbers, so this is exact), and the rounding decision compares whole numbers:
- `down`: the floor. - `up`: the floor, plus one unless `2DS / H` is a perfect square. - `half-up`: plus one when `(2n + 1)^2 * H <= 8DS`, i.e. the root is at least `n + 0.5`. - `half-even`: as half-up, but an exact half goes to the even neighbour.
`half-up` is the usual choice; `up` never under-orders.
**Money is exact.** `orderCost` and `holdingCost` are `Money` in minor units and must be in the same currency; the currencies cancel, so the result is a plain unit count. A holding cost quoted as a percentage of unit cost (a 25% carrying rate) is `money.apply-rate(unitCost, 2500, ...)` first. `8 * D * S` has to stay within 2^53 - 1, which allows, for example, a million units a year at £1,000,000 an order.
Zero demand gives 0. Holding cost must be positive (with free holding the EOQ is unbounded) and neither demand nor order cost may be negative.
Sources: F. W. Harris, "How Many Parts to Make at Once", Factory, The Magazine of Management 10(2), 1913; R. H. Wilson, "A Scientific Routine for Stock Control", Harvard Business Review 13, 1934.
Files
| Path | Bytes |
|---|---|
| README.md | 1,960 |
| impl/python.py | 1,931 |
| impl/rust.rs | 2,635 |
| impl/typescript.ts | 2,001 |
| vectors.json | 3,714 |