When a third has to stay a third
A fraction is two integers and one invariant: the denominator is positive and the pair has no common factor. Everything else — arithmetic, comparison, rounding, formatting — follows from keeping that invariant on the way in.
Exact, all the way down
Addition crosses the denominators and reduces; there is no step where the value is approximated. Sum a third and a sixth and you get a half, not a double that nearly is one.
let a = @moonnum.parse_fraction("1/3")
let b = @moonnum.parse_fraction("1/6")
a + b
a * @moonnum.parse_fraction("3")
a + @moonnum.parse_fraction("0.5")
1/2
1
5/6
Two ways in, and they mean different things
parse_fraction("0.1") reads the text as a decimal literal, so it
is exactly one tenth. Fraction::of_float(0.1) reads the double,
so it is the 3602879701896397/36028797018963968 the hardware holds. Both are
correct; they answer different questions.
@moonnum.parse_fraction("0.1")
@moonnum.Fraction::of_float(0.1)
@moonnum.as_ratio(0.1)
@moonnum.Fraction::of_float(0.1).to_double()
1/10
3602879701896397/36028797018963968
3602879701896397/36028797018963968
0.1
Rounding returns a fraction, not a float
round with a digit count rounds to a decimal place, half to
even, and gives back a fraction — so the result can go on being exact.
Without a digit count it gives an integer.
@moonnum.parse_fraction("7/2").round_big()
@moonnum.parse_fraction("5/2").round_big()
@moonnum.parse_fraction("1/3").round(ndigits=3)
@moonnum.parse_fraction("-7/2").floor_big()
@moonnum.parse_fraction("-7/2").ceil_big()
4
2
333/1000
-4
-3
Powers stay ratios
An integer exponent is applied to the numerator and the denominator separately; nothing is converted to a double on the way. A negative exponent reciprocates, so only a zero base can refuse.
@moonnum.parse_fraction("2/3").pow(3N)
@moonnum.parse_fraction("2/3").pow(-1N)
@moonnum.parse_fraction("7/2").pow(10N)
@moonnum.parse_fraction("0").pow(-1N)
8/27
3/2
282475249/1024
division by zero
The best small denominator
limit_denominator finds the closest fraction whose denominator
does not exceed a bound, by walking the continued fraction. It is how a
measurement becomes a ratio you can write on a drawing.
let pi = @moonnum.as_ratio(3.141592653589793)
pi
pi.limit_denominator(max=10N)
pi.limit_denominator(max=1000N)
884279719003555/281474976710656
22/7
355/113
Printing a ratio, or printing its value
With no presentation type, a fraction prints as a ratio and the grouping flag applies to both halves. With a float type, it is rounded to the digits you asked for — exactly, from the rational, not through a double.
let third = @moonnum.parse_fraction("1/3")
third.format("")
third.format(".6f")
third.format(">12.4f")
third.format(".3%")
@moonnum.parse_fraction("5/2").format("#")
1/3
0.333333
0.3333
33.333%
5/2
Exactness shows up where floats drift. At ten digits both
agree: 3.1428571429. At twenty they do not, and the difference
is the whole argument for having a fraction type:
@moonnum.format(@moonnum.parse("22/7"), ".20f")
@moonnum.format_float(22.0 / 7.0, ".20f")
3.14285714285714285714
3.14285714285714279370