Functional Weave
Code in Rust

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.

pub fn round_div(numerator: i64, denominator: i64, mode: &str) -> i64
numeratorint
denominatorintmust not be zero
modeRoundingMode
returnsint

The type it declares, generated into your project

// RoundingMode is a string in Rust, one of: "half-up", "half-even", "down", "up".
// Parameters take it as &str and results hold it as String.

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

fune!(math.round-div@^1);  // then call round_div(…)
impl/rust.rs · 59 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

/// 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.
///
/// # Panics
/// Panics if `denominator` is zero, or the mode is not recognised.
pub fn round_div(numerator: i64, denominator: i64, mode: &str) -> i64 {
    if denominator == 0 {
        panic!("denominator must not be zero");
    }

    let negative = (numerator < 0) != (denominator < 0);
    let n = numerator.unsigned_abs() as i128;
    let d = denominator.unsigned_abs() as i128;

    let mut quotient = n / d;
    let remainder = n % d;
    let twice = remainder * 2;

    match mode {
        "down" => {}
        "up" => {
            if remainder > 0 {
                quotient += 1;
            }
        }
        "half-even" => {
            if twice > d || (twice == d && quotient % 2 == 1) {
                quotient += 1;
            }
        }
        "half-up" => {
            if twice >= d {
                quotient += 1;
            }
        }
        other => panic!("unknown rounding mode \"{}\"", other),
    }

    let result = quotient as i64;
    if negative {
        -result
    } else {
        result
    }
}

pub fn fune_vector(args: &[Value]) -> Value {
    Value::Int(round_div(
        args[0].as_i64(),
        args[1].as_i64(),
        args[2].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 nothing else, 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 math.round-div
Download for Rust math.round-div-1.0.0-rust.fune · 5,530 bytes sha256 42c6478790a7887fc5f497d8ccbe43d4ed311178d67446089eb5c726f5afdceb

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

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