Functional Weave
Code in TypeScript

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. calculateRational (a: Rational, op: RationalOp, b: Rational) -> Rational
  2. rational (numerator: int, denominator: int) -> Rational
  3. addRational (a: Rational, b: Rational) -> Rational
  4. subtractRational (a: Rational, b: Rational) -> Rational
  5. multiplyRational (a: Rational, b: Rational) -> Rational
  6. divideRational (a: Rational, b: Rational) -> Rational
  7. compareRational (a: Rational, b: Rational) -> int
  8. rationalToInteger (r: Rational, mode: RoundingMode) -> int

The types it declares, generated into your project

/** An exact fraction. Results are always reduced, with a positive denominator. */
export interface Rational {
  /** carries the sign */
  readonly numerator: number;
  /** never zero; positive in every result */
  readonly denominator: number;
}

export type RationalOp = "add" | "subtract" | "multiply" | "divide";

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

calculateRational throws on bad input 17 tests

export function calculateRational(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

  • calculateRational(numerator 1, denominator 3, add, numerator 1, denominator 6) → numerator 1, denominator 2 a third plus a sixth is a half
  • calculateRational(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
  • calculateRational(numerator 3, denominator 4, subtract, numerator 5, denominator 4) → numerator -1, denominator 2 subtracting past zero gives a negative numerator
import { calculateRational } from "#fune/math.rational@^2";
impl/typescript/calculate_rational.ts · 26 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.

import { addRational } from "./math_rational_add_rational.ts";  ← addRational, another function of this group · built into the same file, even by a slim install
import { divideRational } from "./math_rational_divide_rational.ts";  ← divideRational, another function of this group · built into the same file, even by a slim install
import { multiplyRational } from "./math_rational_multiply_rational.ts";  ← multiplyRational, another function of this group · built into the same file, even by a slim install
import { subtractRational } from "./math_rational_subtract_rational.ts";  ← subtractRational, another function of this group · built into the same file, even by a slim install
import { type Rational, type RationalOp } from "./math_rational_types.ts";

/**
 * Add, subtract, multiply or divide two fractions exactly.
 *
 * The result is always reduced with a positive denominator, so equal values
 * have equal fields.
 */
export function calculateRational(a: Rational, op: RationalOp, b: Rational): Rational {
  switch (op) {
    case "add":
      return addRational(a, b);
    case "subtract":
      return subtractRational(a, b);
    case "multiply":
      return multiplyRational(a, b);
    case "divide":
      return divideRational(a, b);
    default:
      throw new RangeError(`unknown operation "${op}"`);
  }
}

rational throws on bad input 11 tests

export function rational(numerator: number, denominator: number): 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
import { rational } from "#fune/math.rational@^2";
impl/typescript/rational.ts · 44 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.

import { gcdWide } from "./math_gcd_lcm.ts";  ← from math.gcd-lcm ^1.0.0 · built alongside by fune
import { type Rational } from "./math_rational_types.ts";

const MAX_SAFE = BigInt(Number.MAX_SAFE_INTEGER);

/**
 * Reduce a wide fraction and bring it back into the exact number range. The
 * arithmetic works on bigint products, which may be past 2^53, and only the
 * reduced result has to fit.
 */
export function rationalFromWide(numerator: bigint, denominator: bigint): Rational {
  if (denominator === 0n) {
    throw new RangeError("denominator must not be zero");
  }
  const g = gcdWide(numerator, denominator);
  let n = numerator / g;
  let d = denominator / g;
  if (d < 0n) {
    n = -n;
    d = -d;
  }
  if (n > MAX_SAFE || n < -MAX_SAFE || d > MAX_SAFE) {
    throw new RangeError("rational overflow: the reduced result exceeds 2^53 - 1");
  }
  // Number(-0n) is 0, so a zero result never comes back as -0.
  return { numerator: Number(n), denominator: Number(d) };
}

/** Build a reduced fraction with a positive denominator. */
export function rational(numerator: number, denominator: number): Rational {
  if (!Number.isInteger(numerator) || !Number.isInteger(denominator)) {
    throw new TypeError("numerator and denominator must be integers");
  }
  if (!Number.isSafeInteger(numerator) || !Number.isSafeInteger(denominator)) {
    throw new RangeError("rational overflow: numerator and denominator must be within 2^53 - 1");
  }
  return rationalFromWide(BigInt(numerator), BigInt(denominator));
}

/** A checked, reduced operand as wide integers, ready to multiply without overflow. */
export function rationalParts(r: Rational): [bigint, bigint] {
  const n = rational(r.numerator, r.denominator);
  return [BigInt(n.numerator), BigInt(n.denominator)];
}

addRational throws on bad input 9 tests

export function addRational(a: Rational, b: Rational): Rational
aRational
bRational
returnsRational

For example

  • addRational(numerator 1, denominator 2, numerator 1, denominator 3) → numerator 5, denominator 6 a half plus a third is five sixths
  • addRational(numerator 1, denominator 10, numerator 2, denominator 10) → numerator 3, denominator 10 a tenth plus two tenths is exactly three tenths
  • addRational(numerator 3, denominator 4, numerator -3, denominator 4) → numerator 0, denominator 1 opposites sum to zero over one
import { addRational } from "#fune/math.rational@^2";
impl/typescript/add_rational.ts · 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.

import { rationalFromWide, rationalParts } from "./math_rational_rational.ts";  ← rational, another function of this group · built into the same file, even by a slim install
import { type Rational } from "./math_rational_types.ts";

/** a + b, reduced. */
export function addRational(a: Rational, b: Rational): Rational {
  const [an, ad] = rationalParts(a);
  const [bn, bd] = rationalParts(b);
  return rationalFromWide(an * bd + bn * ad, ad * bd);
}

subtractRational throws on bad input 9 tests

export function subtractRational(a: Rational, b: Rational): Rational
aRational
bRational
returnsRationala - b

For example

  • subtractRational(numerator 3, denominator 4, numerator 1, denominator 4) → numerator 1, denominator 2 three quarters less a quarter is a half
  • subtractRational(numerator 1, denominator 3, numerator 1, denominator 2) → numerator -1, denominator 6 going below zero gives a negative numerator
  • subtractRational(numerator 2, denominator 6, numerator 1, denominator 3) → numerator 0, denominator 1 equal values give zero over one
import { subtractRational } from "#fune/math.rational@^2";
impl/typescript/subtract_rational.ts · 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.

import { rationalFromWide, rationalParts } from "./math_rational_rational.ts";  ← rational, another function of this group · built into the same file, even by a slim install
import { type Rational } from "./math_rational_types.ts";

/** a - b, reduced. */
export function subtractRational(a: Rational, b: Rational): Rational {
  const [an, ad] = rationalParts(a);
  const [bn, bd] = rationalParts(b);
  return rationalFromWide(an * bd - bn * ad, ad * bd);
}

multiplyRational throws on bad input 8 tests

export function multiplyRational(a: Rational, b: Rational): Rational
aRational
bRational
returnsRational

For example

  • multiplyRational(numerator 2, denominator 3, numerator 3, denominator 4) → numerator 1, denominator 2 two thirds of three quarters is a half
  • multiplyRational(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
  • multiplyRational(numerator -2, denominator 5, numerator -5, denominator 4) → numerator 1, denominator 2 a negative times a negative is positive
import { multiplyRational } from "#fune/math.rational@^2";
impl/typescript/multiply_rational.ts · 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.

import { rationalFromWide, rationalParts } from "./math_rational_rational.ts";  ← rational, another function of this group · built into the same file, even by a slim install
import { type Rational } from "./math_rational_types.ts";

/** a × b, reduced. */
export function multiplyRational(a: Rational, b: Rational): Rational {
  const [an, ad] = rationalParts(a);
  const [bn, bd] = rationalParts(b);
  return rationalFromWide(an * bn, ad * bd);
}

divideRational throws on bad input 9 tests

export function divideRational(a: Rational, b: Rational): Rational
aRational
bRationalmust not be zero
returnsRationala / b

For example

  • divideRational(numerator 1, denominator 2, numerator 1, denominator 4) → numerator 2, denominator 1 a half divided by a quarter is two
  • divideRational(numerator 3, denominator 5, numerator 9, denominator 10) → numerator 2, denominator 3 dividing by a fraction multiplies by its inverse
  • divideRational(numerator 1, denominator 3, numerator -2, denominator 3) → numerator -1, denominator 2 dividing by a negative puts the sign on the numerator
import { divideRational } from "#fune/math.rational@^2";
impl/typescript/divide_rational.ts · 12 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.

import { rationalFromWide, rationalParts } from "./math_rational_rational.ts";  ← rational, another function of this group · built into the same file, even by a slim install
import { type Rational } from "./math_rational_types.ts";

/** a ÷ b, reduced. Dividing by zero is an error, never an infinity. */
export function divideRational(a: Rational, b: Rational): Rational {
  const [an, ad] = rationalParts(a);
  const [bn, bd] = rationalParts(b);
  if (bn === 0n) {
    throw new RangeError("division by zero");
  }
  return rationalFromWide(an * bd, ad * bn);
}

compareRational throws on bad input 9 tests

export function compareRational(a: Rational, b: Rational): number
aRational
bRational
returnsint-1 if a is less than b, 0 if equal, 1 if greater

For example

  • compareRational(numerator 1, denominator 3, numerator 1, denominator 2) → -1 a third is less than a half
  • compareRational(numerator 1, denominator 2, numerator 1, denominator 3) → 1 a half is more than a third
  • compareRational(numerator 2, denominator 4, numerator -3, denominator -6) → 0 equal values in different forms are equal
import { compareRational } from "#fune/math.rational@^2";
impl/typescript/compare_rational.ts · 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.

import { rationalParts } from "./math_rational_rational.ts";  ← rational, another function of this group · built into the same file, even by a slim install
import { type Rational } from "./math_rational_types.ts";

/** -1, 0 or 1. Exact: cross-multiplied wide, never through a float. */
export function compareRational(a: Rational, b: Rational): number {
  const [an, ad] = rationalParts(a);
  const [bn, bd] = rationalParts(b);
  const left = an * bd;
  const right = bn * ad;
  return left < right ? -1 : left > right ? 1 : 0;
}

rationalToInteger throws on bad input 14 tests

export function rationalToInteger(r: Rational, mode: RoundingMode): number
rRational
modeRoundingModehow to round a value that is not whole, as math.round-div does
returnsintthe integer r rounds to

For example

  • rationalToInteger(numerator 7, denominator 2, half-up) → 4 seven halves rounds half up to 4
  • rationalToInteger(numerator 5, denominator 2, half-even) → 2 five halves rounds half even to 2
  • rationalToInteger(numerator 7, denominator 2, half-even) → 4 seven halves rounds half even to 4
import { rationalToInteger } from "#fune/math.rational@^2";
impl/typescript/rational_to_integer.ts · 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.

import { type RoundingMode, roundDiv } from "./math_round_div.ts";  ← from math.round-div ^1.0.0 · built alongside by fune
import { rational } from "./math_rational_rational.ts";  ← rational, another function of this group · built into the same file, even by a slim install
import { type Rational } from "./math_rational_types.ts";

/**
 * The integer a fraction rounds to, under an explicit rounding mode: the
 * point where an exact rate becomes minor units, so the policy is the
 * caller's to state.
 */
export function rationalToInteger(r: Rational, mode: RoundingMode): number {
  const n = rational(r.numerator, r.denominator);
  return roundDiv(n.numerator, n.denominator, mode);
}

Install

fune build

With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and its 2 dependencies, 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.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 TypeScript math.rational-2.0.0-typescript.fune · 36,457 bytes sha256 154a2a10e41824d5780f89ae65b8f9240a14ae25b37920f96cc6c87b810e4958

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

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