Functional Weave
Code in Rust

math.rational

Exact fraction arithmetic, always reduced, for rates and ratios that must not drift.

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

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

What it does

A fraction held as two integers, so 1/3 stays 1/3 and 1/10 + 2/10 is exactly 3/10. Use it for exchange rates, unit conversions and pro-rata factors that are applied more than once: a float rate drifts a little on every step, a rational one never does. Money itself stays in `money.amount` minor units; turn a rational back into an integer with `rationalToInteger` and an explicit rounding mode.

Every result is reduced (numerator and denominator share no factor) and its denominator is positive, so two equal values always have the same fields and compare equal as plain data. Inputs need not be reduced: `{2, -4}` is read as -1/2.

For example

  • calculate_rational(numerator 1, denominator 3, add, numerator 1, denominator 6) → numerator 1, denominator 2 a third plus a sixth is a half
  • calculate_rational(numerator 1, denominator 10, add, numerator 2, denominator 10) → numerator 3, denominator 10 one tenth plus two tenths is exactly three tenths, unlike 0.1 + 0.2
  • calculate_rational(numerator 3, denominator 4, subtract, numerator 5, denominator 4) → numerator -1, denominator 2 subtracting past zero gives a negative numerator

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.

pub fn calculate_rational(a: &Rational, op: &str, b: &Rational) -> Rational
aRationalleft operand; need not be reduced
opRationalOpadd, subtract, multiply or divide
bRationalright operand; need not be reduced
returnsRational

The types it declares, generated into your project

/// An exact fraction. Results are always reduced, with a positive denominator.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Rational {
    /// carries the sign
    pub numerator: i64,
    /// never zero; positive in every result
    pub denominator: i64,
}

// RationalOp is a string in Rust, one of: "add", "subtract", "multiply", "divide".
// Parameters take it as &str and results hold it as String.

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

fune!(math.rational@^1);  // then call calculate_rational(…)
impl/rust.rs · 131 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.

use super::funejson::Value;  ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::math_gcd_lcm::gcd_wide;  ← from math.gcd-lcm ^1.0.0 · built alongside by fune
use super::math_round_div::round_div;  ← from math.round-div ^1.0.0 · built alongside by fune

const MAX_SAFE: i128 = 9_007_199_254_740_991;

/// Reduce a wide fraction and bring it back into the exact number range.
fn reduce(numerator: i128, denominator: i128) -> Rational {
    if denominator == 0 {
        panic!("denominator must not be zero");
    }
    let g = gcd_wide(numerator, denominator);
    let mut n = numerator / g;
    let mut d = denominator / g;
    if d < 0 {
        n = -n;
        d = -d;
    }
    // i64 could hold more, but TypeScript numbers stop being exact here, and
    // the three languages must agree.
    if n > MAX_SAFE || n < -MAX_SAFE || d > MAX_SAFE {
        panic!("rational overflow: the reduced result exceeds 2^53 - 1");
    }
    Rational {
        numerator: n as i64,
        denominator: d as i64,
    }
}

/// Build a reduced fraction with a positive denominator.
///
/// # Panics
/// Panics if the denominator is zero or either part is outside ±(2^53 - 1).
pub fn rational(numerator: i64, denominator: i64) -> Rational {
    let (n, d) = (numerator as i128, denominator as i128);
    if n > MAX_SAFE || n < -MAX_SAFE || d > MAX_SAFE || d < -MAX_SAFE {
        panic!("rational overflow: numerator and denominator must be within 2^53 - 1");
    }
    reduce(n, d)
}

fn parts(r: &Rational) -> (i128, i128) {
    let n = rational(r.numerator, r.denominator);
    (n.numerator as i128, n.denominator as i128)
}

pub fn add_rational(a: &Rational, b: &Rational) -> Rational {
    let (an, ad) = parts(a);
    let (bn, bd) = parts(b);
    reduce(an * bd + bn * ad, ad * bd)
}

pub fn subtract_rational(a: &Rational, b: &Rational) -> Rational {
    let (an, ad) = parts(a);
    let (bn, bd) = parts(b);
    reduce(an * bd - bn * ad, ad * bd)
}

pub fn multiply_rational(a: &Rational, b: &Rational) -> Rational {
    let (an, ad) = parts(a);
    let (bn, bd) = parts(b);
    reduce(an * bn, ad * bd)
}

/// # Panics
/// Panics if `b` is zero.
pub fn divide_rational(a: &Rational, b: &Rational) -> Rational {
    let (an, ad) = parts(a);
    let (bn, bd) = parts(b);
    if bn == 0 {
        panic!("division by zero");
    }
    reduce(an * bd, ad * bn)
}

/// -1, 0 or 1. Exact: cross-multiplied in i128, never through a float.
pub fn compare_rational(a: &Rational, b: &Rational) -> i64 {
    let (an, ad) = parts(a);
    let (bn, bd) = parts(b);
    match (an * bd).cmp(&(bn * ad)) {
        std::cmp::Ordering::Less => -1,
        std::cmp::Ordering::Equal => 0,
        std::cmp::Ordering::Greater => 1,
    }
}

/// The nearest integer under an explicit rounding mode.
pub fn rational_to_integer(r: &Rational, mode: &str) -> i64 {
    let n = rational(r.numerator, r.denominator);
    round_div(n.numerator, n.denominator, mode)
}

/// Add, subtract, multiply or divide two fractions exactly.
///
/// The result is always reduced with a positive denominator, so equal values
/// have equal fields.
///
/// # Panics
/// Panics on a zero denominator, division by zero, an unknown operation, or a
/// result outside ±(2^53 - 1).
pub fn calculate_rational(a: &Rational, op: &str, b: &Rational) -> Rational {
    match op {
        "add" => add_rational(a, b),
        "subtract" => subtract_rational(a, b),
        "multiply" => multiply_rational(a, b),
        "divide" => divide_rational(a, b),
        other => panic!("unknown operation \"{}\"", other),
    }
}

pub fn rational_to_value(r: &Rational) -> Value {
    Value::obj(vec![
        ("numerator", Value::Int(r.numerator)),
        ("denominator", Value::Int(r.denominator)),
    ])
}

pub fn rational_from_value(v: &Value) -> Rational {
    Rational {
        numerator: v.get("numerator").as_i64(),
        denominator: v.get("denominator").as_i64(),
    }
}

pub fn fune_vector(args: &[Value]) -> Value {
    rational_to_value(&calculate_rational(
        &rational_from_value(&args[0]),
        args[1].as_str(),
        &rational_from_value(&args[2]),
    ))
}

Install

fune build

With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and its 2 dependencies, pins them in fune.lock, downloads only the Rust 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. A crate’s build.rs runs it before every compile. Or pin a range in fune.project and build in one step:

fune add math.rational
Download for Rust math.rational-1.0.0-rust.fune · 13,740 bytes sha256 06bd855db2e65a8cdb404698180b1face9e7570bac6d2b32fe76ab17bd304004

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

The whole function, every language, is one file too: math.rational-1.0.0.fune, 20,595 bytes, sha256 525db2981a46569bdeb824d6acadf1eaee1c94a6b4dd0feb5893496901d12f79. 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.rational

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

// fune: after math.rational

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.gcd-lcm in math.rational
// fune: replace math.round-div in math.rational

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.rational --steps.

// fune: step math.rational 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
a third plus a sixth is a half numerator 1, denominator 3, add, numerator 1, denominator 6 → numerator 1, denominator 2
one tenth plus two tenths is exactly three tenths, unlike 0.1 + 0.2 numerator 1, denominator 10, add, numerator 2, denominator 10 → numerator 3, denominator 10
subtracting past zero gives a negative numerator numerator 3, denominator 4, subtract, numerator 5, denominator 4 → numerator -1, denominator 2
equal values subtract to zero over one numerator 1, denominator 2, subtract, numerator 2, denominator 4 → numerator 0, denominator 1
multiplying reduces the result numerator 2, denominator 3, multiply, numerator 9, denominator 4 → numerator 3, denominator 2
dividing by a quarter doubles a half into a whole number numerator 1, denominator 2, divide, numerator 1, denominator 4 → numerator 2, denominator 1
dividing by a negative puts the sign on the numerator numerator 1, denominator 3, divide, numerator -2, denominator 3 → numerator -1, denominator 2
unreduced inputs with a negative denominator are normalised numerator 2, denominator -4, add, numerator 0, denominator 5 → numerator -1, denominator 2
negative over negative is positive numerator -3, denominator -9, multiply, numerator 1, denominator 1 → numerator 1, denominator 3
unreduced products past 2^53 still reduce exactly numerator 9,007,199,254,740,991, denominator 2, multiply, numerator 2, denominator 9,007,199,254,740,991 → numerator 1, denominator 1
Show the other 7 tests
CaseArgumentsExpected
a common denominator past 2^53 before reducing numerator 1, denominator 4,503,599,627,370,496, add, numerator 1, denominator 4,503,599,627,370,496 → numerator 1, denominator 2,251,799,813,685,248
an exchange rate and its inverse multiply to exactly one numerator 11,734, denominator 10,000, multiply, numerator 10,000, denominator 11,734 → numerator 1, denominator 1
a numerator past 2^53 - 1 is an overflow, not a rounded number numerator 9,007,199,254,740,991, denominator 1, add, numerator 1, denominator 1 → error: rational overflow
a denominator past 2^53 - 1 is an overflow numerator 1, denominator 9,007,199,254,740,991, multiply, numerator 1, denominator 2 → error: rational overflow
dividing by zero is an error numerator 1, denominator 2, divide, numerator 0, denominator 5 → error: division by zero
a zero denominator is an error numerator 1, denominator 0, add, numerator 1, denominator 2 → error: denominator must not be zero
an unknown operation is an error numerator 1, denominator 2, power, numerator 1, denominator 2 → error: unknown operation

More from the author

The vectored entry is `calculateRational(a, op, b)`. The module also exports `rational(n, d)` (build and reduce), `addRational`, `subtractRational`, `multiplyRational`, `divideRational`, `compareRational(a, b)` (-1, 0 or 1) and `rationalToInteger(r, mode)` (the `math.round-div` modes), which are the same arithmetic under separate names.

**Limits.** A numerator or denominator must be an integer within ±(2^53 - 1), the range where a JavaScript number is exact, in inputs and in results. Intermediate products are computed wide (TypeScript `bigint`, Python `int`, Rust `i128`) and reduced before the check, so `(2^53-1)/2 × 2/(2^53-1)` is 1 even though the unreduced product is far past the limit. A result that is still too large after reducing is an error ("rational overflow"), never a silently wrong fraction.

Level 1 rather than the catalogue's 0, because it builds on `math.gcd-lcm` and `math.round-div` instead of carrying private copies of them.

Files

PathBytes
README.md1,610
impl/python.py3,128
impl/rust.rs3,979
impl/typescript.ts3,418
vectors.json5,179