So best rational approximations come from a continued fraction expansion.
A continued fraction expansion comes from the loop,
function* continuedFraction(x: number): Iterable<number> {
let n = Math.floor(x)
yield n
x -= n
while (x > 0) {
x = 1 / x
n = Math.floor(x)
x -= n
yield n
}
}
For pi this will yield [3, 7, 15, 1, 292, 1, 1, 1, ...] meaning that
ts-node layout-div.ts '= pi + 3 / 1 + 7 / 1 + 15 / 1 + 1 / 1 + 292 / 1 + 1 ...'
Pretty-printing pi = 3 + 1/(7 + 1/(15 + 1/(1 + 1/(292 + 1/(1 + ...)))))
1
pi = 3 + -----------------------------------------------------------------------
1
7 + ----------------------------------------------------------
1
15 + --------------------------------------------
1
1 + -------------------------------
1
292 + ----------------
1 + ...
and it turns out this is the right way to manufacture best rational approximations out of a number, by truncating this sequence with some final integer.
Before I used to get very frustrated, though.
I used to get frustrated because the obvious way to calculate this is to start from the innermost number, and build a rational moving backwards out of the fraction. But then you can't easily compute the next best rational number, without redoing all of your work. Frustrating!
Today I thought that it might secretly be a matrix problem.
Suppose the rational m/n is stored as the vector [m; n]. Then the fundamental process that we are considering,
(m/n) ----> k + 1 / (m/n)
= k + (n / m)
= (k m + n) / m
becomes a 2x2 matrix:
k + 1/(...) = [ k, 1 ]
[ 1, 0 ]
And matrix algebra is associative, you can do it from the beginning to the end rather than from the end to the beginning like normal.
Some further insight reveals that actually the matrices you get when you do this are:
[ ratApprox(x, n) ratApprox(x, n - 1) ].
This is easiest to see by just operating on this with vectors [1; 0]
and [0; 1]
.
The first one is a representation for a rational number infinity, whence we would find that
Pretty-printing a + 1/(b + 1/∞) = a + 1/b
1 1
a + -------------------- = a + -------
1 b
b + -------
∞
as it should be, truncating the sequence at b
. A similar analysis for the second vector, which is 0, constructs
Pretty-printing a + 1/(b + 1/0) = a + 1/(b + ∞) = a
1 1
a + -------------------- = a + -------------- = a
1 b + ∞
b + -------
0
truncating the sequence at a
.
So our first rational approximation is [floor(x), 1]
with a prior rational approximation of infinity, [1, 0]
in this code.
And then every other rational approximation is constructed from the current digit K of the expansion, plus the last two rational approximations, by matrix multiplication:
[ m, a ] * [ k, 1 ] = [ k m + a, m]
[ n, b ] [ 1, 0 ] [ k n + b, n]
That is what the above code does.
Feel free to use any of this under the MPLv2, or contact me if you want to embed it in a proprietary or GPLed file.