1
\$\begingroup\$

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?

\$\endgroup\$
  • \$\begingroup\$ 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 \$\endgroup\$ – Jonah Sep 18 '17 at 23:34
  • 1
    \$\begingroup\$ 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. \$\endgroup\$ – Marc Rohloff Sep 19 '17 at 4:56
  • \$\begingroup\$ @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? \$\endgroup\$ – Stefan Sep 19 '17 at 6:08
  • 1
    \$\begingroup\$ @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. \$\endgroup\$ – Jonah Sep 19 '17 at 6:11
  • \$\begingroup\$ @MarcRohloff I think it has to be rationalize(0.01), but that is indeed a good suggestion! I wasn't aware of that argument. \$\endgroup\$ – Stefan Sep 19 '17 at 6:12
3
\$\begingroup\$

[Answering my own question here]

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
| improve this answer | |
\$\endgroup\$
  • 1
    \$\begingroup\$ 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. \$\endgroup\$ – Marc Rohloff Sep 19 '17 at 15:22
  • \$\begingroup\$ @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. \$\endgroup\$ – Stefan Sep 19 '17 at 15:27
  • 1
    \$\begingroup\$ I was thinking of something like FRACTIONS[frac] || format('%03i',frac*1000) \$\endgroup\$ – Marc Rohloff Sep 19 '17 at 19:40
  • \$\begingroup\$ @MarcRohloff I see, but in my case, a fraction that is neither a third nor a quarter would indeed be an error. \$\endgroup\$ – Stefan Sep 20 '17 at 10:40
  • 1
    \$\begingroup\$ 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 \$\endgroup\$ – Marc Rohloff Sep 20 '17 at 15:29

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.