manufacturing.batch-size
Economic batch quantity: the EOQ for a batch made at a finite production rate, 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 batch quantity (EBQ, also called the economic production quantity or EPQ): the batch size at which yearly set-up cost equals yearly holding cost when stock is made in-house at a finite rate rather than delivered all at once:
EBQ = sqrt( 2 * D * S / (H * (1 - D / P)) ) = sqrt( 2 * D * S * P / (H * (P - D)) )
For example
economicBatchQuantity(1,000, 2,000, £5.00, £0.50, half-up)→ 200 demand 1000, rate 2000, set-up £5, holding 50p: EBQ is exactly 200 (EOQ would be 141)economicBatchQuantity(1,000, 2,000, £5.00, £0.50, up)→ 200 a perfect square stays put when rounding upeconomicBatchQuantity(10,000, 40,000, £100.00, £2.00, half-up)→ 1,155 demand 10000, rate 40000, set-up £100, holding £2: sqrt(1333333.33) = 1154.70 rounds half-up to 1155
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 economicBatchQuantity(annualDemand: number, annualProductionRate: number, setupCost: Money, holdingCost: Money, mode: RoundingMode): number
| annualDemand | int | units used per year, not negative |
| annualProductionRate | int | units the process makes per year while running; more than annualDemand |
| setupCost | Money | the fixed cost of setting up one batch, whatever its size |
| holdingCost | Money | the cost of holding one unit for a year; same currency, more than zero |
| mode | RoundingMode | how the square root becomes whole units: half-up, half-even, down or up |
| returns | int | the batch size in whole units |
Your code names it in one line, in the file that uses it
import { economicBatchQuantity } from "#fune/manufacturing.batch-size@^1";
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
import { type RoundingMode } from "./math_round_div.ts"; ← from math.round-div ^1.0.0 · built alongside by fune
import { integerSqrt } from "./math_integer_sqrt.ts"; ← from math.integer-sqrt ^1.0.0 · built alongside by fune
import { type Money, assertSameCurrency } from "./money_amount.ts"; ← from money.amount ^1.0.0 · built alongside by fune
const MAX_I64 = (1n << 63n) - 1n;
/**
* Economic batch quantity, sqrt(2DSP / (H(P - D))), 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.
*/
export function economicBatchQuantity(
annualDemand: number,
annualProductionRate: number,
setupCost: Money,
holdingCost: Money,
mode: RoundingMode,
): number {
if (!Number.isSafeInteger(annualDemand) || annualDemand < 0) {
throw new RangeError(`annualDemand must be a whole number of units, not negative, received ${annualDemand}`);
}
if (!Number.isSafeInteger(annualProductionRate) || annualProductionRate <= annualDemand) {
throw new RangeError(
`annualProductionRate must be a whole number greater than annualDemand, received ${annualProductionRate}`,
);
}
assertSameCurrency(setupCost, holdingCost);
if (setupCost.minor < 0) {
throw new RangeError(`setupCost must not be negative, received ${setupCost.minor}`);
}
if (holdingCost.minor <= 0) {
throw new RangeError(`holdingCost must be greater than zero, received ${holdingCost.minor}`);
}
const d = BigInt(annualDemand);
const p = BigInt(annualProductionRate);
const s = BigInt(setupCost.minor);
const eight = 8n * d * s * p;
if (eight > MAX_I64) {
throw new RangeError("annualDemand, setupCost and annualProductionRate are too large: 8 x D x S x P must stay within 2^63 - 1");
}
const two = 2n * d * s * p;
const m = BigInt(holdingCost.minor) * (p - d);
const n = BigInt(integerSqrt(Number(two / m)));
const odd = 2n * n + 1n;
switch (mode) {
case "down":
return Number(n);
case "up":
return Number(n * n * m === two ? n : n + 1n);
case "half-up":
return Number(odd * odd * m <= eight ? n + 1n : n);
case "half-even": {
const half = odd * odd * m;
if (half < eight) return Number(n + 1n);
if (half > eight) return Number(n);
return Number(n % 2n === 0n ? n : n + 1n);
}
default:
throw new RangeError(`unknown rounding mode "${mode}"`);
}
}Install
fune build
With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and its 3 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 manufacturing.batch-size
The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./manufacturing.batch-size-1.0.0-typescript.fune, or fetch it from a terminal with fune pull manufacturing.batch-size@1.0.0:typescript.
The whole function, every language, is one file too: manufacturing.batch-size-1.0.0.fune, 16,906 bytes, sha256 3df9331cc9df8c218c15180f723cc8186675afb8e740d946e9b518896efdd2e5. 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 manufacturing.batch-size
after — your function gets the result and the arguments, and returns the final result.
// fune: after manufacturing.batch-size
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 manufacturing.batch-size
// fune: replace math.round-div in manufacturing.batch-size
// fune: replace money.amount in manufacturing.batch-size
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 manufacturing.batch-size --steps.
// fune: step manufacturing.batch-size 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 | |
|---|---|---|---|
| demand 1000, rate 2000, set-up £5, holding 50p: EBQ is exactly 200 (EOQ would be 141) | 1,000, 2,000, £5.00, £0.50, half-up | → | 200 |
| a perfect square stays put when rounding up | 1,000, 2,000, £5.00, £0.50, up | → | 200 |
| demand 10000, rate 40000, set-up £100, holding £2: sqrt(1333333.33) = 1154.70 rounds half-up to 1155 | 10,000, 40,000, £100.00, £2.00, half-up | → | 1,155 |
| the same rounded down is 1154 | 10,000, 40,000, £100.00, £2.00, down | → | 1,154 |
| a rate close to demand makes the batch much larger: sqrt(180000) = 424.26 | 1,200, 1,500, £45.00, £3.00, half-up | → | 424 |
| the same rounded up is 425 | 1,200, 1,500, £45.00, £3.00, up | → | 425 |
| an exact half, sqrt(6.25) = 2.5, rounds half-up to 3 | 25, 50, £0.01, £0.16, half-up | → | 3 |
| an exact half, sqrt(6.25) = 2.5, rounds half-even to 2 | 25, 50, £0.01, £0.16, half-even | → | 2 |
| an exact half, sqrt(12.25) = 3.5, rounds half-even to 4 | 49, 98, £0.01, £0.16, half-even | → | 4 |
| below the half, sqrt(6) = 2.449, rounds half-up to 2 | 24, 48, £0.01, £0.16, half-up | → | 2 |
Show the other 10 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| zero demand needs no batch | 0, 100, £5.00, £0.50, half-up | → | 0 |
| a free set-up gives a batch of zero | 1,000, 2,000, €0.00, €0.50, up | → | 0 |
| a production rate equal to demand is an error | 1,000, 1,000, £5.00, £0.50, half-up | → | error: annualProductionRate must be a whole number greater than annualDemand |
| a zero holding cost is an error | 1,000, 2,000, £5.00, £0.00, half-up | → | error: holdingCost must be greater than zero |
| a negative set-up cost is an error | 1,000, 2,000, -£5.00, £0.50, half-up | → | error: setupCost must not be negative |
| negative demand is an error | -1, 2,000, £5.00, £0.50, half-up | → | error: annualDemand must be a whole number of units, not negative |
| fractional demand is an error | 10.5, 2,000, £5.00, £0.50, half-up | → | error: annualDemand must be a whole number of units, not negative |
| mixed currencies are an error | 1,000, 2,000, £5.00, €0.50, half-up | → | error: currency mismatch |
| 8DSP beyond 2^63 - 1 is an error | 1,000,000,000, 2,000,000,000, £100,000.00, £0.50, half-up | → | error: 8 x D x S x P must stay within 2^63 - 1 |
| an unknown rounding mode is an error | 1,000, 2,000, £5.00, £0.50, nearest | → | error: unknown rounding mode |
More from the author
`D` is annual demand, `P` the annual production rate (what the process would make in a year of running), `S` the set-up cost of one batch and `H` the cost of holding one unit for a year.
**Why not just EOQ.** Because stock is used while the batch is still being made, the peak stock is `Q * (1 - D/P)`, not `Q`, so the batch is larger than the economic order quantity by `sqrt(P / (P - D))`. With demand 1,000, rate 2,000, set-up £5 and holding 50p, EOQ (`inventory.eoq`) says 141; the EBQ is 200. As `P` grows the EBQ falls back to the EOQ.
**Whole units, rounded exactly.** As in `inventory.eoq`, the root is never taken in floating point. The floor comes from `math.integer-sqrt` applied to `floor(2DSP / (H(P - D)))`, and the rounding decision compares whole numbers, so an exact half (sqrt(6.25) = 2.5) goes by `mode`, not by the language:
- `down`: the floor; `up`: the floor plus one unless the ratio is a perfect square - `half-up`: plus one when `(2n + 1)^2 * H(P - D) <= 8DSP` - `half-even`: as half-up, but an exact half goes to the even neighbour
**Limits.** `8 * D * S * P` must stay within 2^63 - 1 and the ratio under the root within 2^53 - 1 (the range `math.integer-sqrt` takes); both are far beyond real plants. Money is `Money` in minor units, set-up and holding in the same currency; the currencies cancel, so the answer is a plain unit count.
Zero demand gives 0. The production rate must be greater than demand (at or below it the process can never build stock and the formula has no answer), holding cost must be positive, and neither demand nor set-up cost may be negative.
Sources: E. W. Taft, "The most economical production lot", The Iron Age 101, 1918; any operations-management text, e.g. Slack, Brandon-Jones and Johnston, *Operations Management*, chapter on inventory planning (the EBQ model).
Files
| Path | Bytes |
|---|---|
| README.md | 2,196 |
| impl/python.py | 2,345 |
| impl/rust.rs | 3,232 |
| impl/typescript.ts | 2,287 |
| vectors.json | 3,935 |