Rationalize
can find a close rational approximation to any decimal number.
This library also provides a small, incomplete tool set for working with
rational numbers.
Find the closest rational approximation to 0.17 with a denominator no larger than 10:
> Rationalize.closest_rational(0.17, 10)
%Rationalize.Rational{n: 1, d: 6}
Notice that the function closest_rational
returns a struct with fields
n
and d
that hold the numerator and denominator, respectively.
A rational number can be converted to a float or a string:
> Rationalize.to_float(%Rationalize.Rational{n: 1, d: 6})
{:ok, 0.16666666666666666}
> Rationalize.to_string(%Rationalize.Rational{n: 1, d: 6})
"1/6"
The precision of the rational approximation can be increased by increasing the maximum denominator:
> Rationalize.closest_rational(0.17, 100)
%Rationalize.Rational{n: 17, d: 100}
This last result is exact. Further increase in the maximum denominator will not change the result:
> Rationalize.closest_rational(0.17, 1000)
%Rationalize.Rational{n: 17, d: 100}
Working with rational numbers safely means avoiding conversion to floating
numbers unless it's necessary, properly handling cases where the
numerator or denominator may be 0, and allowing the numerator or denominator
to be negative. The module Rationalize.Rational
provides a small set of safe
operations:
# Compare two rational numbers
> r1 = Rationalize.Rational.new(1, 0)
> r2 = Rationalize.Rational.new(1, 2)
> Rationalize.Rational.compare(r1, r2)
:gt # r1 > r2
# Compute the numerical difference between a rational and a float
> r3 = Rationalize.Rational.new(3, 2)
> Rationalize.Rational.diff_num(r3, 0.5)
{:ok, 1.0}
# Operations with negative rationals are allows
> r4 = Rationalize.Rational.new(-3, 2)
> Rationalize.Rational.diff_num(r4, 0.5)
{:ok, -2.0}
# A difference can return :pos_infinity or :neg_infinity
> r5 = Rationalize.Rational.new(-1, 0)
> Rationalize.Rational.diff_num(0.5, r5)
:pos_infinity
# Any operations with 0/0 return :undefined
> r6 = Rationalize.Rational.new(0, 0)
> Rationalize.Rational.diff_num(r6, 1)
:undefined
The utilities in Rationalize.Rational
were originally built only to support
the search for rational approximations. But they have general applicability,
and the tool set could be expanded in the future.
Rationalize
was originally developed for two very specific use cases.
When a probability is presented in decimal form, it may be challenging for some readers to create a mental model of what that probability might mean. For example, a decimal probability could be presented like this in a short narrative:
The chance of a catastrophic meltdown is 0.17.
But it might be preferable to write this:
The chance of a catastrophic meltdown is 1 in 6.
It's sometimes computationally convenient to have a rational approximation of
a decimal number. With a number like pi = 3.14159265359...
it might be handy
to remember that 22/7 is a reasonably close approximation (about 3.143).
Rationalize
uses a nice bit of math to find close rational approximations. The
Stern-Brocot tree
is a binary tree that holds all positive rational numbers whose numerator and
denominator are relatively prime (no common factors). To find a rational
approximation to a decimal number, a binary search is used to navigate the
tree until some termination condition is satisfied.
Each update of the binary search uses the mediant of two rational numbers.
If R1 = n1/d1
and R2 = n2/d2
are two rational numbers, and R1 < R2
,
the mediant is M = (n1 + n2) / (d1 + d2)
, which has the property
R1 <= M <= R2
. For a target decimal d
and R1 < d < R2
, the mediant
is used to recursively narrow the range of the bracketing rationals.
Because of the properties of the Stern-Brocot tree and the mediant-based search,
the rational returned by closest_rational
will always have relatively
prime numerator and denominator.
Rationalize
grew from a specific use case, but it could be generalized to
be a more complete library for working with rational numbers. Possibilities
for extending Rationalize
include
- adaptively computing the maximum denominator based on the size of the decimal.
- adding more utilities for working with rational numbers (e.g., add, multiply, divide, subtract), and bringing these into the top-level API.