# Formatting floating point number as mixed fractional in Ruby

I have to format numbers that are either whole numbers or mixed fractional numbers with quarters (1/4, 1/2, 3/4) or thirds (1/3, 2/3).

The numbers are given as floats, for example 1.3333333 or 1.5 and are therefore inaccurate (but always close to a quarter or a third).

The formatting method is supposed to return Unicode fractions, for example:

1.0    #=> 1
1.3333 #=> 1⅓
1.5    #=> 1½
1.6667 #=> 1⅔


My current approach is to format the number via sprintf and to apply a second formatting based on the result:

def format_fraction(number)
fix, frac = sprintf('%0.2f', number).split('.')
case frac
when '00' then "#{fix}"
when '25' then "#{fix}¼"
when '33' then "#{fix}⅓"
when '50' then "#{fix}½"
when '67' then "#{fix}⅔"
when '75' then "#{fix}¾"
end
end


Is there a better / less string-based / more numeric way to solve this?

• Do you need to support only quarters and thirds? In any case, I think your current solution is pretty good. Ideally, you'd be using rationals instead of floats, but it seems you don't control that part – Jonah Sep 18 '17 at 23:34
• You could start with something like: fix, frac = number.rationalize(0.1).divmod(1) but I'm not sure that would make your code shorter unless you add a method to the Rational class. – Marc Rohloff Sep 19 '17 at 4:56
• @Jonah yes, it's only quarters and thirds. The values come from a JSON API with SQL backend. I could request a change on that part, but if I'm not mistaken, neither JSON nor SQL have a rational type. Besides, how would I turn 4/3r into "1⅓" in Ruby? – Stefan Sep 19 '17 at 6:08
• @Stefan, well you could just do your matching using == instead of the string stuff if you were working with rationals. But it's hardly worth a backend change. Your current solution is exactly what I'd do here. – Jonah Sep 19 '17 at 6:11
• @MarcRohloff I think it has to be rationalize(0.01), but that is indeed a good suggestion! I wasn't aware of that argument. – Stefan Sep 19 '17 at 6:12

Following Marc Rohloff's suggestion, I'm using Float#rationalize with an argument of 0.01 to convert the floats to rational numbers:

1.0.rationalize(0.01)
#=> (1/1)

1.3333.rationalize(0.01)
#=> (4/3)

1.5.rationalize(0.01)
#=> (3/2)

1.6666.rationalize(0.01)
#=> (5/3)


divmod can then be used to get the fixed and fractional part:

(5/3r).divmod(1)
#=> [1, (2/3)]


Applied to my method:

def format_fraction(number)
fix, frac = number.rationalize(0.01).divmod(1)
case frac
when 0/1r then "#{fix}"
when 1/4r then "#{fix}¼"
when 1/3r then "#{fix}⅓"
when 1/2r then "#{fix}½"
when 2/3r then "#{fix}⅔"
when 3/4r then "#{fix}¾"
end
end

format_fraction(1.0)    #=> "1"
format_fraction(1.3333) #=> "1⅓"
format_fraction(1.5)    #=> "1½"
format_fraction(1.6666) #=> "1⅔"


It's not a huge change, but the when arguments now resemble the output more closely, which makes the code easier to understand IMO.

The method could be further shortened by extracting the rational => string pairs:

FRACTIONS = { 1/4r => '¼', 1/3r => '⅓', 1/2r => '½', 2/3r => '⅔', 3/4r => '¾' }

def format_fraction(number)
fix, frac = number.rationalize(0.01).divmod(1)
[fix, FRACTIONS[frac]].join
end

• I like the last version, it would make it easier to add representations later. It might be worth adding a fallback though, just in case. – Marc Rohloff Sep 19 '17 at 15:22
• @MarcRohloff I could use FRACTIONS.fetch(frac) instead which would raise an exception if the key doesn't exist, e.g. when attempting to call format_fraction(1.2) which results in a frac value of 1/5r. – Stefan Sep 19 '17 at 15:27
• I was thinking of something like FRACTIONS[frac] || format('%03i',frac*1000) – Marc Rohloff Sep 19 '17 at 19:40
• @MarcRohloff I see, but in my case, a fraction that is neither a third nor a quarter would indeed be an error. – Stefan Sep 20 '17 at 10:40
• In that case I would prefer a more descriptive error like: FRACTIONS[frac] || raise "#{number} is not a valid fraction" you would also need to add 0/1r => '' to your list – Marc Rohloff Sep 20 '17 at 15:29