Functional Weave
Code in Python

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 division
  • round_div(7, 2, half-up) → 4 half rounds away from zero
  • round_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
numeratorint
denominatorintmust not be zero
modeRoundingMode
returnsint

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
impl/python.py · 40 lines · open · raw
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 quotient

Install

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
Download for Python math.round-div-1.0.0-python.fune · 5,381 bytes sha256 6e40f34991aa09add1bd0f538a46ed372e7ef44394ee07deaa5d604698dce76b

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.

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

PathBytes
README.md316
impl/python.py1,461
impl/rust.rs1,603
impl/typescript.ts1,423
vectors.json1,806