math.percent-change
Percentage change from one value to another, in basis points, with an explicit rounding mode.
1.0.0 · published 2026-10-03 by charlie · Anterra
Pinned by 19 tests, run in TypeScript, Python and Rust.
What it does
`(toValue - fromValue) / |fromValue|`, in basis points: 100 to 150 is 5000 (+50%), 150 to 100 is -3333 (-33.33% half-up). The inputs are integers in any unit - minor units of money, counts, basis points - and the rounding to a whole basis point uses the `math.round-div` mode you pass, so no float is involved.
**Dividing by the absolute value.** From -100 to -50 is an improvement, and this returns +5000 for it. Dividing by the signed starting value would report -5000, a fall, for a loss that halved. The sign of the result is always the direction of the change.
For example
percentChange(100, 150, half-up)→ 5,000 100 to 150 is up 50 percentpercentChange(150, 100, half-up)→ -3,333 150 to 100 is down 33.33 percent, not the 50 percent it went uppercentChange(200, 100, half-up)→ -5,000 halving is down 50 percent
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 percentChange(fromValue: number, toValue: number, mode: RoundingMode): number | null
| fromValue | int | the starting value (last month, last year, the old price) |
| toValue | int | the new value, in the same units |
| mode | RoundingMode | how to round to a whole basis point |
| returns | int? | 10000 = +100%; null when fromValue is zero |
Your code names it in one line, in the file that uses it
import { percentChange } from "#fune/math.percent-change@^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
// (difference * 10000) must stay an exact integer in a JavaScript number.
const MAX_DIFFERENCE = 900719925474;
/**
* Percentage change from `fromValue` to `toValue`, in basis points.
*
* Divides by |fromValue| so the sign always says which way the value moved,
* even when the starting value is negative.
*/
export function percentChange(fromValue: number, toValue: number, mode: RoundingMode): number | null {
if (!Number.isSafeInteger(fromValue) || !Number.isSafeInteger(toValue)) {
throw new TypeError("percentChange operates on integers only");
}
if (fromValue === 0) return null;
const difference = toValue - fromValue;
if (Math.abs(difference) > MAX_DIFFERENCE) {
throw new RangeError(`the change from ${fromValue} to ${toValue} is too large to express exactly`);
}
return roundDiv(difference * 10000, Math.abs(fromValue), mode);
}Install
fune build
With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and its 1 dependency, 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 math.percent-change
The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./math.percent-change-1.0.0-typescript.fune, or fetch it from a terminal with fune pull math.percent-change@1.0.0:typescript.
The whole function, every language, is one file too: math.percent-change-1.0.0.fune, 8,362 bytes, sha256 133895b99e1f08eef17414ed38f294e326208c6c9c87007a408d2bb44d6c4c5c. 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.percent-change
after — your function gets the result and the arguments, and returns the final result.
// fune: after math.percent-change
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 math.percent-change
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.percent-change --steps.
// fune: step math.percent-change 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 | |
|---|---|---|---|
| 100 to 150 is up 50 percent | 100, 150, half-up | → | 5,000 |
| 150 to 100 is down 33.33 percent, not the 50 percent it went up | 150, 100, half-up | → | -3,333 |
| halving is down 50 percent | 200, 100, half-up | → | -5,000 |
| doubling is up 100 percent | 2,500, 5,000, half-up | → | 10,000 |
| no change is zero | 100, 100, half-up | → | 0 |
| falling to zero is down 100 percent | 999, 0, half-up | → | -10,000 |
| 3 to 4 is 3333.33 basis points, rounded half-up | 3, 4, half-up | → | 3,333 |
| 3 to 4 rounded up | 3, 4, up | → | 3,334 |
| 3 to 2 rounded up is away from zero | 3, 2, up | → | -3,334 |
| an exact half basis point rounds away from zero under half-up | 20,000, 20,001, half-up | → | 1 |
Show the other 9 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| an exact half basis point rounds to even under half-even | 20,000, 20,001, half-even | → | 0 |
| a loss that halves is an improvement, not a fall | -100, -50, half-up | → | 5,000 |
| a loss that grows is a fall | -100, -150, half-up | → | -5,000 |
| from a loss to a profit | -50, 50, half-up | → | 20,000 |
| from zero there is no percentage change | 0, 500, half-up | → | — |
| from zero to zero is still undefined | 0, 0, half-up | → | — |
| the largest difference that is still exact | 1, 900,719,925,475, down | → | 9,007,199,254,740,000 |
| a larger difference is an error, not a rounded answer | 1, 900,719,925,476, half-up | → | error: too large to express exactly |
| an unknown rounding mode is an error | 100, 150, nearest | → | error: unknown rounding mode |
More from the author
**From zero** there is no percentage change: growth from nothing is not infinite percent, and it is not zero percent either. The result is null, as `finance.margin` does for its undefined ratios, so a dashboard can show "new" or "n/a" rather than a number that means nothing. Treating null as 0 will understate.
**Limits.** `|toValue - fromValue|` must be at most 900,719,925,474 (so that the difference times 10000 is still an exact integer in JavaScript); a larger difference is an error rather than a rounded answer. Values in minor units up to nine billion pounds are well inside that.
Files
| Path | Bytes |
|---|---|
| README.md | 1,182 |
| impl/python.py | 1,014 |
| impl/rust.rs | 1,270 |
| impl/typescript.ts | 935 |
| vectors.json | 2,013 |