v2 / vlib / math / fractions / approximations.v
117 lines · 109 sloc · 3.63 KB · 008aaad99981918c51194d7aaaaaccb4c258f244
Raw
1// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
2// Use of this source code is governed by an MIT license
3// that can be found in the LICENSE file.
4module fractions
5
6import math
7
8const default_eps = 1.0e-4
9const max_iterations = 50
10const zero = fraction(0, 1)
11
12// ------------------------------------------------------------------------
13// Unwrapped evaluation methods for fast evaluation of continued fractions.
14// ------------------------------------------------------------------------
15// We need these functions because the evaluation of continued fractions
16// always has to be done from the end. Also, the numerator-denominator pairs
17// are generated from front to end. This means building a result from a
18// previous one isn't possible. So we need unrolled versions to ensure that
19// we don't take too much of a performance penalty by calling eval_cf
20// several times.
21// ------------------------------------------------------------------------
22// eval_1 returns the result of evaluating a continued fraction series of length 1
23fn eval_1(whole i64, d []i64) Fraction {
24 return fraction(whole * d[0] + 1, d[0])
25}
26
27// eval_2 returns the result of evaluating a continued fraction series of length 2
28fn eval_2(whole i64, d []i64) Fraction {
29 den := d[0] * d[1] + 1
30 return fraction(whole * den + d[1], den)
31}
32
33// eval_3 returns the result of evaluating a continued fraction series of length 3
34fn eval_3(whole i64, d []i64) Fraction {
35 d1d2_plus_n2 := d[1] * d[2] + 1
36 den := d[0] * d1d2_plus_n2 + d[2]
37 return fraction(whole * den + d1d2_plus_n2, den)
38}
39
40// eval_cf evaluates a continued fraction series and returns a Fraction.
41fn eval_cf(whole i64, den []i64) Fraction {
42 count := den.len
43 // Offload some small-scale calculations
44 // to dedicated functions
45 match count {
46 1 {
47 return eval_1(whole, den)
48 }
49 2 {
50 return eval_2(whole, den)
51 }
52 3 {
53 return eval_3(whole, den)
54 }
55 else {
56 last := count - 1
57 mut n := i64(1)
58 mut d := den[last]
59 // The calculations are done from back to front
60 for index := count - 2; index >= 0; index-- {
61 t := d
62 d = den[index] * d + n
63 n = t
64 }
65 return fraction(d * whole + n, d)
66 }
67 }
68}
69
70// approximate returns a Fraction that approcimates the given value to
71// within the default epsilon value (1.0e-4). This means the result will
72// be accurate to 3 places after the decimal.
73pub fn approximate(val f64) Fraction {
74 return approximate_with_eps(val, default_eps)
75}
76
77// approximate_with_eps returns a Fraction
78pub fn approximate_with_eps(val f64, eps f64) Fraction {
79 if val == 0.0 {
80 return zero
81 }
82 if eps < 0.0 {
83 panic('Epsilon value cannot be negative.')
84 }
85 if math.abs(val) > max_i64 {
86 panic('Value out of range.')
87 }
88 // The integer part is separated first. Then we process the fractional
89 // part to generate numerators and denominators in tandem.
90 whole := i64(val)
91 mut frac := val - f64(whole)
92 // Quick exit for integers
93 if frac == 0.0 {
94 return fraction(whole, 1)
95 }
96 mut d := []i64{}
97 mut partial := zero
98 // We must complete the approximation within the maximum number of
99 // itertations allowed. If we can't panic.
100 // Empirically tested: the hardest constant to approximate is the
101 // golden ratio (math.phi) and for f64s, it only needs 38 iterations.
102 for _ in 0 .. max_iterations {
103 // We calculate the reciprocal. That's why the numerator is
104 // always 1.
105 frac = 1.0 / frac
106 den := i64(frac)
107 d << den
108 // eval_cf is called often so it needs to be performant
109 partial = eval_cf(whole, d)
110 // Check if we're done
111 if math.abs(val - partial.f64()) < eps {
112 return partial
113 }
114 frac -= f64(den)
115 }
116 panic("Couldn't converge. Please create an issue on https://github.com/vlang/v")
117}
118