math.round-div-big
Divide two integers of any size with an explicit rounding mode, on decimal strings, for exact money sums past 2^53.
1.0.0 · published 2026-10-03 by charlie · Anterra
Pinned by 21 tests, run in TypeScript, Python and Rust.
What it does
`math.round-div` for integers of any size. The numerator and denominator are decimal strings (the form `math.big-integer` uses), and so is the answer: `roundDivBig("9007199254740993", "2", "down")` is `"4503599627370496"`.
## Why it exists
For example
round_div_big(7, 2, half-up)→ 4 a half rounds away from zero under half-upround_div_big(-7, 2, half-up)→ -4 a negative half rounds away from zero too, as math.round-div doesround_div_big(5, 2, half-even)→ 2 half-even rounds 2.5 down to the even 2
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_big(numerator: &str, denominator: &str, mode: &str) -> String
| numerator | string | a whole number in decimal: optional "-", digits, no leading zeros |
| denominator | string | a whole number in decimal, not zero |
| mode | RoundingMode | half-up and half-even round ties away from zero and to even; down and up are towards and away from zero |
| returns | string | the rounded quotient in the same decimal form |
Your code names it in one line, in the file that uses it
fune!(math.round-div-big@^1); // then call round_div_big(…)
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_big_integer::{parse_big_integer, BigInt}; ← from math.big-integer ^1.0.0 · built alongside by fune
/// math.round-div, for integers too big for an i64.
///
/// An exact product such as distance x gallon size x price per litre easily
/// passes 2^63, where `round_div` would overflow. The rounding rules are the
/// same as math.round-div's: every mode works on the magnitude and puts the
/// sign back afterwards, so -7/2 half-up is -4.
///
/// # Panics
/// Panics on a malformed number, a zero denominator or an unknown mode.
pub fn round_div_big(numerator: &str, denominator: &str, mode: &str) -> String {
let n0 = parse_big_integer(numerator);
let d0 = parse_big_integer(denominator);
if d0.is_zero() {
panic!("denominator must not be zero");
}
let negative = n0.is_negative() != d0.is_negative();
let n = n0.abs();
let d = d0.abs();
let (mut quotient, remainder) = n.div_rem(&d);
let twice = remainder.add(&remainder);
let one = BigInt::from_i64(1);
match mode {
"down" => {}
"up" => {
if !remainder.is_zero() {
quotient = quotient.add(&one);
}
}
"half-even" => {
let odd = !quotient.rem(&BigInt::from_i64(2)).is_zero();
if twice > d || (twice == d && odd) {
quotient = quotient.add(&one);
}
}
"half-up" => {
if twice >= d {
quotient = quotient.add(&one);
}
}
other => panic!("unknown rounding mode \"{}\"", other),
}
if negative && !quotient.is_zero() {
quotient = quotient.neg();
}
quotient.to_string()
}
pub fn fune_vector(args: &[Value]) -> Value {
for (i, name) in [(0usize, "numerator"), (1usize, "denominator")] {
if !matches!(args[i], Value::Str(_)) {
panic!("not a whole number in decimal: {} must be a string", name);
}
}
Value::str(&round_div_big(args[0].as_str(), args[1].as_str(), 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 its 2 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 math.round-div-big
The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./math.round-div-big-1.0.0-rust.fune, or fetch it from a terminal with fune pull math.round-div-big@1.0.0:rust.
The whole function, every language, is one file too: math.round-div-big-1.0.0.fune, 10,443 bytes, sha256 84e7151555e35d220724d00e6c32107dde025c9fa1ca43935b7e774824f035d7. 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-big
after — your function gets the result and the arguments, and returns the final result.
// fune: after math.round-div-big
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.big-integer in math.round-div-big
// fune: replace math.round-div in math.round-div-big
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-big --steps.
// fune: step math.round-div-big 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 | |
|---|---|---|---|
| a half rounds away from zero under half-up | 7, 2, half-up | → | 4 |
| a negative half rounds away from zero too, as math.round-div does | -7, 2, half-up | → | -4 |
| half-even rounds 2.5 down to the even 2 | 5, 2, half-even | → | 2 |
| half-even rounds 3.5 up to the even 4 | 7, 2, half-even | → | 4 |
| 2^53 + 1 halved, down: a JavaScript number would already have lost the 1 | 9007199254740993, 2, down | → | 4503599627370496 |
| 2^53 + 1 halved, half-up | 9007199254740993, 2, half-up | → | 4503599627370497 |
| thirty digits divided by a thousand, half-up | 123456789012345678901234567890, 1000, half-up | → | 123456789012345678901234568 |
| thirty negative digits divided by a thousand, down is towards zero | -123456789012345678901234567890, 1000, down | → | -123456789012345678901234567 |
| up rounds any remainder away from zero | 1, 3, up | → | 1 |
| up on a negative is away from zero as well | -1, 3, up | → | -1 |
Show the other 11 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| a small negative that rounds to zero is 0, never -0 | -1, 3, half-up | → | 0 |
| a negative denominator makes the quotient negative | 10, -4, half-up | → | -3 |
| zero divided by anything is zero | 0, 5, up | → | 0 |
| an exact tie beyond 64 bits goes to even under half-even | 25000000000000000000, 10000000000000000000, half-even | → | 2 |
| the next tie beyond 64 bits goes up to even | 35000000000000000000, 10000000000000000000, half-even | → | 4 |
| exact division has nothing to round | 100000000000000000000, 4, up | → | 25000000000000000000 |
| a zero denominator is an error | 1, 0, half-up | → | error: denominator must not be zero |
| a decimal point is not a whole number | 1.5, 2, half-up | → | error: not a whole number in decimal |
| a leading zero is refused | 07, 2, half-up | → | error: not a whole number in decimal |
| a number instead of a string is refused | 7, 2, half-up | → | error: not a whole number in decimal |
| an unknown mode is an error | 7, 2, sideways | → | error: unknown rounding mode |
More from the author
Exact money arithmetic multiplies before it divides: litres from a distance and a fuel economy, times a price in tenths of a penny, or a value retained through several years of depreciation at basis-point rates. The product passes 2^53 quickly, and `math.round-div` takes a JavaScript `number` (and a Rust `i64`), which silently loses digits past that point. Doing the one rounding step here keeps the answer exact and identical in all three languages.
## Rounding
Exactly math.round-div's modes, applied to the magnitude with the sign put back afterwards:
- `half-up`: ties away from zero (7/2 is 4, -7/2 is -4); - `half-even`: ties to the even neighbour (5/2 is 2, 7/2 is 4); - `down`: towards zero; - `up`: away from zero.
A negative quotient that rounds to nothing is `"0"`, never `"-0"`.
## Input form
Canonical decimal integers only, as `math.big-integer` parses them: an optional `-`, then digits, no leading zeros, no `+`, no point, no spaces. Anything else is an error, as is a zero denominator or an unknown mode.
Files
| Path | Bytes |
|---|---|
| README.md | 1,294 |
| impl/python.py | 722 |
| impl/rust.rs | 2,020 |
| impl/typescript.ts | 1,387 |
| vectors.json | 2,682 |