Functional Weave
Code in Python

math.basis-points

Convert a rate between percent, basis points and a plain ratio exactly, as decimal text or integer basis points.

1.0.0 (not the latest) · published 2026-10-03 by charlie · Anterra

Pinned by 19 tests, run in TypeScript, Python and Rust.

What it does

100 basis points is 1 percent is a ratio of 0.01. The three units differ only by powers of ten, so converting between them is moving a decimal point, and this does exactly that on the text of the number. `"0.07"` as a ratio is `"7"` percent, where `0.07 * 100` in floating point is `7.000000000000001`.

Values are decimal strings in and out, never floats, so any rate a person can write down converts without loss and without a size limit. Output is the shortest form: no leading zeros, no trailing fractional zeros, no trailing point, and zero is `"0"` (never `"-0"`).

For example

  • convert_rate(12.5, percent, basis-points) → 1250 12.5 percent is 1250 basis points
  • convert_rate(1250, basis-points, percent) → 12.5 1250 basis points is 12.5 percent
  • convert_rate(0.125, ratio, percent) → 12.5 a ratio of 0.125 is 12.5 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 convert_rate(value: str, from_unit: RateUnit, to_unit: RateUnit) -> str
valuestringa plain decimal: digits, an optional leading "-", an optional "." with digits after it
from_unitRateUnitthe unit value is in
to_unitRateUnitthe unit wanted
returnsstringthe same rate in the new unit, as the shortest exact decimal

The type it declares, generated into your project

RateUnit = Literal["percent", "basis-points", "ratio"]

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

from fune.math.basis_points import convert_rate  # math.basis-points@^1
impl/python.py · 64 lines · open · raw
import re

from .math_basis_points_types import RateUnit

# Explicit [0-9]: Python's \d also matches other scripts' digits.
DECIMAL = re.compile(r"^-?[0-9]+(\.[0-9]+)?\Z")
MAX_SAFE = 9007199254740991


def _exponent(unit: str) -> int:
    """Powers of ten between each unit and basis points."""
    if unit == "basis-points":
        return 0
    if unit == "percent":
        return 2
    if unit == "ratio":
        return 4
    raise ValueError('unknown rate unit "%s"' % (unit,))


def convert_rate(value: str, from_unit: RateUnit, to_unit: RateUnit) -> str:
    """Convert a rate between percent, basis points and a ratio.

    The units differ by powers of ten, so this moves the decimal point in the
    text itself: exact for any input, with no float anywhere.
    """
    if not isinstance(value, str) or not DECIMAL.match(value):
        raise ValueError('"%s" is not a decimal number' % (value,))
    shift = _exponent(from_unit) - _exponent(to_unit)

    negative = value.startswith("-")
    body = value[1:] if negative else value
    point = body.find(".")
    digits = body if point < 0 else body[:point] + body[point + 1 :]
    position = (len(body) if point < 0 else point) + shift

    if position > len(digits):
        digits = digits + "0" * (position - len(digits))
    if position < 0:
        digits = "0" * (-position) + digits
        position = 0

    whole = digits[:position].lstrip("0") or "0"
    fraction = digits[position:].rstrip("0")
    text = whole + "." + fraction if fraction else whole
    return "-" + text if negative and text != "0" else text


def to_basis_points(value: str, unit: RateUnit) -> int:
    """An integer number of basis points, refusing a rate that is not whole."""
    text = convert_rate(value, unit, "basis-points")
    if "." in text:
        raise ValueError("%s %s is not a whole number of basis points" % (value, unit))
    bp = int(text)
    if bp > MAX_SAFE or bp < -MAX_SAFE:
        raise ValueError("%s %s is too large for integer basis points" % (value, unit))
    return bp


def from_basis_points(basis_points: int, unit: RateUnit) -> str:
    """Render integer basis points in another unit."""
    if isinstance(basis_points, bool) or not isinstance(basis_points, int):
        raise TypeError("basis_points must be an integer, received %r" % (basis_points,))
    return convert_rate(str(basis_points), "basis-points", unit)

Install

fune build

With that line in your source, in a Python project (language python in fune.project), fune build resolves it and nothing else, 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.basis-points
Download for Python math.basis-points-1.0.0-python.fune · 8,138 bytes sha256 3504c6c7c7ab0a64afa8de33508ff28f098922c8b8ac166088e1d6439a1e78d7

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

The whole function, every language, is one file too: math.basis-points-1.0.0.fune, 14,007 bytes, sha256 7ee7e2aa9137812ddceea028ea4164333ce73b3305eb9db7a5ccd4a4259d0e1b. 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.basis-points

after — your function gets the result and the arguments, and returns the final result.

# fune: after math.basis-points

replace — it requires no other capability, so there is no dependency to replace.

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.basis-points --steps.

# fune: step math.basis-points 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
12.5 percent is 1250 basis points 12.5, percent, basis-points → 1250
1250 basis points is 12.5 percent 1250, basis-points, percent → 12.5
a ratio of 0.125 is 12.5 percent 0.125, ratio, percent → 12.5
20 percent is a ratio of 0.2 20, percent, ratio → 0.2
one basis point is a ratio of 0.0001 1, basis-points, ratio → 0.0001
half a basis point as a ratio needs leading zeros 0.5, basis-points, ratio → 0.00005
0.07 as a ratio is exactly 7 percent, not 7.000000000000001 0.07, ratio, percent → 7
a negative rate keeps its sign -2.75, percent, basis-points → -275
same unit gives the canonical form 007.500, percent, percent → 7.5
zero with trailing zeros is plain zero 0.00, ratio, basis-points → 0
Show the other 9 tests
CaseArgumentsExpected
negative zero is zero -0.0, percent, ratio → 0
a ratio of one is 10000 basis points 1, ratio, basis-points → 10000
numbers beyond any float or integer convert exactly 123456789012345678901.23, percent, basis-points → 12345678901234567890123
a percent sign is not part of the number 12.5%, percent, basis-points → error: is not a decimal number
exponent notation is refused 1e3, basis-points, percent → error: is not a decimal number
a bare leading point is refused .5, percent, ratio → error: is not a decimal number
a decimal comma is refused 1,5, percent, ratio → error: is not a decimal number
an empty string is refused , percent, ratio → error: is not a decimal number
an unknown unit is an error 5, permille, percent → error: unknown rate unit

More from the author

Input is strict: an optional leading `-`, digits, and optionally `.` followed by digits. `"12.5%"`, `"+5"`, `".5"`, `"5."`, `"1e3"`, `"1,5"` and spaces are all errors. Strip a `%` sign before calling; this is not a parser for free-form text.

The registry keeps rates as integer basis points, so two helpers are exported for the common edges: `toBasisPoints(value, unit)` returns an integer and refuses a rate that is not a whole number of basis points (12.345% is 1234.5 basis points, which no integer holds) or is beyond ±(2^53 - 1), and `fromBasisPoints(bp, unit)` renders an integer basis-point rate as a decimal string.

Files

PathBytes
README.md1,219
impl/python.py2,399
impl/rust.rs3,181
impl/typescript.ts2,412
vectors.json2,292