math.round-div
Integer division with an explicit rounding mode, for money arithmetic that must not drift.
1.0.0 · published 2026-10-03 by charlie · Anterra
Pinned by 14 tests, run in TypeScript, Python and Rust.
What it does
half-up rounds a half away from zero, which is what UK tax guidance and most invoicing systems assume.
half-even is banker's rounding, used where repeated rounding must not bias upward.
For example
round_div(100, 4, half-up)→ 25 exact divisionround_div(7, 2, half-up)→ 4 half rounds away from zeroround_div(-7, 2, half-up)→ -4 negative half rounds away from zero
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.
def round_div(numerator: int, denominator: int, mode: RoundingMode) -> int
| numerator | int | |
| denominator | int | must not be zero |
| mode | RoundingMode | |
| returns | int |
The type it declares, generated into your project
RoundingMode = Literal["half-up", "half-even", "down", "up"]
Your code names it in one line, in the file that uses it
from fune.math.round_div import round_div # math.round-div@^1
from .math_round_div_types import RoundingMode
def round_div(numerator: int, denominator: int, mode: RoundingMode = "half-up") -> int:
"""Divide two integers, rounding according to ``mode``.
Money is held in minor units (pence, cents) as integers. Every rate,
discount and tax calculation eventually needs to divide, and that division
is where rounding policy lives. Making it explicit here means a system can
state its policy once instead of scattering ``round()`` through a codebase.
"""
if isinstance(numerator, bool) or isinstance(denominator, bool):
raise TypeError("round_div operates on integers only")
if not isinstance(numerator, int) or not isinstance(denominator, int):
raise TypeError("round_div operates on integers only")
if denominator == 0:
raise ValueError("denominator must not be zero")
negative = (numerator < 0) != (denominator < 0)
n = abs(numerator)
d = abs(denominator)
quotient, remainder = divmod(n, d)
twice = remainder * 2
if mode == "down":
pass
elif mode == "up":
if remainder > 0:
quotient += 1
elif mode == "half-even":
if twice > d or (twice == d and quotient % 2 == 1):
quotient += 1
elif mode == "half-up":
if twice >= d:
quotient += 1
else:
raise ValueError('unknown rounding mode "%s"' % mode)
return -quotient if negative else quotientInstall
fune build
With that line in your source, in a Python project (language python in fune.project), fune build resolves it and nothing else, pins them in fune.lock, downloads only the Python 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 math.round-div
The manifest, vectors and README with only the Python implementation. Install it without the registry with fune add ./math.round-div-1.0.0-python.fune, or fetch it from a terminal with fune pull math.round-div@1.0.0:python.
The whole function, every language, is one file too: math.round-div-1.0.0.fune, 8,574 bytes, sha256 080fff23c095f85b327bfd1e4e45f2dd32088a6ece3c4bfdb3629ee684b44b73. 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 math.round-div
after — your function gets the result and the arguments, and returns the final result.
# fune: after math.round-div
replace — it requires no other capability, so there is no dependency to replace.
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 math.round-div --steps.
# fune: step math.round-div 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 | |
|---|---|---|---|
| exact division | 100, 4, half-up | → | 25 |
| half rounds away from zero | 7, 2, half-up | → | 4 |
| negative half rounds away from zero | -7, 2, half-up | → | -4 |
| below half rounds down | 7, 3, half-up | → | 2 |
| banker's rounding keeps 2 even | 5, 2, half-even | → | 2 |
| banker's rounding keeps 4 even | 7, 2, half-even | → | 4 |
| banker's rounding away from an odd quotient | 11, 2, half-even | → | 6 |
| down truncates toward zero | 199, 100, down | → | 1 |
| down truncates negatives toward zero | -199, 100, down | → | -1 |
| up rounds away from zero on any remainder | 101, 100, up | → | 2 |
Show the other 4 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| negative divisor keeps the sign rule | 7, -2, half-up | → | -4 |
| zero numerator | 0, 7, half-up | → | 0 |
| 20 percent of 199 pence | 3,980, 100, half-up | → | 40 |
| divide by zero is an error | 1, 0, half-up | → | error: denominator must not be zero |
More from the author
down truncates toward zero.
Every mode is defined on integers only: no float ever touches a monetary amount.
Files
| Path | Bytes |
|---|---|
| README.md | 316 |
| impl/python.py | 1,461 |
| impl/rust.rs | 1,603 |
| impl/typescript.ts | 1,423 |
| vectors.json | 1,806 |