math.fractional-power
Raise a positive decimal to a fractional power, (1.199)^(31/365), identically in every language.
1.0.0 · published 2026-10-03 by charlie · Anterra
Pinned by 19 tests, run in TypeScript, Python and Rust.
What it does
Raises a positive decimal to a rational power, x^(p/q), and gets the same digits in TypeScript, Python and Rust. Finance needs this wherever a rate is compounded over part of a year: the consumer-credit APR and early-settlement formulas raise (1 + APR) to the power "days / 365", and a monthly factor is (1 + APR)^(1/12). Those are irrational numbers, so no exact answer exists; floating-point `Math.pow`, `**` and `powf` do not promise the same last digit on every platform, and a settlement figure must not depend on the platform.
## How it is computed
For example
fractionalPower(1.1, 2, 1)→ 1.210000000000 a whole power that is exact in decimalfractionalPower(2, 1, 2)→ 1.414213562373 square root of twofractionalPower(1.199, 1, 12)→ 1.015238935953 monthly factor of a 19.9% annual 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 fractionalPower(base: string, exponentNumerator: number, exponentDenominator: number): string
| base | string | a positive decimal, at most 18 places: "1.199" |
| exponentNumerator | int | -100000 to 100000; negative gives the reciprocal |
| exponentDenominator | int | 1 to 100000; the root taken |
| returns | string | the result to 12 decimal places, half-up, e.g. "1.015521428741" |
Your code names it in one line, in the file that uses it
import { fractionalPower } from "#fune/math.fractional-power@^1";
/** Every value is held as an integer count of 10^-18. */
export const FIXED_SCALE = 10n ** 18n;
/** Intermediate values beyond 2^512 (about 10^136 after scaling) are refused. */
const MAX_BITS = 512;
const DECIMAL = /^[0-9]+(\.[0-9]{1,18})?$/;
function tooLarge(value: bigint): boolean {
return value.toString(2).length > MAX_BITS;
}
/** Product of two non-negative fixed-point values, floored. */
export function mulFixed(a: bigint, b: bigint): bigint {
return (a * b) / FIXED_SCALE;
}
/**
* x^n for a non-negative fixed-point x, by square-and-multiply from the lowest
* bit, flooring after every product. The order of operations is part of the
* contract: it is what makes TypeScript, Python and Rust agree to the last
* digit. Returns bound + 1 as soon as the result must exceed `bound` (x >= 1
* only grows), so a bisection never builds an astronomically large number.
*/
export function powFixedBounded(x: bigint, n: number, bound: bigint | null): bigint {
let result = FIXED_SCALE;
let base = x;
let e = n;
while (e > 0) {
if (e % 2 === 1) {
result = mulFixed(result, base);
if (bound !== null && result > bound && x >= FIXED_SCALE) return bound + 1n;
}
e = Math.floor(e / 2);
if (e > 0) {
base = mulFixed(base, base);
if (bound !== null && base > bound && x >= FIXED_SCALE) return bound + 1n;
if (tooLarge(base)) throw new RangeError("fractional power result too large");
}
}
if (tooLarge(result)) throw new RangeError("fractional power result too large");
return result;
}
/** x^n in fixed point; n is a whole number of 0 or more. */
export function powFixed(x: bigint, n: number): bigint {
return powFixedBounded(x, n, null);
}
/**
* The q-th root of x in fixed point: the largest y with powFixed(y, q) <= x,
* found by bisection. Because powFixed floors, this is the root as this
* arithmetic sees it, identical in every language.
*/
export function rootFixed(x: bigint, q: number): bigint {
if (q === 1) return x;
let lo = 0n;
// For x >= 1, (1 + (x - 1)/q)^q >= x (Bernoulli), so the root is below it.
let hi = x >= FIXED_SCALE ? FIXED_SCALE + (x - FIXED_SCALE) / BigInt(q) + 2n : FIXED_SCALE + 1n;
while (hi - lo > 1n) {
const mid = (lo + hi) / 2n;
if (powFixedBounded(mid, q, x) <= x) lo = mid;
else hi = mid;
}
return lo;
}
/** x^(p/q) in fixed point: the q-th root first, then the power, then the reciprocal if p < 0. */
export function fractionalPowerFixed(x: bigint, p: number, q: number): bigint {
if (!Number.isInteger(p) || p < -100000 || p > 100000) {
throw new RangeError(`exponentNumerator must be between -100000 and 100000, received ${p}`);
}
if (!Number.isInteger(q) || q < 1 || q > 100000) {
throw new RangeError(`exponentDenominator must be between 1 and 100000, received ${q}`);
}
if (x <= 0n) {
throw new RangeError("base must be greater than zero");
}
const root = rootFixed(x, q);
if (p >= 0) return powFixed(root, p);
const denominator = powFixed(root, -p);
if (denominator === 0n) throw new RangeError("fractional power result too large");
const result = (FIXED_SCALE * FIXED_SCALE) / denominator;
if (tooLarge(result)) throw new RangeError("fractional power result too large");
return result;
}
/** Parse a non-negative decimal of at most 18 places into fixed point. */
export function parseFixed(text: string): bigint {
if (typeof text !== "string" || !DECIMAL.test(text)) {
throw new RangeError(`base must be a positive decimal with at most 18 places, received "${text}"`);
}
const [whole, fraction = ""] = text.split(".");
return BigInt(whole) * FIXED_SCALE + BigInt(fraction.padEnd(18, "0"));
}
/**
* base^(exponentNumerator / exponentDenominator), to 12 decimal places.
*
* Computed in 18-place fixed point with a floor after every step, then
* rounded half-up to 12 places, so the last digit printed is right unless the
* true value sits within about 10^-15 of a rounding boundary.
*/
export function fractionalPower(base: string, exponentNumerator: number, exponentDenominator: number): string {
const x = parseFixed(base);
const value = fractionalPowerFixed(x, exponentNumerator, exponentDenominator);
const rounded = (value + 500000n) / 1000000n;
const whole = rounded / 10n ** 12n;
const fraction = (rounded % 10n ** 12n).toString().padStart(12, "0");
return `${whole}.${fraction}`;
}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.fractional-power
The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./math.fractional-power-1.0.0-typescript.fune, or fetch it from a terminal with fune pull math.fractional-power@1.0.0:typescript.
The whole function, every language, is one file too: math.fractional-power-1.0.0.fune, 22,168 bytes, sha256 0066bd743f23291b72f2b4086147d1571e046c6c0b3849aa079fa868ff3e143c. 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.fractional-power
after — your function gets the result and the arguments, and returns the final result.
// fune: after math.fractional-power
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.big-integer in math.fractional-power
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.fractional-power --steps.
// fune: step math.fractional-power 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 | |
|---|---|---|---|
| a whole power that is exact in decimal | 1.1, 2, 1 | → | 1.210000000000 |
| square root of two | 2, 1, 2 | → | 1.414213562373 |
| monthly factor of a 19.9% annual rate | 1.199, 1, 12 | → | 1.015238935953 |
| 31 days of a 19.9% APR on a 365-day year | 1.199, 31, 365 | → | 1.015533447820 |
| a negative exponent discounts: 1/1.05^10 | 1.05, -10, 1 | → | 0.613913253541 |
| three halves power | 1.5, 3, 2 | → | 1.837117307087 |
| a root of a number below one | 0.5, 1, 3 | → | 0.793700525984 |
| one to any power is one | 1, 7, 365 | → | 1.000000000000 |
| reciprocal of ten | 10, -1, 1 | → | 0.100000000000 |
| 30 years of monthly compounding at 5.4% | 1.0045, 360, 1 | → | 5.034760199014 |
Show the other 9 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| a payday-style 1000% APR over 30 days | 11, 30, 365 | → | 1.217850333327 |
| a base with the full 18 decimal places | 1.000000000000000001, 1,000, 1 | → | 1.000000000000 |
| fraction not in lowest terms gives the same answer | 1.199, 62, 730 | → | 1.015533447820 |
| zero base is refused | 0, 1, 2 | → | error: base must be greater than zero |
| a negative base is refused | -1, 1, 1 | → | error: base must be a positive decimal with at most 18 places |
| more than 18 decimal places is refused | 1.1234567890123456789, 1, 1 | → | error: base must be a positive decimal with at most 18 places |
| exponent denominator of zero | 2, 1, 0 | → | error: exponentDenominator must be between 1 and 100000 |
| exponent numerator out of range | 2, 100,001, 1 | → | error: exponentNumerator must be between -100000 and 100000 |
| a result beyond 2^512 is refused | 10, 200, 1 | → | error: fractional power result too large |
More from the author
Everything is an integer count of 10^-18 (18-place fixed point), and every step floors, in a fixed order, so the three languages agree bit for bit:
1. The q-th root: the largest y with y^q <= x, found by bisection. 2. The power: y^|p| by square-and-multiply from the lowest bit, flooring each product. 3. For a negative p, the reciprocal, floored.
The vectored function returns the result rounded half-up to 12 decimal places. Each floor loses under 10^-18, so the 12th place is right unless the true value lies within roughly 10^-15 of a rounding boundary. Exact powers that fit in 18 places come out exact: 1.1^2 is 1.21.
Other capabilities use the fixed-point pieces directly: `FIXED_SCALE`, `mulFixed`, `powFixed`, `powFixedBounded`, `rootFixed`, `fractionalPowerFixed` and `parseFixed` (snake_case in Python and Rust; the Rust versions take `math.big-integer`'s `BigInt`).
## Limits
- The base is a positive decimal with at most 18 places; zero and negatives are refused (a fractional power of a negative number is not real). - The exponent's numerator is -100000 to 100000 and its denominator 1 to 100000. The fraction need not be in lowest terms: 62/730 gives the same answer as 31/365 up to the floors, and in practice the same 12 places. - A value that would pass 2^512 in fixed point (about 10^136) is an error, not a slow computation.
Files
| Path | Bytes |
|---|---|
| README.md | 1,944 |
| impl/python.py | 4,248 |
| impl/rust.rs | 6,009 |
| impl/typescript.ts | 4,416 |
| vectors.json | 2,908 |