Saying stuff about stuff.

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