Supporting fractions in Ruby/Rails
Unicode comes with a selection of common single-character fractions like ½, ¼, and even ⅐ (see en.wikipedia.org/wiki/Number_Forms for more), but I need to store and display somewhat arbitrary fractions in a Rails app and it isn’t something I’ve had to do before so I’m documenting my workings.
Ruby has the Rational
class that makes it easy to work with fractions and can perform a specific calculation I need (½ + ⅐ = ⁹⁄₁₄):
a = Rational(1, 2)
# => (1/2)
b = Rational(1, 7)
# => (1/7)
a + b
# => (9/14)
My numbers are stored in a PostgreSQL database which doesn’t support rationals but fortunately in Ruby any number can be turned into a Rational
with #to_r
:
0.5.to_r
# => (1/2)
Unfortunately you don’t often get a sensible fraction:
(9.0 / 14).to_r
# => (5790342378047781/9007199254740992)
A Rational
can be simplified with Rational#rationalize
and with a bit of trial and error I determined that to arrive at ⁹⁄₁₄ I need to store the number with 3 decimal places and pass a magic number of ~0.005 to Rational#rationalize
:
0.643.to_r
# => (2895814560399229/4503599627370496)
0.643.to_r.rationalize(0.1)
# => (2/3)
0.643.to_r.rationalize(0.01)
# => (7/11)
0.643.to_r.rationalize(0.001)
# => (9/14) ✅
# Can I store the number with two decimal places? No.
0.64.to_r.rationalize(0.001)
# => (16/25)
# What magic number do I need?
0.643.to_r.rationalize(0.005)
# => (9/14) ✅
Next, I can never remember how to use decimals with Rails/PostgreSQL. The following Rails migration adds a decimal type column that can store numbers up to 999.999 — precision: 6
means the number can have total of 6 digits, scale: 3
means 3 of those digits come after the decimal point (surely it should be the other way round, scale for how big the number is and precision for its decimal places?):
add_column :standings, :points, :decimal, precision: 6, scale: 3
Now that I’m storing the number with an appropriate fidelity and can turn it into a sensible fraction I want it to look nice. The following Rails helper method determines if a number is a fraction and formats it with <sup>
/<sub>
tags:
module ApplicationHelper
def fraction(numerator, denominator)
capture do
concat tag.sup(numerator)
concat '⁄' # Unicode fraction slash.
concat tag.sub(denominator)
end
end
def points(number)
return '0' if number.zero?
rational = number.to_r.rationalize(0.005)
whole, remainder = rational.numerator.divmod(rational.denominator)
capture do
concat whole.to_s unless whole.zero?
concat fraction(remainder, rational.denominator) unless remainder.zero?
end
end
end
Storing approximate values in the database means you might have to be careful about losing precision, for example ⅐ × 49 = 7 but 0.143 × 49 ≠ 7:
BigDecimal('0.143').to_r.rationalize(0.005) * 49
# => (7/1)
BigDecimal('0.143') * 49
# => 0.7007e1
(BigDecimal('0.143') * 49).to_r.rationalize(0.005)
# => (589/84)
Luckily I perform limited calculations on my data so this isn’t something I have to worry about.
Bonus: Unicode fractions
I happened upon a site that demonstrates how to construct fractions from Unicode superscript and subscript characters. Even better is that the source is on GitHub so I borrowed the approach and implemented it in Ruby/Rails:
module ApplicationHelper
SUB = %w(₀ ₁ ₂ ₃ ₄ ₅ ₆ ₇ ₈ ₉)
SUP = %w(⁰ ¹ ² ³ ⁴ ⁵ ⁶ ⁷ ⁸ ⁹)
def fraction(numerator, denominator)
numerator.digits.reverse.map { |char| SUP.fetch(char) }.join \
+ '⁄' \ # Unicode fraction slash.
+ denominator.digits.reverse.map { |char| SUB.fetch(char) }.join
end
end