money.apply-rate
Apply a rate expressed in basis points to a monetary amount, with an explicit rounding mode.
1.0.1 · published 2026-10-03 by charlie · Anterra
Pinned by 22 tests, run in TypeScript, Python and Rust.
What it does
Rates are integers in basis points, never floats. A VAT rate of 20% is 2000, not 0.2, so the calculation stays in exact integer arithmetic from input to rounded result.
This single function covers VAT, discounts, interest, commission and service charges: they differ in the rate and the rounding policy, not in the arithmetic.
For example
applyRate(£100.00, 20%, half-up)→ £20.00 20 percent VAT on 100 poundsapplyRate(£1.99, 20%, half-up)→ £0.40 20 percent of 1.99 rounds upapplyRate(£49.99, 5%, half-up)→ £2.50 5 percent reduced rate
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.
export function applyRate(amount: Money, basisPoints: number, mode: RoundingMode): Money
| amount | Money | |
| basisPoints | int | 10000 = 100%, 2000 = 20%, 25 = 0.25% |
| mode | RoundingMode | |
| returns | Money |
Your code names it in one line, in the file that uses it
import { applyRate } from "#fune/money.apply-rate@^1";
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
import { type RoundingMode, roundDiv } from "./math_round_div.ts"; ← from math.round-div ^1.0.0 · built alongside by fune
import { type Money, money } from "./money_amount.ts"; ← from money.amount ^1.0.0 · built alongside by fune
/**
* Apply a rate given in basis points (10000 = 100%).
*
* Rates are integers, never floats, so VAT, discounts, interest and
* commission all stay in exact arithmetic right up to the single rounding
* step the caller asked for.
*/
export function applyRate(amount: Money, basisPoints: number, mode: RoundingMode = "half-up"): Money {
if (!Number.isInteger(basisPoints)) {
throw new TypeError(`basisPoints must be an integer, received ${basisPoints}`);
}
return money(roundDiv(amount.minor * basisPoints, 10000, mode), amount.currency);
}
/** The complement: what is left after the rate is taken off. */
export function applyRateComplement(amount: Money, basisPoints: number, mode: RoundingMode = "half-up"): Money {
return money(amount.minor - applyRate(amount, basisPoints, mode).minor, amount.currency);
}Install
fune build
With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and its 2 dependencies, pins them in fune.lock, downloads only the TypeScript 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 money.apply-rate
The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./money.apply-rate-1.0.1-typescript.fune, or fetch it from a terminal with fune pull money.apply-rate@1.0.1:typescript.
The whole function, every language, is one file too: money.apply-rate-1.0.1.fune, 8,862 bytes, sha256 e34dd8f310c3432e1767a88c32e75318e3764f0ec7a0b3f941fa8e19e4e2ce1b. 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.apply-rate
after — your function gets the result and the arguments, and returns the final result.
// fune: after money.apply-rate
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.round-div in money.apply-rate
// fune: replace money.amount in money.apply-rate
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.apply-rate --steps.
// fune: step money.apply-rate 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 | |
|---|---|---|---|
| 20 percent VAT on 100 pounds | £100.00, 20%, half-up | → | £20.00 |
| 20 percent of 1.99 rounds up | £1.99, 20%, half-up | → | £0.40 |
| 5 percent reduced rate | £49.99, 5%, half-up | → | £2.50 |
| exact half rounds up under half-up | £0.05, 50%, half-up | → | £0.03 |
| exact half rounds to even under half-even | £0.05, 50%, half-even | → | £0.02 |
| down never rounds up | £1.99, 20%, down | → | £0.39 |
| zero rate | £123.45, 0%, half-up | → | £0.00 |
| 100 percent is the amount itself | £123.45, 100%, half-up | → | £123.45 |
| quarter of a percent | £1,000.00, 0.25%, half-up | → | £2.50 |
| credits rate symmetrically | -£1.99, 20%, half-up | → | -£0.40 |
Show the other 12 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| a negative half rounds away from zero under half-up, not towards it as Math.round does | -£0.05, 50%, half-up | → | -£0.03 |
| half-even rounds an odd quotient's half up | £0.15, 50%, half-even | → | £0.08 |
| up rounds any fraction away from zero | £1.01, 1%, up | → | £0.02 |
| up on a credit rounds away from zero too | -£1.01, 1%, up | → | -£0.02 |
| down on a credit truncates towards zero | -£1.99, 20%, down | → | -£0.39 |
| a rate above 100 percent | £10.00, 150%, half-up | → | £15.00 |
| a negative rate, as a discount | £19.99, -10%, half-up | → | -£2.00 |
| a large amount stays exact | £9,007,199,254.74, 20%, half-up | → | £1,801,439,850.95 |
| zero-decimal currency | ¥1,234, 10%, half-even | → | ¥123 |
| a zero amount is zero at any rate | ¥0, 20%, up | → | ¥0 |
| an unknown rounding mode is an error, not a default | £1.00, 20%, nearest | → | error: unknown rounding mode |
| an invalid currency on the amount is an error | 1.00 gbp, 20%, half-up | → | error: ISO 4217 |
More from the author
1.0.1 adds tests; behaviour unchanged.
Files
| Path | Bytes |
|---|---|
| README.md | 388 |
| impl/python.py | 948 |
| impl/rust.rs | 1,001 |
| impl/typescript.ts | 952 |
| vectors.json | 3,543 |