Functional Weave
Code in Rust

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

  • economic_batch_quantity(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)
  • economic_batch_quantity(1,000, 2,000, £5.00, £0.50, up) → 200 a perfect square stays put when rounding up
  • economic_batch_quantity(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.

pub fn economic_batch_quantity(annual_demand: i64, annual_production_rate: i64, setup_cost: &Money, holding_cost: &Money, mode: &str) -> i64
annual_demandintunits used per year, not negative
annual_production_rateintunits the process makes per year while running; more than annualDemand
setup_costMoneythe fixed cost of setting up one batch, whatever its size
holding_costMoneythe cost of holding one unit for a year; same currency, more than zero
modeRoundingModehow the square root becomes whole units: half-up, half-even, down or up
returnsintthe batch size in whole units

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

fune!(manufacturing.batch-size@^1);  // then call economic_batch_quantity(…)
impl/rust.rs · 96 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_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: i64 = (1i64 << 53) - 1;

/// 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.
///
/// # Panics
/// Panics on negative demand or set-up cost, a production rate not above
/// demand, a holding cost that is not positive, mixed currencies, an unknown
/// mode, or 8DSP beyond 2^63 - 1.
pub fn economic_batch_quantity(
    annual_demand: i64,
    annual_production_rate: i64,
    setup_cost: &Money,
    holding_cost: &Money,
    mode: &str,
) -> i64 {
    if !(0..=MAX_SAFE).contains(&annual_demand) {
        panic!("annualDemand must be a whole number of units, not negative, received {}", annual_demand);
    }
    if annual_production_rate <= annual_demand || annual_production_rate > MAX_SAFE {
        panic!(
            "annualProductionRate must be a whole number greater than annualDemand, received {}",
            annual_production_rate
        );
    }
    assert_same_currency(setup_cost, holding_cost);
    if setup_cost.minor < 0 {
        panic!("setupCost must not be negative, received {}", setup_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 p = annual_production_rate as i128;
    let s = setup_cost.minor as i128;
    let eight = (8 * d)
        .checked_mul(s)
        .and_then(|x| x.checked_mul(p))
        .filter(|x| *x <= i64::MAX as i128)
        .unwrap_or_else(|| {
            panic!("annualDemand, setupCost and annualProductionRate are too large: 8 x D x S x P must stay within 2^63 - 1")
        });
    let two = eight / 4;
    let m = holding_cost.minor as i128 * (p - d);
    let n = integer_sqrt((two / m) as i64) as i128;
    let odd = 2 * n + 1;
    let result = match mode {
        "down" => n,
        "up" => {
            if n * n * m == two {
                n
            } else {
                n + 1
            }
        }
        "half-up" => {
            if odd * odd * m <= eight {
                n + 1
            } else {
                n
            }
        }
        "half-even" => {
            let half = odd * odd * m;
            if half < eight || (half == eight && 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);
    }
    if let Value::Float(f) = &args[1] {
        panic!("annualProductionRate must be a whole number greater than annualDemand, received {}", f);
    }
    Value::Int(economic_batch_quantity(
        args[0].as_i64(),
        args[1].as_i64(),
        &money_from_value(&args[2]),
        &money_from_value(&args[3]),
        args[4].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 manufacturing.batch-size
Download for Rust manufacturing.batch-size-1.0.0-rust.fune · 12,073 bytes sha256 6cf9debe228804a589791d4ab559a322052eeef54545295df8159504334b0ece

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

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.

CaseArgumentsExpected
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
CaseArgumentsExpected
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

PathBytes
README.md2,196
impl/python.py2,345
impl/rust.rs3,232
impl/typescript.ts2,287
vectors.json3,935