Functional Weave
Code in Python

math.rational

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

2.0.0 · published 2026-10-03 by charlie · Anterra

Pinned by 86 tests, run in TypeScript, Python and Rust.calculateRational 17 · rational 11 · addRational 9 · subtractRational 9 · multiplyRational 8 · divideRational 9 · compareRational 9 · rationalToInteger 14

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.

The functions

A group: 8 functions that work together, each in its own file, each pinned by its own tests in TypeScript, Python and Rust. A project can install only the ones it calls.

  1. calculate_rational (a: Rational, op: RationalOp, b: Rational) -> Rational
  2. rational (numerator: int, denominator: int) -> Rational
  3. add_rational (a: Rational, b: Rational) -> Rational
  4. subtract_rational (a: Rational, b: Rational) -> Rational
  5. multiply_rational (a: Rational, b: Rational) -> Rational
  6. divide_rational (a: Rational, b: Rational) -> Rational
  7. compare_rational (a: Rational, b: Rational) -> int
  8. rational_to_integer (r: Rational, mode: RoundingMode) -> int

The types it declares, generated into your project

@dataclass(frozen=True)
class Rational:
    """An exact fraction. Results are always reduced, with a positive denominator."""

    #: carries the sign
    numerator: int
    #: never zero; positive in every result
    denominator: int

RationalOp = Literal["add", "subtract", "multiply", "divide"]

Once installed, your code imports each one from the group's module.

calculate_rational throws on bad input 17 tests

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

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
from fune.math.rational import calculate_rational  # math.rational@^2
impl/python/calculate_rational.py · 22 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 .math_rational_add_rational import add_rational  ← addRational, another function of this group · built into the same file, even by a slim install
from .math_rational_divide_rational import divide_rational  ← divideRational, another function of this group · built into the same file, even by a slim install
from .math_rational_multiply_rational import multiply_rational  ← multiplyRational, another function of this group · built into the same file, even by a slim install
from .math_rational_subtract_rational import subtract_rational  ← subtractRational, another function of this group · built into the same file, even by a slim install
from .math_rational_types import Rational, RationalOp


def calculate_rational(a: Rational, op: RationalOp, b: Rational) -> Rational:
    """Add, subtract, multiply or divide two fractions exactly.

    The result is always reduced with a positive denominator, so equal values
    have equal fields.
    """
    if op == "add":
        return add_rational(a, b)
    if op == "subtract":
        return subtract_rational(a, b)
    if op == "multiply":
        return multiply_rational(a, b)
    if op == "divide":
        return divide_rational(a, b)
    raise ValueError('unknown operation "%s"' % (op,))

rational throws on bad input 11 tests

def rational(numerator: int, denominator: int) -> Rational
numeratorintcarries the sign; within ±(2^53 - 1)
denominatorintnot zero; may be negative; within ±(2^53 - 1)
returnsRationalreduced, with a positive denominator

For example

  • rational(6, 8) → numerator 3, denominator 4 six eighths reduces to three quarters
  • rational(3, 7) → numerator 3, denominator 7 already reduced is unchanged
  • rational(3, -9) → numerator -1, denominator 3 a negative denominator moves the sign to the numerator
from fune.math.rational import rational  # math.rational@^2
impl/python/rational.py · 42 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 Tuple

from .math_gcd_lcm import gcd_wide  ← from math.gcd-lcm ^1.0.0 · built alongside by fune
from .math_rational_types import Rational

_MAX_SAFE = 9007199254740991


def rational_from_wide(numerator: int, denominator: int) -> Rational:
    """Reduce a wide fraction and bring it back into the exact number range.

    The arithmetic works on products that may be past 2^53, and only the
    reduced result has to fit.
    """
    if denominator == 0:
        raise ValueError("denominator must not be zero")
    g = gcd_wide(numerator, denominator)
    n = numerator // g
    d = denominator // g
    if d < 0:
        n, d = -n, -d
    # Python ints never overflow, but TypeScript numbers stop being exact here,
    # and the three languages must agree.
    if n > _MAX_SAFE or n < -_MAX_SAFE or d > _MAX_SAFE:
        raise ValueError("rational overflow: the reduced result exceeds 2^53 - 1")
    return Rational(numerator=n, denominator=d)


def rational(numerator: int, denominator: int) -> Rational:
    """Build a reduced fraction with a positive denominator."""
    for v in (numerator, denominator):
        if isinstance(v, bool) or not isinstance(v, int):
            raise TypeError("numerator and denominator must be integers")
        if v > _MAX_SAFE or v < -_MAX_SAFE:
            raise ValueError("rational overflow: numerator and denominator must be within 2^53 - 1")
    return rational_from_wide(numerator, denominator)


def rational_parts(r: Rational) -> Tuple[int, int]:
    """A checked, reduced operand as (numerator, denominator)."""
    n = rational(r.numerator, r.denominator)
    return n.numerator, n.denominator

add_rational throws on bad input 9 tests

def add_rational(a: Rational, b: Rational) -> Rational
aRational
bRational
returnsRational

For example

  • add_rational(numerator 1, denominator 2, numerator 1, denominator 3) → numerator 5, denominator 6 a half plus a third is five sixths
  • add_rational(numerator 1, denominator 10, numerator 2, denominator 10) → numerator 3, denominator 10 a tenth plus two tenths is exactly three tenths
  • add_rational(numerator 3, denominator 4, numerator -3, denominator 4) → numerator 0, denominator 1 opposites sum to zero over one
from fune.math.rational import add_rational  # math.rational@^2
impl/python/add_rational.py · 9 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 .math_rational_rational import rational_from_wide, rational_parts  ← rational, another function of this group · built into the same file, even by a slim install
from .math_rational_types import Rational


def add_rational(a: Rational, b: Rational) -> Rational:
    """a + b, reduced."""
    an, ad = rational_parts(a)
    bn, bd = rational_parts(b)
    return rational_from_wide(an * bd + bn * ad, ad * bd)

subtract_rational throws on bad input 9 tests

def subtract_rational(a: Rational, b: Rational) -> Rational
aRational
bRational
returnsRationala - b

For example

  • subtract_rational(numerator 3, denominator 4, numerator 1, denominator 4) → numerator 1, denominator 2 three quarters less a quarter is a half
  • subtract_rational(numerator 1, denominator 3, numerator 1, denominator 2) → numerator -1, denominator 6 going below zero gives a negative numerator
  • subtract_rational(numerator 2, denominator 6, numerator 1, denominator 3) → numerator 0, denominator 1 equal values give zero over one
from fune.math.rational import subtract_rational  # math.rational@^2
impl/python/subtract_rational.py · 9 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 .math_rational_rational import rational_from_wide, rational_parts  ← rational, another function of this group · built into the same file, even by a slim install
from .math_rational_types import Rational


def subtract_rational(a: Rational, b: Rational) -> Rational:
    """a - b, reduced."""
    an, ad = rational_parts(a)
    bn, bd = rational_parts(b)
    return rational_from_wide(an * bd - bn * ad, ad * bd)

multiply_rational throws on bad input 8 tests

def multiply_rational(a: Rational, b: Rational) -> Rational
aRational
bRational
returnsRational

For example

  • multiply_rational(numerator 2, denominator 3, numerator 3, denominator 4) → numerator 1, denominator 2 two thirds of three quarters is a half
  • multiply_rational(numerator 11,734, denominator 10,000, numerator 10,000, denominator 11,734) → numerator 1, denominator 1 a rate and its inverse multiply to exactly one
  • multiply_rational(numerator -2, denominator 5, numerator -5, denominator 4) → numerator 1, denominator 2 a negative times a negative is positive
from fune.math.rational import multiply_rational  # math.rational@^2
impl/python/multiply_rational.py · 9 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 .math_rational_rational import rational_from_wide, rational_parts  ← rational, another function of this group · built into the same file, even by a slim install
from .math_rational_types import Rational


def multiply_rational(a: Rational, b: Rational) -> Rational:
    """a × b, reduced."""
    an, ad = rational_parts(a)
    bn, bd = rational_parts(b)
    return rational_from_wide(an * bn, ad * bd)

divide_rational throws on bad input 9 tests

def divide_rational(a: Rational, b: Rational) -> Rational
aRational
bRationalmust not be zero
returnsRationala / b

For example

  • divide_rational(numerator 1, denominator 2, numerator 1, denominator 4) → numerator 2, denominator 1 a half divided by a quarter is two
  • divide_rational(numerator 3, denominator 5, numerator 9, denominator 10) → numerator 2, denominator 3 dividing by a fraction multiplies by its inverse
  • divide_rational(numerator 1, denominator 3, numerator -2, denominator 3) → numerator -1, denominator 2 dividing by a negative puts the sign on the numerator
from fune.math.rational import divide_rational  # math.rational@^2
impl/python/divide_rational.py · 11 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 .math_rational_rational import rational_from_wide, rational_parts  ← rational, another function of this group · built into the same file, even by a slim install
from .math_rational_types import Rational


def divide_rational(a: Rational, b: Rational) -> Rational:
    """a ÷ b, reduced. Dividing by zero is an error, never an infinity."""
    an, ad = rational_parts(a)
    bn, bd = rational_parts(b)
    if bn == 0:
        raise ValueError("division by zero")
    return rational_from_wide(an * bd, ad * bn)

compare_rational throws on bad input 9 tests

def compare_rational(a: Rational, b: Rational) -> int
aRational
bRational
returnsint-1 if a is less than b, 0 if equal, 1 if greater

For example

  • compare_rational(numerator 1, denominator 3, numerator 1, denominator 2) → -1 a third is less than a half
  • compare_rational(numerator 1, denominator 2, numerator 1, denominator 3) → 1 a half is more than a third
  • compare_rational(numerator 2, denominator 4, numerator -3, denominator -6) → 0 equal values in different forms are equal
from fune.math.rational import compare_rational  # math.rational@^2
impl/python/compare_rational.py · 11 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 .math_rational_rational import rational_parts  ← rational, another function of this group · built into the same file, even by a slim install
from .math_rational_types import Rational


def compare_rational(a: Rational, b: Rational) -> int:
    """-1, 0 or 1. Exact: cross-multiplied, never through a float."""
    an, ad = rational_parts(a)
    bn, bd = rational_parts(b)
    left = an * bd
    right = bn * ad
    return -1 if left < right else 1 if left > right else 0

rational_to_integer throws on bad input 14 tests

def rational_to_integer(r: Rational, mode: RoundingMode) -> int
rRational
modeRoundingModehow to round a value that is not whole, as math.round-div does
returnsintthe integer r rounds to

For example

  • rational_to_integer(numerator 7, denominator 2, half-up) → 4 seven halves rounds half up to 4
  • rational_to_integer(numerator 5, denominator 2, half-even) → 2 five halves rounds half even to 2
  • rational_to_integer(numerator 7, denominator 2, half-even) → 4 seven halves rounds half even to 4
from fune.math.rational import rational_to_integer  # math.rational@^2
impl/python/rational_to_integer.py · 13 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 .math_rational_rational import rational  ← rational, another function of this group · built into the same file, even by a slim install
from .math_rational_types import Rational
from .math_round_div import RoundingMode, round_div  ← from math.round-div ^1.0.0 · built alongside by fune


def rational_to_integer(r: Rational, mode: RoundingMode) -> int:
    """The integer a fraction rounds to, under an explicit rounding mode.

    This is the point where an exact rate becomes minor units, so the policy
    is the caller's to state.
    """
    n = rational(r.numerator, r.denominator)
    return round_div(n.numerator, n.denominator, 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 2 dependencies, 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.rational

That builds the whole group. To build only what you call, and whatever it uses inside the group:

fune add math.rational --only calculateRational
Download for Python math.rational-2.0.0-python.fune · 35,969 bytes sha256 f0e2d1248313ccb2b0c6b90eb659b2eca0fed44d326ebb06f52548459ee7cf4e

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

The whole function, every language, is one file too: math.rational-2.0.0.fune, 50,001 bytes, sha256 0a32a648ef33659cc1265ea863eaf465419d3a5a7145b44278f83bdbcd1ace30. 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.calculateRational
# fune: before math.rational.rational
# fune: before math.rational.addRational
# fune: before math.rational.subtractRational
# fune: before math.rational.multiplyRational
# fune: before math.rational.divideRational
# fune: before math.rational.compareRational
# fune: before math.rational.rationalToInteger

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

# fune: after math.rational.calculateRational
# fune: after math.rational.rational
# fune: after math.rational.addRational
# fune: after math.rational.subtractRational
# fune: after math.rational.multiplyRational
# fune: after math.rational.divideRational
# fune: after math.rational.compareRational
# fune: after math.rational.rationalToInteger

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 a 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.<fn> 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.

calculateRational 17 tests

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

rational 11 tests

CaseArgumentsExpected
six eighths reduces to three quarters 6, 8 → numerator 3, denominator 4
already reduced is unchanged 3, 7 → numerator 3, denominator 7
a negative denominator moves the sign to the numerator 3, -9 → numerator -1, denominator 3
negative over negative is positive -10, -4 → numerator 5, denominator 2
zero is zero over one, whatever the denominator 0, -17 → numerator 0, denominator 1
a whole number has denominator one 12, 4 → numerator 3, denominator 1
the largest safe parts reduce to one 9,007,199,254,740,991, -9,007,199,254,740,991 → numerator -1, denominator 1
the largest safe numerator over one 9,007,199,254,740,991, 1 → numerator 9,007,199,254,740,991, denominator 1
a zero denominator is an error 5, 0 → error: denominator must not be zero
a numerator past 2^53 - 1 is an overflow 9,007,199,254,740,992, 3 → error: rational overflow
Show the other 1 test
CaseArgumentsExpected
a fractional part is an error 1.5, 2 → error: must be integers

addRational 9 tests

CaseArgumentsExpected
a half plus a third is five sixths numerator 1, denominator 2, numerator 1, denominator 3 → numerator 5, denominator 6
a tenth plus two tenths is exactly three tenths numerator 1, denominator 10, numerator 2, denominator 10 → numerator 3, denominator 10
opposites sum to zero over one numerator 3, denominator 4, numerator -3, denominator 4 → numerator 0, denominator 1
a negative plus a smaller positive stays negative numerator -5, denominator 6, numerator 1, denominator 3 → numerator -1, denominator 2
unreduced inputs with negative denominators numerator 4, denominator -8, numerator 6, denominator -9 → numerator -7, denominator 6
adding zero gives the other, reduced numerator 10, denominator 15, numerator 0, denominator 7 → numerator 2, denominator 3
a common denominator past 2^53 reduces back into range numerator 1, denominator 4,503,599,627,370,496, numerator 1, denominator 4,503,599,627,370,496 → numerator 1, denominator 2,251,799,813,685,248
a sum past 2^53 - 1 is an overflow numerator 9,007,199,254,740,991, denominator 1, numerator 1, denominator 1 → error: rational overflow
a zero denominator is an error numerator 1, denominator 2, numerator 1, denominator 0 → error: denominator must not be zero

subtractRational 9 tests

CaseArgumentsExpected
three quarters less a quarter is a half numerator 3, denominator 4, numerator 1, denominator 4 → numerator 1, denominator 2
going below zero gives a negative numerator numerator 1, denominator 3, numerator 1, denominator 2 → numerator -1, denominator 6
equal values give zero over one numerator 2, denominator 6, numerator 1, denominator 3 → numerator 0, denominator 1
subtracting a negative adds numerator 1, denominator 4, numerator -1, denominator 4 → numerator 1, denominator 2
zero less a fraction is its negative numerator 0, denominator 1, numerator 5, denominator 8 → numerator -5, denominator 8
two nearly equal fractions differ by a millionth of a millionth numerator 999,999, denominator 1,000,000, numerator 999,998, denominator 999,999 → numerator 1, denominator 999,999,000,000
a difference whose reduced denominator is past 2^53 - 1 is an overflow numerator 9,007,199,254,740,990, denominator 9,007,199,254,740,991, numerator 9,007,199,254,740,989, denominator 9,007,199,254,740,990 → error: rational overflow
a difference past -(2^53 - 1) is an overflow numerator -9,007,199,254,740,991, denominator 1, numerator 1, denominator 1 → error: rational overflow
a numerator past 2^53 - 1 in an operand is an overflow numerator 9,007,199,254,740,992, denominator 1, numerator 0, denominator 1 → error: rational overflow

multiplyRational 8 tests

CaseArgumentsExpected
two thirds of three quarters is a half numerator 2, denominator 3, numerator 3, denominator 4 → numerator 1, denominator 2
a rate and its inverse multiply to exactly one numerator 11,734, denominator 10,000, numerator 10,000, denominator 11,734 → numerator 1, denominator 1
a negative times a negative is positive numerator -2, denominator 5, numerator -5, denominator 4 → numerator 1, denominator 2
a negative times a positive is negative numerator 7, denominator 3, numerator -3, denominator 14 → numerator -1, denominator 2
anything times zero is zero over one numerator 123, denominator 456, numerator 0, denominator -9 → numerator 0, denominator 1
unreduced products past 2^53 still reduce exactly numerator 9,007,199,254,740,991, denominator 2, numerator 2, denominator 9,007,199,254,740,991 → numerator 1, denominator 1
a denominator past 2^53 - 1 is an overflow numerator 1, denominator 9,007,199,254,740,991, numerator 1, denominator 2 → error: rational overflow
a zero denominator is an error numerator 1, denominator 0, numerator 1, denominator 2 → error: denominator must not be zero

divideRational 9 tests

CaseArgumentsExpected
a half divided by a quarter is two numerator 1, denominator 2, numerator 1, denominator 4 → numerator 2, denominator 1
dividing by a fraction multiplies by its inverse numerator 3, denominator 5, numerator 9, denominator 10 → numerator 2, denominator 3
dividing by a negative puts the sign on the numerator numerator 1, denominator 3, numerator -2, denominator 3 → numerator -1, denominator 2
a negative divided by a negative is positive numerator -4, denominator 7, numerator -8, denominator 21 → numerator 3, denominator 2
zero divided by anything is zero numerator 0, denominator 3, numerator 5, denominator 2 → numerator 0, denominator 1
a value divided by itself is one numerator 9,007,199,254,740,991, denominator 9,007,199,254,740,990, numerator 9,007,199,254,740,991, denominator 9,007,199,254,740,990 → numerator 1, denominator 1
dividing by zero is an error, not infinity numerator 1, denominator 2, numerator 0, denominator 5 → error: division by zero
a quotient past 2^53 - 1 is an overflow numerator 9,007,199,254,740,991, denominator 1, numerator 1, denominator 2 → error: rational overflow
a zero denominator in the divisor is an error numerator 1, denominator 2, numerator 3, denominator 0 → error: denominator must not be zero

compareRational 9 tests

CaseArgumentsExpected
a third is less than a half numerator 1, denominator 3, numerator 1, denominator 2 → -1
a half is more than a third numerator 1, denominator 2, numerator 1, denominator 3 → 1
equal values in different forms are equal numerator 2, denominator 4, numerator -3, denominator -6 → 0
a negative is less than zero numerator -1, denominator 1,000,000, numerator 0, denominator 1 → -1
the larger debt is less numerator -3, denominator 4, numerator -2, denominator 3 → -1
fractions a float could not tell apart numerator 9,007,199,254,740,990, denominator 9,007,199,254,740,991, numerator 9,007,199,254,740,989, denominator 9,007,199,254,740,990 → 1
the largest safe values at opposite signs numerator -9,007,199,254,740,991, denominator 1, numerator 9,007,199,254,740,991, denominator 1 → -1
a zero denominator is an error numerator 1, denominator 0, numerator 1, denominator 2 → error: denominator must not be zero
a part past 2^53 - 1 is an overflow numerator 1, denominator 2, numerator 1, denominator 9,007,199,254,740,992 → error: rational overflow

rationalToInteger 14 tests

CaseArgumentsExpected
seven halves rounds half up to 4 numerator 7, denominator 2, half-up → 4
five halves rounds half even to 2 numerator 5, denominator 2, half-even → 2
seven halves rounds half even to 4 numerator 7, denominator 2, half-even → 4
down rounds toward zero numerator 29, denominator 10, down → 2
up rounds away from zero numerator 21, denominator 10, up → 3
a negative half rounds half up away from zero numerator -5, denominator 2, half-up → -3
a negative rounds down toward zero numerator -29, denominator 10, down → -2
a negative rounds up away from zero numerator -21, denominator 10, up → -3
a whole number is not rounded up numerator 12, denominator 4, up → 3
zero is zero numerator 0, denominator 5, half-up → 0
Show the other 4 tests
CaseArgumentsExpected
just under a half rounds down numerator 4,999, denominator 10,000, half-up → 0
the largest safe numerator over two numerator 9,007,199,254,740,991, denominator 2, half-even → 4,503,599,627,370,496
a zero denominator is an error numerator 1, denominator 0, half-up → error: denominator must not be zero
an unknown rounding mode is an error numerator 1, denominator 2, nearest → error: unknown rounding mode

More from the author

The functions:

- `rational(n, d)` builds and reduces a fraction. - `addRational`, `subtractRational`, `multiplyRational` and `divideRational` do the arithmetic; dividing by zero is an error ("division by zero"). - `calculateRational(a, op, b)` is the same four operations chosen by name, for callers that hold the operation as data. - `compareRational(a, b)` is -1, 0 or 1, exact by cross-multiplying wide, never through a float, so fractions a double cannot tell apart still compare correctly. - `rationalToInteger(r, mode)` rounds to an integer with a `math.round-div` mode: `half-up` and `up` round away from zero, `down` toward zero, `half-even` to the even neighbour on a tie.

**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.

## What changed from 1.0.0

1.0.0 was one function, `calculateRational`, with `rational`, the four operations, `compareRational` and `rationalToInteger` exported beside it but unpinned: no signatures in the manifest and no vectors. 2.0.0 is a group of eight functions, each with its own signature, file and vectors; `calculateRational` keeps every 1.0.0 vector. Each file is its own module: the operations import `rational`'s file for the shared reduction (`rationalFromWide`, `rationalParts`, and in Rust `rational_to_value` and `rational_from_value`), and `calculateRational` imports the four operations, so `only=compareRational` installs just that and `rational`.

No answer changed. The Rust adapters now refuse a fractional numerator or denominator with the "must be integers" wording TypeScript and Python use (`rational_from_value` used to truncate it). It is a new major version because the package's shape and public surface changed: it installs as one module per function plus the group module (`math_rational` still re-exports every function and helper), a project can take only some of it, the Python module no longer exports its `MAX_SAFE` constant, and the formerly private reduction helpers are now public under new names. Dependents stay on `^1.0.0` until they move deliberately; 1.0.0 is unchanged.

It still requires `math.gcd-lcm ^1.0.0`, not `^2.0.0`. It imports only `gcdWide`, which is identical in both, so there is nothing to gain from the new major, and staying on `^1.0.0` lets a project that also has an older dependent of `math.gcd-lcm ^1.0.0` resolve one flat graph.

Files

PathBytes
README.md3,564
impl/python/add_rational.py317
impl/python/calculate_rational.py843
impl/python/compare_rational.py381
impl/python/divide_rational.py421
impl/python/multiply_rational.py313
impl/python/rational.py1,611
impl/python/rational_to_integer.py496
impl/python/subtract_rational.py322
impl/rust/add_rational.rs587
impl/rust/calculate_rational.rs1,179
impl/rust/compare_rational.rs708
impl/rust/divide_rational.rs709
impl/rust/multiply_rational.rs588
impl/rust/rational.rs2,681
impl/rust/rational_to_integer.rs702
impl/rust/subtract_rational.rs597
impl/typescript/add_rational.ts358
impl/typescript/calculate_rational.ts922
impl/typescript/compare_rational.ts436
impl/typescript/divide_rational.ts470
impl/typescript/multiply_rational.ts354
impl/typescript/rational.ts1,727
impl/typescript/rational_to_integer.ts539
impl/typescript/subtract_rational.ts363
vectors.json16,879