Functional Weave
Code in Python

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

  • percent_change(100, 150, half-up) → 5,000 100 to 150 is up 50 percent
  • percent_change(150, 100, half-up) → -3,333 150 to 100 is down 33.33 percent, not the 50 percent it went up
  • percent_change(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.

def percent_change(from_value: int, to_value: int, mode: RoundingMode) -> Optional[int]
from_valueintthe starting value (last month, last year, the old price)
to_valueintthe new value, in the same units
modeRoundingModehow to round to a whole basis point
returnsint?10000 = +100%; null when fromValue is zero

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

from fune.math.percent_change import percent_change  # math.percent-change@^1
impl/python.py · 24 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.

from typing import Optional

from .math_round_div import RoundingMode, round_div  ← from math.round-div ^1.0.0 · built alongside by fune

# (difference * 10000) must stay an exact integer in a JavaScript number, and
# the three languages must agree on what is too large.
MAX_DIFFERENCE = 900719925474


def percent_change(from_value: int, to_value: int, mode: RoundingMode) -> Optional[int]:
    """Percentage change from ``from_value`` to ``to_value``, in basis points.

    Divides by |from_value| so the sign always says which way the value moved,
    even when the starting value is negative.
    """
    for v in (from_value, to_value):
        if isinstance(v, bool) or not isinstance(v, int):
            raise TypeError("percent_change operates on integers only")
    if from_value == 0:
        return None
    difference = to_value - from_value
    if abs(difference) > MAX_DIFFERENCE:
        raise ValueError("the change from %d to %d is too large to express exactly" % (from_value, to_value))
    return round_div(difference * 10000, abs(from_value), mode)

Install

fune build

With that line in your source, in a Python project (language python in fune.project), fune build resolves it and its 1 dependency, pins them in fune.lock, downloads only the Python 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
Download for Python math.percent-change-1.0.0-python.fune · 6,061 bytes sha256 a1a94c9d9b57f701b8dd321e7f57c70517ccdf6886abec8097235a4318a49a30

The manifest, vectors and README with only the Python implementation. Install it without the registry with fune add ./math.percent-change-1.0.0-python.fune, or fetch it from a terminal with fune pull math.percent-change@1.0.0:python.

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.

CaseArgumentsExpected
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
CaseArgumentsExpected
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

PathBytes
README.md1,182
impl/python.py1,014
impl/rust.rs1,270
impl/typescript.ts935
vectors.json2,013