From c88cc38f4b0cb6160f14c643926176b73ee4becb Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Wed, 25 Mar 2026 22:44:49 +0300 Subject: [PATCH] math.fractions: use bignum (bigint) in fractions (fixes #19089) --- vlib/math/fractions/README.md | 28 ++ vlib/math/fractions/fraction.v | 414 ++++++++++++++++++++-------- vlib/math/fractions/fraction_test.v | 36 +++ 3 files changed, 359 insertions(+), 119 deletions(-) create mode 100644 vlib/math/fractions/README.md diff --git a/vlib/math/fractions/README.md b/vlib/math/fractions/README.md new file mode 100644 index 000000000..161ff3dec --- /dev/null +++ b/vlib/math/fractions/README.md @@ -0,0 +1,28 @@ +# math.fractions + +`math.fractions` provides exact rational arithmetic. + +## Existing `i64` API + +```v +import math.fractions + +f := fractions.fraction(4, 8) +assert f.reduce().str() == '1/2' +``` + +## Generic backends + +`fractions.Rational[T]` supports signed builtin integers and `math.big.Integer`. + +```v +import math.big +import math.fractions + +huge := big.integer_from_string('1000000000000000000000000000000')! +f := fractions.big_fraction(huge, big.integer_from_int(3)) +g := fractions.rational(i32(5), i32(10)) + +assert f.reduce().str() == '1000000000000000000000000000000/3' +assert g.reduce().str() == '1/2' +``` diff --git a/vlib/math/fractions/fraction.v b/vlib/math/fractions/fraction.v index c0347d62e..7b777ba89 100644 --- a/vlib/math/fractions/fraction.v +++ b/vlib/math/fractions/fraction.v @@ -3,17 +3,9 @@ // that can be found in the LICENSE file. module fractions -import math -import math.bits - -// Fraction Struct -// --------------- -// A Fraction has a numerator (n) and a denominator (d). If the user uses -// the helper functions in this module, then the following are guaranteed: -// 1. If the user provides n and d with gcd(n, d) > 1, the fraction will -// not be reduced automatically. -// 2. d cannot be set to zero. The factory function will panic. -// 3. If provided d is negative, it will be made positive. n will change as well. +import math.big + +// Fraction stores a numerator and denominator using the existing `i64` backend. pub struct Fraction { pub: n i64 @@ -21,27 +13,108 @@ pub: is_reduced bool } -// A factory function for creating a Fraction, adds a boundary condition -// to ensure that the denominator is non-zero. It automatically converts -// the negative denominator to positive and adjusts the numerator. -// NOTE: Fractions created are not reduced by default. +// Rational stores a numerator and denominator using the backend type `T`. +// +// Supported backend types are signed builtin integers and `math.big.Integer`. +pub struct Rational[T] { +pub: + n T + d T + is_reduced bool +} + +// fraction creates an `i64`-backed Fraction. +// +// The denominator must be non-zero. Negative denominators are normalized so +// that the stored denominator is always positive. pub fn fraction(n i64, d i64) Fraction { - if d == 0 { + return rational_to_fraction(rational[i64](n, d)) +} + +// big_fraction creates a `math.big.Integer`-backed Rational. +// +// The denominator must be non-zero. Negative denominators are normalized so +// that the stored denominator is always positive. +pub fn big_fraction(n big.Integer, d big.Integer) Rational[big.Integer] { + return rational[big.Integer](n, d) +} + +// rational creates a Rational backed by the concrete numeric type `T`. +// +// Fractions created this way are not reduced automatically, but `is_reduced` +// reflects whether the input was already in lowest terms. +pub fn rational[T](n T, d T) Rational[T] { + ensure_supported_backend[T]() + if is_zero_value[T](d) { panic('Denominator cannot be zero') } - // The denominator is always guaranteed to be positive (and non-zero). - if d < 0 { - return fraction(-n, -d) + if is_negative_value[T](d) { + return rational[T](negate_value[T](n), negate_value[T](d)) } - return Fraction{ + return Rational[T]{ n: n d: d - is_reduced: math.gcd(n, d) == 1 + is_reduced: gcd_values[T](n, d) == one_value[T]() } } -// To String method +// str returns the fraction in `n/d` form. pub fn (f Fraction) str() string { + return f.to_rational().str() +} + +// Fraction add using operator overloading. +pub fn (f1 Fraction) + (f2 Fraction) Fraction { + return rational_to_fraction(f1.to_rational() + f2.to_rational()) +} + +// Fraction subtract using operator overloading. +pub fn (f1 Fraction) - (f2 Fraction) Fraction { + return rational_to_fraction(f1.to_rational() - f2.to_rational()) +} + +// Fraction multiply using operator overloading. +pub fn (f1 Fraction) * (f2 Fraction) Fraction { + return rational_to_fraction(f1.to_rational() * f2.to_rational()) +} + +// Fraction divide using operator overloading. +pub fn (f1 Fraction) / (f2 Fraction) Fraction { + return rational_to_fraction(f1.to_rational() / f2.to_rational()) +} + +// negate returns the additive inverse of the Fraction. +pub fn (f Fraction) negate() Fraction { + return rational_to_fraction(f.to_rational().negate()) +} + +// reciprocal returns the reciprocal of the Fraction. +pub fn (f Fraction) reciprocal() Fraction { + return rational_to_fraction(f.to_rational().reciprocal()) +} + +// reduce returns the Fraction reduced to lowest terms. +pub fn (f Fraction) reduce() Fraction { + return rational_to_fraction(f.to_rational().reduce()) +} + +// f64 converts the Fraction to 64-bit floating point. +pub fn (f Fraction) f64() f64 { + return f.to_rational().f64() +} + +// return true if f1 == f2. +pub fn (f1 Fraction) == (f2 Fraction) bool { + return cmp_fraction(f1, f2) == 0 +} + +// return true if f1 < f2. +pub fn (f1 Fraction) < (f2 Fraction) bool { + return cmp_fraction(f1, f2) < 0 +} + +// str returns the fraction in `n/d` form. +pub fn (f Rational[T]) str() string { return '${f.n}/${f.d}' } @@ -52,17 +125,23 @@ pub fn (f Fraction) str() string { // // These are implemented from Knuth, TAOCP Vol 2. Section 4.5 // -// Returns a correctly reduced result for both addition and subtraction -// NOTE: requires reduced inputs -fn general_addition_result(f1 Fraction, f2 Fraction, addition bool) Fraction { - d1 := math.gcd(f1.d, f2.d) +// Returns a correctly reduced result for both addition and subtraction. +// NOTE: requires reduced inputs. +fn general_addition_result[T](f1 Rational[T], f2 Rational[T], addition bool) Rational[T] { + d1 := gcd_values[T](f1.d, f2.d) // d1 happens to be 1 around 600/(pi)^2 or 61 percent of the time (Theorem 4.5.2D) - if d1 == 1 { + if d1 == one_value[T]() { num1n2d := f1.n * f2.d num1d2n := f1.d * f2.n - n := if addition { num1n2d + num1d2n } else { num1n2d - num1d2n } - return Fraction{ - n: n + if addition { + return Rational[T]{ + n: add_values[T](num1n2d, num1d2n) + d: f1.d * f2.d + is_reduced: true + } + } + return Rational[T]{ + n: sub_values[T](num1n2d, num1d2n) d: f1.d * f2.d is_reduced: true } @@ -72,28 +151,37 @@ fn general_addition_result(f1 Fraction, f2 Fraction, addition bool) Fraction { f2den := f2.d / d1 term1 := f1.n * f2den term2 := f2.n * f1den - t := if addition { term1 + term2 } else { term1 - term2 } - d2 := math.gcd(t, d1) - return Fraction{ + if addition { + t := add_values[T](term1, term2) + d2 := gcd_values[T](t, d1) + return Rational[T]{ + n: t / d2 + d: f1den * (f2.d / d2) + is_reduced: true + } + } + t := sub_values[T](term1, term2) + d2 := gcd_values[T](t, d1) + return Rational[T]{ n: t / d2 d: f1den * (f2.d / d2) is_reduced: true } } -// Fraction add using operator overloading -pub fn (f1 Fraction) + (f2 Fraction) Fraction { - return general_addition_result(f1.reduce(), f2.reduce(), true) +// Fraction add using operator overloading. +pub fn (f1 Rational[T]) + (f2 Rational[T]) Rational[T] { + return general_addition_result[T](f1.reduce(), f2.reduce(), true) } -// Fraction subtract using operator overloading -pub fn (f1 Fraction) - (f2 Fraction) Fraction { - return general_addition_result(f1.reduce(), f2.reduce(), false) +// Fraction subtract using operator overloading. +pub fn (f1 Rational[T]) - (f2 Rational[T]) Rational[T] { + return general_addition_result[T](f1.reduce(), f2.reduce(), false) } -// Returns a correctly reduced result for both multiplication and division -// NOTE: requires reduced inputs -fn general_multiplication_result(f1 Fraction, f2 Fraction, multiplication bool) Fraction { +// Returns a correctly reduced result for both multiplication and division. +// NOTE: requires reduced inputs. +fn general_multiplication_result[T](f1 Rational[T], f2 Rational[T], multiplication bool) Rational[T] { // * Theorem: If f1 and f2 are reduced i.e. gcd(f1.n, f1.d) == 1 and gcd(f2.n, f2.d) == 1, // then gcd(f1.n * f2.n, f1.d * f2.d) == gcd(f1.n, f2.d) * gcd(f1.d, f2.n) // * Knuth poses this an exercise for 4.5.1. - Exercise 2 @@ -103,17 +191,17 @@ fn general_multiplication_result(f1 Fraction, f2 Fraction, multiplication bool) // * One more thing: // if d = gcd(a, b) for example, then d divides both a and b if multiplication { - d1 := math.gcd(f1.n, f2.d) - d2 := math.gcd(f1.d, f2.n) - return Fraction{ + d1 := gcd_values[T](f1.n, f2.d) + d2 := gcd_values[T](f1.d, f2.n) + return Rational[T]{ n: (f1.n / d1) * (f2.n / d2) d: (f2.d / d1) * (f1.d / d2) is_reduced: true } } else { - d1 := math.gcd(f1.n, f2.n) - d2 := math.gcd(f1.d, f2.d) - return Fraction{ + d1 := gcd_values[T](f1.n, f2.n) + d2 := gcd_values[T](f1.d, f2.d) + return Rational[T]{ n: (f1.n / d1) * (f2.d / d2) d: (f2.n / d1) * (f1.d / d2) is_reduced: true @@ -121,61 +209,57 @@ fn general_multiplication_result(f1 Fraction, f2 Fraction, multiplication bool) } } -// Fraction multiply using operator overloading -pub fn (f1 Fraction) * (f2 Fraction) Fraction { - return general_multiplication_result(f1.reduce(), f2.reduce(), true) +// Fraction multiply using operator overloading. +pub fn (f1 Rational[T]) * (f2 Rational[T]) Rational[T] { + return general_multiplication_result[T](f1.reduce(), f2.reduce(), true) } -// Fraction divide using operator overloading -pub fn (f1 Fraction) / (f2 Fraction) Fraction { - if f2.n == 0 { +// Fraction divide using operator overloading. +pub fn (f1 Rational[T]) / (f2 Rational[T]) Rational[T] { + if is_zero_value[T](f2.n) { panic('Cannot divide by zero') } - // If the second fraction is negative, it will - // mess up the sign. We need positive denominator - if f2.n < 0 { + // If the second fraction is negative, it will mess up the sign. + // We need a positive denominator. + if is_negative_value[T](f2.n) { return f1.negate() / f2.negate() } - return general_multiplication_result(f1.reduce(), f2.reduce(), false) + return general_multiplication_result[T](f1.reduce(), f2.reduce(), false) } -// Fraction negate method -pub fn (f Fraction) negate() Fraction { - return Fraction{ - n: -f.n +// negate returns the additive inverse of the Rational. +pub fn (f Rational[T]) negate() Rational[T] { + return Rational[T]{ + n: negate_value[T](f.n) d: f.d is_reduced: f.is_reduced } } -// Fraction reciprocal method -pub fn (f Fraction) reciprocal() Fraction { - if f.n == 0 { +// reciprocal returns the reciprocal of the Rational. +pub fn (f Rational[T]) reciprocal() Rational[T] { + if is_zero_value[T](f.n) { panic('Denominator cannot be zero') } - return Fraction{ - n: f.d - d: f.n - is_reduced: f.is_reduced - } + return rational[T](f.d, f.n) } -// Fraction method which reduces the fraction -pub fn (f Fraction) reduce() Fraction { +// reduce returns the Rational reduced to lowest terms. +pub fn (f Rational[T]) reduce() Rational[T] { if f.is_reduced { return f } - cf := math.gcd(f.n, f.d) - return Fraction{ + cf := gcd_values[T](f.n, f.d) + return Rational[T]{ n: f.n / cf d: f.d / cf is_reduced: true } } -// f64 converts the Fraction to 64-bit floating point -pub fn (f Fraction) f64() f64 { - return f64(f.n) / f64(f.d) +// f64 converts the Rational to 64-bit floating point. +pub fn (f Rational[T]) f64() f64 { + return to_f64_value[T](f.n) / to_f64_value[T](f.d) } // @@ -183,63 +267,155 @@ pub fn (f Fraction) f64() f64 { // | Utility functions.| // + ------------------+ // -// Returns the absolute value of an i64 -fn abs(num i64) i64 { - if num < 0 { - return -num - } else { - return num + +fn ensure_supported_backend[T]() { + $if T is big.Integer || T is i8 || T is i16 || T is i32 || T is i64 || T is int { + } $else { + $compile_error('fractions: Rational only supports signed builtin integers and math.big.Integer backends') } } -// cmp_i64s compares the two arguments, returns 0 when equal, 1 when the first is bigger, -1 otherwise -fn cmp_i64s(a i64, b i64) int { - if a == b { - return 0 - } else if a > b { - return 1 - } else { - return -1 +@[inline] +fn (f Fraction) to_rational() Rational[i64] { + return Rational[i64]{ + n: f.n + d: f.d + is_reduced: f.is_reduced } } -// cmp_f64s compares the two arguments, returns 0 when equal, 1 when the first is bigger, -1 otherwise -fn cmp_f64s(a f64, b f64) int { - // V uses epsilon comparison internally - if a == b { - return 0 - } else if a > b { - return 1 - } else { - return -1 +@[inline] +fn rational_to_fraction(r Rational[i64]) Fraction { + return Fraction{ + n: r.n + d: r.d + is_reduced: r.is_reduced } } -// Two integers are safe to multiply when their bit lengths -// sum up to less than 64 (conservative estimate). -fn safe_to_multiply(a i64, b i64) bool { - return (bits.len_64(u64(abs(a))) + bits.len_64(u64(abs(b)))) < 64 +fn cmp_fraction(f1 Fraction, f2 Fraction) int { + return cmp_values[big.Integer](to_big_integer[i64](f1.n) * to_big_integer[i64](f2.d), + to_big_integer[i64](f2.n) * to_big_integer[i64](f1.d)) } -// cmp compares the two arguments, returns 0 when equal, 1 when the first is bigger, -1 otherwise -fn cmp(f1 Fraction, f2 Fraction) int { - if safe_to_multiply(f1.n, f2.d) && safe_to_multiply(f2.n, f1.d) { - return cmp_i64s(f1.n * f2.d, f2.n * f1.d) - } else { - return cmp_f64s(f1.f64(), f2.f64()) +@[inline] +fn zero_value[T]() T { + $if T is big.Integer { + return big.zero_int + } $else { + return T(0) + } +} + +@[inline] +fn one_value[T]() T { + $if T is big.Integer { + return big.one_int + } $else { + return T(1) } } -// +-----------------------------+ -// | Public comparison functions | -// +-----------------------------+ +@[inline] +fn is_zero_value[T](value T) bool { + return value == zero_value[T]() +} -// return true if f1 == f2 -pub fn (f1 Fraction) == (f2 Fraction) bool { - return cmp(f1, f2) == 0 +@[inline] +fn is_negative_value[T](value T) bool { + $if T is big.Integer { + return value < big.zero_int + } $else { + return value < 0 + } } -// return true if f1 < f2 -pub fn (f1 Fraction) < (f2 Fraction) bool { - return cmp(f1, f2) < 0 +@[inline] +fn negate_value[T](value T) T { + $if T is big.Integer { + return value.neg() + } $else { + return -value + } +} + +@[inline] +fn abs_value[T](value T) T { + $if T is big.Integer { + return value.abs() + } $else { + return if value < 0 { -value } else { value } + } +} + +fn gcd_values[T](a T, b T) T { + $if T is big.Integer { + return a.gcd(b) + } $else { + mut x := abs_value[T](a) + mut y := abs_value[T](b) + for y != zero_value[T]() { + x %= y + if x == zero_value[T]() { + return y + } + y %= x + } + return x + } +} + +@[inline] +fn to_f64_value[T](value T) f64 { + $if T is big.Integer { + return value.str().f64() + } $else { + return f64(value) + } +} + +@[inline] +fn to_big_integer[T](value T) big.Integer { + $if T is big.Integer { + return value + } $else { + return big.integer_from_i64(i64(value)) + } +} + +@[inline] +fn add_values[T](a T, b T) T { + return a + b +} + +@[inline] +fn sub_values[T](a T, b T) T { + return a - b +} + +// cmp compares the two arguments, returns 0 when equal, 1 when the first is bigger, -1 otherwise. +fn cmp[T](f1 Rational[T], f2 Rational[T]) int { + $if T is big.Integer { + return cmp_values[T](f1.n * f2.d, f2.n * f1.d) + } $else { + return cmp_values[big.Integer](to_big_integer[T](f1.n) * to_big_integer[T](f2.d), + to_big_integer[T](f2.n) * to_big_integer[T](f1.d)) + } +} + +fn cmp_values[T](a T, b T) int { + if a == b { + return 0 + } + return if a > b { 1 } else { -1 } +} + +// return true if f1 == f2. +pub fn (f1 Rational[T]) == (f2 Rational[T]) bool { + return cmp[T](f1, f2) == 0 +} + +// return true if f1 < f2. +pub fn (f1 Rational[T]) < (f2 Rational[T]) bool { + return cmp[T](f1, f2) < 0 } diff --git a/vlib/math/fractions/fraction_test.v b/vlib/math/fractions/fraction_test.v index 03b087a4c..94cdbe155 100644 --- a/vlib/math/fractions/fraction_test.v +++ b/vlib/math/fractions/fraction_test.v @@ -1,10 +1,15 @@ // Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved. // Use of this source code is governed by an MIT license // that can be found in the LICENSE file. +import math.big import math.fractions // (Old) results are verified using https://www.calculatorsoup.com/calculators/math/fractions.php // Newer ones are contrived for corner cases or prepared by hand. +fn bi(s string) big.Integer { + return big.integer_from_string(s) or { panic(err) } +} + fn test_4_by_8_f64_and_str() { f := fractions.fraction(4, 8) assert f.f64() == 0.5 @@ -267,3 +272,34 @@ fn test_49_by_75_not_greater_than_2_by_3() { assert !(f1 > f2) assert !(f1 >= f2) } + +fn test_i32_rational_addition() { + f1 := fractions.rational(i32(4), i32(8)) + f2 := fractions.rational(i32(5), i32(10)) + sum := f1 + f2 + assert sum.str() == '1/1' + assert sum == fractions.rational(i32(1), i32(1)) +} + +fn test_big_fraction_normalizes_and_reduces() { + f := fractions.big_fraction(bi('-14'), bi('-21')) + assert f.str() == '14/21' + assert f.reciprocal().str() == '21/14' + assert f.reduce().str() == '2/3' +} + +fn test_big_fraction_addition() { + f1 := fractions.big_fraction(bi('100000000000000000000000000000000000000'), bi('3')) + f2 := fractions.big_fraction(bi('5'), bi('6')) + sum := f1 + f2 + assert sum.str() == '200000000000000000000000000000000000005/6' + assert sum > f1 +} + +fn test_big_fraction_exact_comparison_beyond_i64() { + huge := bi('9223372036854775808123456789') + f1 := fractions.big_fraction(huge + big.one_int, huge) + f2 := fractions.big_fraction(huge, huge + big.one_int) + assert f1 > f2 + assert f1 != f2 +} -- 2.39.5