From 8aba3eaa07e3e1c40839a040cbb26e11ec93f029 Mon Sep 17 00:00:00 2001 From: Hungry Blue Dev <46458389+hungrybluedev@users.noreply.github.com> Date: Sun, 10 May 2020 19:55:33 +0530 Subject: [PATCH] math.fractions: refactor and add more tests --- vlib/math/fractions/fraction.v | 259 +++++++++++++++---- vlib/math/fractions/fraction_test.v | 369 ++++++++++++++++++---------- 2 files changed, 451 insertions(+), 177 deletions(-) diff --git a/vlib/math/fractions/fraction.v b/vlib/math/fractions/fraction.v index 6be1a2a10d..fa5d62113e 100644 --- a/vlib/math/fractions/fraction.v +++ b/vlib/math/fractions/fraction.v @@ -1,105 +1,219 @@ // Copyright (c) 2019-2020 Alexander Medvednikov. All rights reserved. // Use of this source code is governed by an MIT license // 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. struct Fraction { - n i64 - d i64 + n i64 + d i64 +pub: + is_reduced bool } // A factory function for creating a Fraction, adds a boundary condition -pub fn fraction(n i64, d i64) Fraction{ +// 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. +pub fn fraction(n, d i64) Fraction { if d != 0 { - return Fraction{n, d} - } - else { + // The denominator is always guaranteed to be positive (and non-zero). + if d < 0 { + return fraction(-n, -d) + } else { + return Fraction{ + n: n + d: d + is_reduced: math.gcd(n, d) == 1 + } + } + } else { panic('Denominator cannot be zero') } } // To String method -pub fn (f Fraction) str() string { - return '$f.n/$f.d' +pub fn (f Fraction) str() string { + return '$f.n/$f.d' +} + +// +// + ---------------------+ +// | Arithmetic functions.| +// + ---------------------+ +// +// These are implemented from Knuth, TAOCP Vol 2. Section 4.5 +// +// Returns a correctly reduced result for both addition and subtraction +fn general_addition_result(f1, f2 Fraction, addition bool) Fraction { + d1 := math.gcd(f1.d, f2.d) + // d1 happends to be 1 around 600/(pi)^2 or 61 percent of the time (Theorem 4.5.2D) + if d1 == 1 { + mut n := i64(0) + num1n2d := f1.n * f2.d + num1d2n := f1.d * f2.n + if addition { + n = num1n2d + num1d2n + } else { + n = num1n2d - num1d2n + } + return Fraction{ + n: n + d: f1.d * f2.d + is_reduced: true + } + } + // Here d1 > 1. + // Without the i64(...), t is declared as an int + // and it does not have enough precision + mut t := i64(0) + term1 := f1.n * (f2.d / d1) + term2 := f2.n * (f1.d / d1) + if addition { + t = term1 + term2 + } else { + t = term1 - term2 + } + d2 := math.gcd(t, d1) + return Fraction{ + n: t / d2 + d: (f1.d / d1) * (f2.d / d2) + is_reduced: true + } } // Fraction add using operator overloading -pub fn (f1 Fraction) + (f2 Fraction) Fraction { - if f1.d == f2.d { - return Fraction{f1.n + f2.n, f1.d} - } - else { - return Fraction{(f1.n * f2.d) + (f2.n * f1.d), f1.d * f2.d} - } +pub fn (f1 Fraction) +(f2 Fraction) Fraction { + return general_addition_result(f1.reduce(), f2.reduce(), true) } // Fraction subtract using operator overloading -pub fn (f1 Fraction) - (f2 Fraction) Fraction { - if f1.d == f2.d { - return Fraction{f1.n - f2.n, f1.d} +pub fn (f1 Fraction) -(f2 Fraction) Fraction { + return general_addition_result(f1.reduce(), f2.reduce(), false) +} + +// Returns a correctly reduced result for both multiplication and division +fn general_multiplication_result(f1, f2 Fraction, multiplication bool) Fraction { + // 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 + mut d1 := i64(0) + mut d2 := i64(0) + mut n := i64(0) + mut d := i64(0) + // The terms are flipped for multiplication and division, so the gcds must be calculated carefully + // We do multiple divisions in order to prevent any possible overflows. Also, note that: + // 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) + n = (f1.n / d1) * (f2.n / d2) + d = (f2.d / d1) * (f1.d / d2) + } else { + d1 = math.gcd(f1.n, f2.n) + d2 = math.gcd(f1.d, f2.d) + n = (f1.n / d1) * (f2.d / d2) + d = (f2.n / d1) * (f1.d / d2) } - else { - return Fraction{(f1.n * f2.d) - (f2.n * f1.d), f1.d * f2.d} + return Fraction{ + n: n + d: d + is_reduced: true } } // Fraction multiply using operator overloading -// pub fn (f1 Fraction) * (f2 Fraction) Fraction { -// return Fraction{f1.n * f2.n,f1.d * f2.d} -// } +pub fn (f1 Fraction) *(f2 Fraction) Fraction { + return general_multiplication_result(f1.reduce(), f2.reduce(), true) +} // Fraction divide using operator overloading -// pub fn (f1 Fraction) / (f2 Fraction) Fraction { -// return Fraction{f1.n * f2.d,f1.d * f2.n} -// } +pub fn (f1 Fraction) /(f2 Fraction) Fraction { + if f2.n == 0 { + panic('Cannot divive by zero') + } + // If the second fraction is negative, it will + // mess up the sign. We need positive denominator + if f2.n < 0 { + return f1.negate() / f2.negate() + } + return general_multiplication_result(f1.reduce(), f2.reduce(), false) +} -// Fraction add method +// Fraction add method. Deprecated. Use the operator instead. +[deprecated] pub fn (f1 Fraction) add(f2 Fraction) Fraction { return f1 + f2 } -// Fraction subtract method +// Fraction subtract method. Deprecated. Use the operator instead. +[deprecated] pub fn (f1 Fraction) subtract(f2 Fraction) Fraction { return f1 - f2 } -// Fraction multiply method +// Fraction multiply method. Deprecated. Use the operator instead. +[deprecated] pub fn (f1 Fraction) multiply(f2 Fraction) Fraction { - return Fraction{f1.n * f2.n, f1.d * f2.d} + return f1 * f2 } -// Fraction divide method +// Fraction divide method. Deprecated. Use the operator instead. +[deprecated] pub fn (f1 Fraction) divide(f2 Fraction) Fraction { - return Fraction{f1.n * f2.d, f1.d * f2.n} + return f1 / f2 +} + +// Fraction negate method +pub fn (f1 Fraction) negate() Fraction { + return Fraction{ + n: -f1.n + d: f1.d + is_reduced: f1.is_reduced + } } // Fraction reciprocal method pub fn (f1 Fraction) reciprocal() Fraction { - if f1.n == 0 { panic('Denominator cannot be zero') } - return Fraction{f1.d, f1.n} -} - -// Fraction method which gives greatest common divisor of numerator and denominator -pub fn (f1 Fraction) gcd() i64 { - return math.gcd(f1.n, f1.d) + if f1.n == 0 { + panic('Denominator cannot be zero') + } + return Fraction{ + n: f1.d + d: f1.n + is_reduced: f1.is_reduced + } } // Fraction method which reduces the fraction pub fn (f1 Fraction) reduce() Fraction { - cf := f1.gcd() - return Fraction{f1.n / cf, f1.d / cf} + if f1.is_reduced { + return f1 + } + cf := math.gcd(f1.n, f1.d) + return Fraction{ + n: f1.n / cf + d: f1.d / cf + is_reduced: true + } } -// Converts Fraction to decimal +// f64 converts the Fraction to 64-bit floating point pub fn (f1 Fraction) f64() f64 { return f64(f1.n) / f64(f1.d) } +// +// + ------------------+ +// | Utility functions.| +// + ------------------+ +// // Returns the absolute value of an i64 fn abs(num i64) i64 { if num < 0 { @@ -109,18 +223,65 @@ fn abs(num i64) i64 { } } +fn cmp_i64s(a, b i64) int { + if a == b { + return 0 + } else if a > b { + return 1 + } else { + return -1 + } +} + +fn cmp_f64s(a, b f64) int { + // V uses epsilon comparison internally + if a == b { + return 0 + } else if a > b { + return 1 + } else { + return -1 + } +} + // Two integers are safe to multiply when their bit lengths // sum up to less than 64 (conservative estimate). fn safe_to_multiply(a, b i64) bool { return (bits.len_64(abs(a)) + bits.len_64(abs(b))) < 64 } -// Compares two Fractions -pub fn (f1 Fraction) equals(f2 Fraction) bool { +fn cmp(f1, f2 Fraction) int { if safe_to_multiply(f1.n, f2.d) && safe_to_multiply(f2.n, f1.d) { - return (f1.n * f2.d) == (f2.n * f1.d) + return cmp_i64s(f1.n * f2.d, f2.n * f1.d) + } else { + return cmp_f64s(f1.f64(), f2.f64()) } - r1 := f1.reduce() - r2 := f2.reduce() - return (r1.n == r2.n) && (r1.d == r2.d) +} + +// +-----------------------------+ +// | Public comparison functions | +// +-----------------------------+ +// equals returns true if both the Fractions are equal +pub fn (f1 Fraction) equals(f2 Fraction) bool { + return cmp(f1, f2) == 0 +} + +// ge returns true if f1 >= f2 +pub fn (f1 Fraction) ge(f2 Fraction) bool { + return cmp(f1, f2) >= 0 +} + +// gt returns true if f1 > f2 +pub fn (f1 Fraction) gt(f2 Fraction) bool { + return cmp(f1, f2) > 0 +} + +// le returns true if f1 <= f2 +pub fn (f1 Fraction) le(f2 Fraction) bool { + return cmp(f1, f2) <= 0 +} + +// lt returns true if f1 < f2 +pub fn (f1 Fraction) lt(f2 Fraction) bool { + return cmp(f1, f2) < 0 } diff --git a/vlib/math/fractions/fraction_test.v b/vlib/math/fractions/fraction_test.v index 28a8a961f0..2ff34b62bf 100644 --- a/vlib/math/fractions/fraction_test.v +++ b/vlib/math/fractions/fraction_test.v @@ -1,153 +1,266 @@ -import math.fractions as fractions +import math.fractions -// Results are verified using https://www.calculatorsoup.com/calculators/math/fractions.php - -fn test_fraction_creation() { - mut f1 := fractions.fraction(4,8) - assert f1.f64() == 0.5 - assert f1.str().eq('4/8') - f1 = fractions.fraction(10,5) - assert f1.f64() == 2.0 - assert f1.str().eq('10/5') - f1 = fractions.fraction(9,3) - assert f1.f64() == 3.0 - assert f1.str().eq('9/3') +// (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 test_4_by_8_f64_and_str() { + f := fractions.fraction(4, 8) + assert f.f64() == 0.5 + assert f.str() == '4/8' } -fn test_fraction_add() { - mut f1 := fractions.fraction(4,8) - mut f2 := fractions.fraction(5,10) - mut sum := f1 + f2 +fn test_10_by_5_f64_and_str() { + f := fractions.fraction(10, 5) + assert f.f64() == 2.0 + assert f.str() == '10/5' +} + +fn test_9_by_3_f64_and_str() { + f := fractions.fraction(9, 3) + assert f.f64() == 3.0 + assert f.str() == '9/3' +} + +fn test_4_by_minus_5_f64_and_str() { + f := fractions.fraction(4, -5) + assert f.f64() == -0.8 + assert f.str() == '-4/5' +} + +fn test_minus_7_by_minus_92_str() { + f := fractions.fraction(-7, -5) + assert f.str() == '7/5' +} + +fn test_4_by_8_plus_5_by_10() { + f1 := fractions.fraction(4, 8) + f2 := fractions.fraction(5, 10) + sum := f1 + f2 assert sum.f64() == 1.0 - assert sum.str().eq('80/80') - f1 = fractions.fraction(5,5) - f2 = fractions.fraction(8,8) - sum = f1 + f2 + assert sum.str() == '1/1' + assert sum.equals(fractions.fraction(1, 1)) +} + +fn test_5_by_5_plus_8_by_8() { + f1 := fractions.fraction(5, 5) + f2 := fractions.fraction(8, 8) + sum := f1 + f2 assert sum.f64() == 2.0 - assert sum.str().eq('80/40') - f1 = fractions.fraction(9,3) - f2 = fractions.fraction(1,3) - sum = f1 + f2 - $if debug { - println(sum.f64()) - } - assert sum.str().eq('10/3') - f1 = fractions.fraction(3,7) - f2 = fractions.fraction(1,4) - sum = f1 + f2 - $if debug { - println(sum.f64()) - } - assert sum.str().eq('19/28') + assert sum.str() == '2/1' + assert sum.equals(fractions.fraction(2, 1)) } -fn test_fraction_subtract() { - mut f1 := fractions.fraction(4,8) - mut f2 := fractions.fraction(5,10) - mut diff := f2 - f1 - assert diff.f64() == 0 - assert diff.str().eq('0/80') - f1 = fractions.fraction(5,5) - f2 = fractions.fraction(8,8) - diff = f2 - f1 - assert diff.f64() == 0 - assert diff.str().eq('0/40') - f1 = fractions.fraction(9,3) - f2 = fractions.fraction(1,3) - diff = f1 - f2 - $if debug { - println(diff.f64()) - } - assert diff.str().eq('8/3') - f1 = fractions.fraction(3,7) - f2 = fractions.fraction(1,4) - diff = f1 - f2 - $if debug { - println(diff.f64()) - } - assert diff.str().eq('5/28') +fn test_9_by_3_plus_1_by_3() { + f1 := fractions.fraction(9, 3) + f2 := fractions.fraction(1, 3) + sum := f1 + f2 + assert sum.str() == '10/3' + assert sum.equals(fractions.fraction(10, 3)) } -fn test_fraction_multiply() { - mut f1 := fractions.fraction(4,8) - mut f2 := fractions.fraction(5,10) - mut product := f1.multiply(f2) +fn test_3_by_7_plus_1_by_4() { + f1 := fractions.fraction(3, 7) + f2 := fractions.fraction(1, 4) + sum := f1 + f2 + assert sum.str() == '19/28' + assert sum.equals(fractions.fraction(19, 28)) +} + +fn test_36529_by_12409100000_plus_418754901_by_9174901000() { + f1 := fractions.fraction(i64(36529), i64(12409100000)) + f2 := fractions.fraction(i64(418754901), i64(9174901000)) + sum := f1 + f2 + assert sum.str() == '5196706591957729/113852263999100000' +} + +fn test_4_by_8_plus_minus_5_by_10() { + f1 := fractions.fraction(4, 8) + f2 := fractions.fraction(-5, 10) + diff := f2 + f1 + assert diff.f64() == 0 + assert diff.str() == '0/1' +} + +fn test_4_by_8_minus_5_by_10() { + f1 := fractions.fraction(4, 8) + f2 := fractions.fraction(5, 10) + diff := f2 - f1 + assert diff.f64() == 0 + assert diff.str() == '0/1' +} + +fn test_5_by_5_minus_8_by_8() { + f1 := fractions.fraction(5, 5) + f2 := fractions.fraction(8, 8) + diff := f2 - f1 + assert diff.f64() == 0 + assert diff.str() == '0/1' +} + +fn test_9_by_3_minus_1_by_3() { + f1 := fractions.fraction(9, 3) + f2 := fractions.fraction(1, 3) + diff := f1 - f2 + assert diff.str() == '8/3' +} + +fn test_3_by_7_minus_1_by_4() { + f1 := fractions.fraction(3, 7) + f2 := fractions.fraction(1, 4) + diff := f1 - f2 + assert diff.str() == '5/28' +} + +fn test_36529_by_12409100000_minus_418754901_by_9174901000() { + f1 := fractions.fraction(i64(36529), i64(12409100000)) + f2 := fractions.fraction(i64(418754901), i64(9174901000)) + sum := f1 - f2 + assert sum.str() == '-5196036292040471/113852263999100000' +} + +fn test_4_by_8_times_5_by_10() { + f1 := fractions.fraction(4, 8) + f2 := fractions.fraction(5, 10) + product := f1 * f2 assert product.f64() == 0.25 - assert product.str().eq('20/80') - f1 = fractions.fraction(5,5) - f2 = fractions.fraction(8,8) - product = f1.multiply(f2) + assert product.str() == '1/4' +} + +fn test_5_by_5_times_8_by_8() { + f1 := fractions.fraction(5, 5) + f2 := fractions.fraction(8, 8) + product := f1 * f2 assert product.f64() == 1.0 - assert product.str().eq('40/40') - f1 = fractions.fraction(9,3) - f2 = fractions.fraction(1,3) - product = f1.multiply(f2) + assert product.str() == '1/1' +} + +fn test_9_by_3_times_1_by_3() { + f1 := fractions.fraction(9, 3) + f2 := fractions.fraction(1, 3) + product := f1 * f2 assert product.f64() == 1.0 - assert product.str().eq('9/9') - f1 = fractions.fraction(3,7) - f2 = fractions.fraction(1,4) - product = f1.multiply(f2) - $if debug { - println(product.f64()) - } - assert product.str().eq('3/28') + assert product.str() == '1/1' } -fn test_fraction_divide() { - mut f1 := fractions.fraction(4,8) - mut f2 := fractions.fraction(5,10) - mut re := f1.divide(f2) - assert re.f64() == 1.0 - assert re.str().eq('40/40') - f1 = fractions.fraction(5,5) - f2 = fractions.fraction(8,8) - re = f1.divide(f2) - assert re.f64() == 1.0 - assert re.str().eq('40/40') - f1 = fractions.fraction(9,3) - f2 = fractions.fraction(1,3) - re = f1.divide(f2) - assert re.f64() == 9.0 - assert re.str().eq('27/3') - f1 = fractions.fraction(3,7) - f2 = fractions.fraction(1,4) - re = f1.divide(f2) - $if debug { - println(re.f64()) - } - assert re.str().eq('12/7') +fn test_3_by_7_times_1_by_4() { + f1 := fractions.fraction(3, 7) + f2 := fractions.fraction(1, 4) + product := f2 * f1 + assert product.f64() == (3.0 / 28.0) + assert product.str() == '3/28' } -fn test_fraction_reciprocal() { - mut f1 := fractions.fraction(4,8) - assert f1.reciprocal().str().eq('8/4') - f1 = fractions.fraction(5,10) - assert f1.reciprocal().str().eq('10/5') - f1 = fractions.fraction(5,5) - assert f1.reciprocal().str().eq('5/5') - f1 = fractions.fraction(8,8) - assert f1.reciprocal().str().eq('8/8') - f1 = fractions.fraction(9,3) - assert f1.reciprocal().str().eq('3/9') - f1 = fractions.fraction(1,3) - assert f1.reciprocal().str().eq('3/1') - f1 = fractions.fraction(3,7) - assert f1.reciprocal().str().eq('7/3') - f1 = fractions.fraction(1,4) - assert f1.reciprocal().str().eq('4/1') +fn test_4_by_8_over_5_by_10() { + f1 := fractions.fraction(4, 8) + f2 := fractions.fraction(5, 10) + q := f1 / f2 + assert q.f64() == 1.0 + assert q.str() == '1/1' } -fn test_fraction_equals() { - mut f1 := fractions.fraction(4,8) - mut f2 := fractions.fraction(5,10) +fn test_5_by_5_over_8_by_8() { + f1 := fractions.fraction(5, 5) + f2 := fractions.fraction(8, 8) + q := f1 / f2 + assert q.f64() == 1.0 + assert q.str() == '1/1' +} + +fn test_9_by_3_over_1_by_3() { + f1 := fractions.fraction(9, 3) + f2 := fractions.fraction(1, 3) + q := f1 / f2 + assert q.f64() == 9.0 + assert q.str() == '9/1' +} + +fn test_3_by_7_over_1_by_4() { + f1 := fractions.fraction(3, 7) + f2 := fractions.fraction(1, 4) + q := f1 / f2 + assert q.str() == '12/7' +} + +fn test_reciprocal_4_by_8() { + f := fractions.fraction(4, 8) + assert f.reciprocal().str() == '8/4' +} + +fn test_reciprocal_5_by_10() { + f := fractions.fraction(5, 10) + assert f.reciprocal().str() == '10/5' +} + +fn test_reciprocal_5_by_5() { + f := fractions.fraction(5, 5) + assert f.reciprocal().str() == '5/5' +} + +fn test_reciprocal_8_by_8() { + f := fractions.fraction(8, 8) + assert f.reciprocal().str() == '8/8' +} + +fn test_reciprocal_9_by_3() { + f := fractions.fraction(9, 3) + assert f.reciprocal().str() == '3/9' +} + +fn test_reciprocal_1_by_3() { + f := fractions.fraction(1, 3) + assert f.reciprocal().str() == '3/1' +} + +fn test_reciprocal_7_by_3() { + f := fractions.fraction(7, 3) + assert f.reciprocal().str() == '3/7' +} + +fn test_reciprocal_1_by_4() { + f := fractions.fraction(1, 4) + assert f.reciprocal().str() == '4/1' +} + +fn test_4_by_8_equals_5_by_10() { + f1 := fractions.fraction(4, 8) + f2 := fractions.fraction(5, 10) assert f1.equals(f2) - f1 = fractions.fraction(1,2) - f2 = fractions.fraction(3,4) +} + +fn test_1_by_2_does_not_equal_3_by_4() { + f1 := fractions.fraction(1, 2) + f2 := fractions.fraction(3, 4) assert !f1.equals(f2) } -fn test_gcd_and_reduce(){ +fn test_reduce_3_by_9() { f := fractions.fraction(3, 9) - assert f.gcd() == 3 assert f.reduce().equals(fractions.fraction(1, 3)) } + +fn test_1_by_3_less_than_2_by_4() { + f1 := fractions.fraction(1, 3) + f2 := fractions.fraction(2, 4) + assert f1.lt(f2) + assert f1.le(f2) +} + +fn test_2_by_3_greater_than_2_by_4() { + f1 := fractions.fraction(2, 3) + f2 := fractions.fraction(2, 4) + assert f1.gt(f2) + assert f1.ge(f2) +} + +fn test_5_by_7_not_less_than_2_by_4() { + f1 := fractions.fraction(5, 7) + f2 := fractions.fraction(2, 4) + assert !f1.lt(f2) + assert !f1.le(f2) +} + +fn test_49_by_75_not_greater_than_2_by_3() { + f1 := fractions.fraction(49, 75) + f2 := fractions.fraction(2, 3) + assert !f1.gt(f2) + assert !f1.ge(f2) +} \ No newline at end of file