money.allocate
Split an amount across ratios without losing or inventing a single minor unit.
1.0.1 · published 2026-10-03 by charlie · Anterra
Pinned by 17 tests, run in TypeScript, Python and Rust.
What it does
Splitting 10.00 three ways gives 3.34, 3.33, 3.33 - never 3.33 three times with a penny quietly lost, and never 3.34 three times with a penny invented.
The leftover is distributed by largest remainder, ties broken by position, so the same input always produces the same split in every language.
For example
allocate(£10.00, 1, 1, 1)→ £3.34, £3.33, £3.33 ten pounds three ways keeps the pennyallocate(£5.00, 3, 7)→ £1.50, £3.50 exact split needs no remainderallocate(£100.00, 1, 1, 1, 1, 1, 1)→ £16.67, £16.67, £16.67, £16.67, £16.66, £16.66 weighted split distributes by largest remainder
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 allocate(amount: &Money, ratios: &[i64]) -> Vec<Money>
| amount | Money | |
| ratios | int[] | relative weights; [1,1,1] is an even three-way split |
| returns | Money[] |
Your code names it in one line, in the file that uses it
fune!(money.allocate@^1); // then call allocate(…)
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::money_amount::{money, money_from_value, money_to_value, Money}; ← from money.amount ^1.0.0 · built alongside by fune
/// Split `amount` across `ratios` so the parts add back up to the whole.
///
/// Splitting 10.00 three ways gives 3.34, 3.33, 3.33 - never 3.33 three times
/// with a penny quietly lost. The leftover goes to the largest remainders,
/// ties broken by position, so the split is stable and reproducible.
///
/// # Panics
/// Panics if `ratios` is empty or sums to zero.
pub fn allocate(amount: &Money, ratios: &[i64]) -> Vec<Money> {
if ratios.is_empty() {
panic!("allocate needs at least one ratio");
}
let total: i64 = ratios.iter().sum();
if total == 0 {
panic!("ratios must not sum to zero");
}
let mut bases: Vec<i64> = Vec::with_capacity(ratios.len());
let mut remainders: Vec<i64> = Vec::with_capacity(ratios.len());
let total128 = total as i128;
for ratio in ratios {
// Euclidean division keeps the remainder non-negative, which is the
// invariant the largest-remainder pass below relies on, and matches
// floor division in the TypeScript and Python implementations.
let numerator = (amount.minor as i128) * (*ratio as i128);
let base = numerator.div_euclid(total128);
bases.push(base as i64);
remainders.push(numerator.rem_euclid(total128) as i64);
}
let mut leftover = amount.minor - bases.iter().sum::<i64>();
let mut order: Vec<usize> = (0..ratios.len()).collect();
order.sort_by(|a, b| remainders[*b].cmp(&remainders[*a]).then(a.cmp(b)));
for index in order {
if leftover <= 0 {
break;
}
bases[index] += 1;
leftover -= 1;
}
bases
.iter()
.map(|base| money(*base, &amount.currency))
.collect()
}
pub fn fune_vector(args: &[Value]) -> Value {
let ratios: Vec<i64> = args[1].as_arr().iter().map(|v| v.as_i64()).collect();
Value::Arr(
allocate(&money_from_value(&args[0]), &ratios)
.iter()
.map(money_to_value)
.collect(),
)
}Install
fune build
With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and its 1 dependency, 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 money.allocate
The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./money.allocate-1.0.1-rust.fune, or fetch it from a terminal with fune pull money.allocate@1.0.1:rust.
The whole function, every language, is one file too: money.allocate-1.0.1.fune, 10,802 bytes, sha256 9cce7c55a15100ba63fdb2177af7103771921fe17b05187a707c3473df1440ee. 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 money.allocate
after — your function gets the result and the arguments, and returns the final result.
// fune: after money.allocate
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 money.amount in money.allocate
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 money.allocate --steps.
// fune: step money.allocate 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 | |
|---|---|---|---|
| ten pounds three ways keeps the penny | £10.00, 1, 1, 1 | → | £3.34, £3.33, £3.33 |
| exact split needs no remainder | £5.00, 3, 7 | → | £1.50, £3.50 |
| weighted split distributes by largest remainder | £100.00, 1, 1, 1, 1, 1, 1 | → | £16.67, £16.67, £16.67, £16.67, £16.66, £16.66 |
| credit notes split without inventing money | -£10.00, 1, 1, 1 | → | -£3.33, -£3.33, -£3.34 |
| single share takes everything | £9.99, 1 | → | £9.99 |
| zero ratios are an error | £1.00, 0, 0 | → | error: ratios must not sum to zero |
| empty ratios are an error | £1.00, | → | error: at least one ratio |
| the leftover penny goes to the largest remainder, not the first share | £1.00, 1, 2 | → | £0.33, £0.67 |
| one penny three ways: only the first share gets it | £0.01, 1, 1, 1 | → | £0.01, £0.00, £0.00 |
| two pennies three ways: ties go in order | £0.02, 1, 1, 1 | → | £0.01, £0.01, £0.00 |
Show the other 7 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| a zero ratio gets nothing | £10.00, 1, 0, 1 | → | £5.00, £0.00, £5.00 |
| a zero amount splits into zeros | £0.00, 1, 2 | → | £0.00, £0.00 |
| a weighted split with a remainder | £10.01, 1, 2, 3, 4 | → | £1.00, £2.00, £3.00, £4.01 |
| a weighted credit still adds back to the whole | -£1.00, 1, 2 | → | -£0.33, -£0.67 |
| zero-decimal currency | ¥1,000, 1, 1, 1 | → | ¥334, ¥333, ¥333 |
| the largest safe integer halves without losing a unit | £90,071,992,547,409.91, 1, 1 | → | £45,035,996,273,704.96, £45,035,996,273,704.95 |
| ratios that cancel out are an error | £1.00, 1, -1 | → | error: ratios must not sum to zero |
More from the author
1.0.1 adds tests; behaviour unchanged.
Files
| Path | Bytes |
|---|---|
| README.md | 354 |
| impl/python.py | 1,279 |
| impl/rust.rs | 2,108 |
| impl/typescript.ts | 1,414 |
| vectors.json | 3,682 |