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.
- calculate_rational (a: Rational, op: RationalOp, b: Rational) -> Rational
- rational (numerator: int, denominator: int) -> Rational
- add_rational (a: Rational, b: Rational) -> Rational
- subtract_rational (a: Rational, b: Rational) -> Rational
- multiply_rational (a: Rational, b: Rational) -> Rational
- divide_rational (a: Rational, b: Rational) -> Rational
- compare_rational (a: Rational, b: Rational) -> int
- rational_to_integer (r: Rational, mode: RoundingMode) -> int
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.
Once installed, your code imports each one from the group's module.
calculate_rational throws on bad input 17 tests
pub fn calculate_rational(a: &Rational, op: &str, b: &Rational) -> Rational
| a | Rational | left operand; need not be reduced |
| op | RationalOp | add, subtract, multiply or divide |
| b | Rational | right operand; need not be reduced |
| returns | Rational |
For example
calculate_rational(numerator 1, denominator 3, add, numerator 1, denominator 6)→ numerator 1, denominator 2 a third plus a sixth is a halfcalculate_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.2calculate_rational(numerator 3, denominator 4, subtract, numerator 5, denominator 4)→ numerator -1, denominator 2 subtracting past zero gives a negative numerator
fune!(math.rational@^2); // then call calculate_rational(…)
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_rational_add_rational::add_rational; ← addRational, another function of this group · built into the same file, even by a slim install
use super::math_rational_divide_rational::divide_rational; ← divideRational, another function of this group · built into the same file, even by a slim install
use super::math_rational_multiply_rational::multiply_rational; ← multiplyRational, another function of this group · built into the same file, even by a slim install
use super::math_rational_rational::{rational_from_value, rational_to_value}; ← rational, another function of this group · built into the same file, even by a slim install
use super::math_rational_subtract_rational::subtract_rational; ← subtractRational, another function of this group · built into the same file, even by a slim install
/// 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 fune_vector(args: &[Value]) -> Value {
rational_to_value(&calculate_rational(
&rational_from_value(&args[0]),
args[1].as_str(),
&rational_from_value(&args[2]),
))
}rational throws on bad input 11 tests
pub fn rational(numerator: i64, denominator: i64) -> Rational
| numerator | int | carries the sign; within ±(2^53 - 1) |
| denominator | int | not zero; may be negative; within ±(2^53 - 1) |
| returns | Rational | reduced, with a positive denominator |
For example
rational(6, 8)→ numerator 3, denominator 4 six eighths reduces to three quartersrational(3, 7)→ numerator 3, denominator 7 already reduced is unchangedrational(3, -9)→ numerator -1, denominator 3 a negative denominator moves the sign to the numerator
fune!(math.rational@^2); // then call rational(…)
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
const MAX_SAFE: i128 = 9_007_199_254_740_991;
/// Reduce a wide fraction and bring it back into the exact number range. The
/// arithmetic works on i128 products, which may be past 2^53, and only the
/// reduced result has to fit.
///
/// # Panics
/// Panics if the denominator is zero or the reduced result is outside ±(2^53 - 1).
pub fn rational_from_wide(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");
}
rational_from_wide(n, d)
}
/// A checked, reduced operand as i128s, ready to multiply without overflow.
pub fn rational_parts(r: &Rational) -> (i128, i128) {
let n = rational(r.numerator, r.denominator);
(n.numerator as i128, n.denominator as i128)
}
pub fn rational_to_value(r: &Rational) -> Value {
Value::obj(vec![
("numerator", Value::Int(r.numerator)),
("denominator", Value::Int(r.denominator)),
])
}
/// Read an integer from JSON, refusing a fraction with the wording TypeScript
/// and Python use rather than let `as_i64` quietly truncate it.
pub fn rational_int_from_value(v: &Value) -> i64 {
if let Value::Float(f) = v {
if f.fract() != 0.0 {
panic!("numerator and denominator must be integers");
}
}
v.as_i64()
}
pub fn rational_from_value(v: &Value) -> Rational {
Rational {
numerator: rational_int_from_value(v.get("numerator")),
denominator: rational_int_from_value(v.get("denominator")),
}
}
pub fn fune_vector(args: &[Value]) -> Value {
rational_to_value(&rational(
rational_int_from_value(&args[0]),
rational_int_from_value(&args[1]),
))
}add_rational throws on bad input 9 tests
pub fn add_rational(a: &Rational, b: &Rational) -> Rational
| a | Rational | |
| b | Rational | |
| returns | Rational |
For example
add_rational(numerator 1, denominator 2, numerator 1, denominator 3)→ numerator 5, denominator 6 a half plus a third is five sixthsadd_rational(numerator 1, denominator 10, numerator 2, denominator 10)→ numerator 3, denominator 10 a tenth plus two tenths is exactly three tenthsadd_rational(numerator 3, denominator 4, numerator -3, denominator 4)→ numerator 0, denominator 1 opposites sum to zero over one
fune!(math.rational@^2); // then call add_rational(…)
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_rational_rational::{rational_from_value, rational_from_wide, rational_parts, rational_to_value}; ← rational, another function of this group · built into the same file, even by a slim install
/// a + b, reduced.
///
/// # Panics
/// Panics on a zero denominator or a result outside ±(2^53 - 1).
pub fn add_rational(a: &Rational, b: &Rational) -> Rational {
let (an, ad) = rational_parts(a);
let (bn, bd) = rational_parts(b);
rational_from_wide(an * bd + bn * ad, ad * bd)
}
pub fn fune_vector(args: &[Value]) -> Value {
rational_to_value(&add_rational(&rational_from_value(&args[0]), &rational_from_value(&args[1])))
}subtract_rational throws on bad input 9 tests
pub fn subtract_rational(a: &Rational, b: &Rational) -> Rational
| a | Rational | |
| b | Rational | |
| returns | Rational | a - b |
For example
subtract_rational(numerator 3, denominator 4, numerator 1, denominator 4)→ numerator 1, denominator 2 three quarters less a quarter is a halfsubtract_rational(numerator 1, denominator 3, numerator 1, denominator 2)→ numerator -1, denominator 6 going below zero gives a negative numeratorsubtract_rational(numerator 2, denominator 6, numerator 1, denominator 3)→ numerator 0, denominator 1 equal values give zero over one
fune!(math.rational@^2); // then call subtract_rational(…)
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_rational_rational::{rational_from_value, rational_from_wide, rational_parts, rational_to_value}; ← rational, another function of this group · built into the same file, even by a slim install
/// a - b, reduced.
///
/// # Panics
/// Panics on a zero denominator or a result outside ±(2^53 - 1).
pub fn subtract_rational(a: &Rational, b: &Rational) -> Rational {
let (an, ad) = rational_parts(a);
let (bn, bd) = rational_parts(b);
rational_from_wide(an * bd - bn * ad, ad * bd)
}
pub fn fune_vector(args: &[Value]) -> Value {
rational_to_value(&subtract_rational(&rational_from_value(&args[0]), &rational_from_value(&args[1])))
}multiply_rational throws on bad input 8 tests
pub fn multiply_rational(a: &Rational, b: &Rational) -> Rational
| a | Rational | |
| b | Rational | |
| returns | Rational |
For example
multiply_rational(numerator 2, denominator 3, numerator 3, denominator 4)→ numerator 1, denominator 2 two thirds of three quarters is a halfmultiply_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 onemultiply_rational(numerator -2, denominator 5, numerator -5, denominator 4)→ numerator 1, denominator 2 a negative times a negative is positive
fune!(math.rational@^2); // then call multiply_rational(…)
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_rational_rational::{rational_from_value, rational_from_wide, rational_parts, rational_to_value}; ← rational, another function of this group · built into the same file, even by a slim install
/// a × b, reduced.
///
/// # Panics
/// Panics on a zero denominator or a result outside ±(2^53 - 1).
pub fn multiply_rational(a: &Rational, b: &Rational) -> Rational {
let (an, ad) = rational_parts(a);
let (bn, bd) = rational_parts(b);
rational_from_wide(an * bn, ad * bd)
}
pub fn fune_vector(args: &[Value]) -> Value {
rational_to_value(&multiply_rational(&rational_from_value(&args[0]), &rational_from_value(&args[1])))
}divide_rational throws on bad input 9 tests
pub fn divide_rational(a: &Rational, b: &Rational) -> Rational
| a | Rational | |
| b | Rational | must not be zero |
| returns | Rational | a / b |
For example
divide_rational(numerator 1, denominator 2, numerator 1, denominator 4)→ numerator 2, denominator 1 a half divided by a quarter is twodivide_rational(numerator 3, denominator 5, numerator 9, denominator 10)→ numerator 2, denominator 3 dividing by a fraction multiplies by its inversedivide_rational(numerator 1, denominator 3, numerator -2, denominator 3)→ numerator -1, denominator 2 dividing by a negative puts the sign on the numerator
fune!(math.rational@^2); // then call divide_rational(…)
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_rational_rational::{rational_from_value, rational_from_wide, rational_parts, rational_to_value}; ← rational, another function of this group · built into the same file, even by a slim install
/// a ÷ b, reduced. Dividing by zero is an error, never an infinity.
///
/// # Panics
/// Panics if `b` is zero, on a zero denominator, or a result outside ±(2^53 - 1).
pub fn divide_rational(a: &Rational, b: &Rational) -> Rational {
let (an, ad) = rational_parts(a);
let (bn, bd) = rational_parts(b);
if bn == 0 {
panic!("division by zero");
}
rational_from_wide(an * bd, ad * bn)
}
pub fn fune_vector(args: &[Value]) -> Value {
rational_to_value(÷_rational(&rational_from_value(&args[0]), &rational_from_value(&args[1])))
}compare_rational throws on bad input 9 tests
pub fn compare_rational(a: &Rational, b: &Rational) -> i64
| a | Rational | |
| b | Rational | |
| returns | int | -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 halfcompare_rational(numerator 1, denominator 2, numerator 1, denominator 3)→ 1 a half is more than a thirdcompare_rational(numerator 2, denominator 4, numerator -3, denominator -6)→ 0 equal values in different forms are equal
fune!(math.rational@^2); // then call compare_rational(…)
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_rational_rational::{rational_from_value, rational_parts}; ← rational, another function of this group · built into the same file, even by a slim install
/// -1, 0 or 1. Exact: cross-multiplied in i128, never through a float.
///
/// # Panics
/// Panics on a zero denominator or a part outside ±(2^53 - 1).
pub fn compare_rational(a: &Rational, b: &Rational) -> i64 {
let (an, ad) = rational_parts(a);
let (bn, bd) = rational_parts(b);
match (an * bd).cmp(&(bn * ad)) {
std::cmp::Ordering::Less => -1,
std::cmp::Ordering::Equal => 0,
std::cmp::Ordering::Greater => 1,
}
}
pub fn fune_vector(args: &[Value]) -> Value {
Value::Int(compare_rational(&rational_from_value(&args[0]), &rational_from_value(&args[1])))
}rational_to_integer throws on bad input 14 tests
pub fn rational_to_integer(r: &Rational, mode: &str) -> i64
| r | Rational | |
| mode | RoundingMode | how to round a value that is not whole, as math.round-div does |
| returns | int | the integer r rounds to |
For example
rational_to_integer(numerator 7, denominator 2, half-up)→ 4 seven halves rounds half up to 4rational_to_integer(numerator 5, denominator 2, half-even)→ 2 five halves rounds half even to 2rational_to_integer(numerator 7, denominator 2, half-even)→ 4 seven halves rounds half even to 4
fune!(math.rational@^2); // then call rational_to_integer(…)
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_rational_rational::{rational, rational_from_value}; ← rational, another function of this group · built into the same file, even by a slim install
use super::math_round_div::round_div; ← from math.round-div ^1.0.0 · built alongside by fune
/// 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.
///
/// # Panics
/// Panics on a zero denominator, a part outside ±(2^53 - 1), or an unknown 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)
}
pub fn fune_vector(args: &[Value]) -> Value {
Value::Int(rational_to_integer(&rational_from_value(&args[0]), args[1].as_str()))
}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
That builds the whole group. To build only what you call, and whatever it uses inside the group:
fune add math.rational --only calculateRational
The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./math.rational-2.0.0-rust.fune, or fetch it from a terminal with fune pull math.rational@2.0.0:rust.
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
| Case | Arguments | Expected | |
|---|---|---|---|
| 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
| Case | Arguments | Expected | |
|---|---|---|---|
| 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
| Case | Arguments | Expected | |
|---|---|---|---|
| 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
| Case | Arguments | Expected | |
|---|---|---|---|
| a fractional part is an error | 1.5, 2 | → | error: must be integers |
addRational 9 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| 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
| Case | Arguments | Expected | |
|---|---|---|---|
| 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
| Case | Arguments | Expected | |
|---|---|---|---|
| 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
| Case | Arguments | Expected | |
|---|---|---|---|
| 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
| Case | Arguments | Expected | |
|---|---|---|---|
| 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
| Case | Arguments | Expected | |
|---|---|---|---|
| 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
| Case | Arguments | Expected | |
|---|---|---|---|
| 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.