Saturday, September 19, 2009

Pretty Floats

‹prev | My Chain | next›

Today, I would like to get pretty printing of Float instances working. Specifically I would like:
  • 1.125.to_s == '1⅛'
  • 1.25.to_s == '1¼'
  • 0.875.to_s == '⅞'
First up, I describe existing behavior that should not change:
describe Float, "pretty printing" do
specify "1.23.to_s == '1.23'" do
1.23.to_s.should == "1.23"
end
As expected, that example passes without any changes. Next up, I describe precision. There is no reason to specify more than two decimal points when trying to measure things, so, in RSpec format:
  specify "1.236.to_s == '1.24'" do
1.236.to_s.should == "1.24"
end
That fails with:
1)
'Float to_s should only include 2 decimals precision' FAILED
expected: "1.24",
got: "1.236" (using ==)
./spec/float_spec.rb:8:
To get that to pass, I re-open the Float to override to_s:
class Float
def to_s
"%.2f" % self
end
end
The updated Float class is not automatically included in my Sinatra application (and thus in the RSpec examples). I save it in the lib directory, and then add this to the Sinatra application:
$: << File.expand_path(File.dirname(__FILE__) + '/lib')

require 'float'
For the remaining numbers that are likely to appear in recipes using imperial units of measure, I use these examples:
  specify "0.25.to_s == '¼'"
specify "0.5.to_s == '½'"
specify "0.75.to_s == '¾'"
specify "0.33.to_s == '⅓'"
specify "0.66.to_s == '⅔'"
specify "0.125.to_s == '⅛'"
specify "0.325.to_s == '⅜'"
specify "0.625.to_s == '⅝'"
specify "0.875.to_s == '⅞'"
I end up using a long case statement to get that passing:
class Float
def to_s
int = self.to_i
frac = pretty_fraction(self - int)

if frac
(int == 0 ? "" : int.to_s) + frac
else
"%.2f" % self
end
end

private

def pretty_fraction(fraction)
case fraction
when 0.25
"¼"
when 0.5
"½"
when 0.75
"¾"
when 0.33
"⅓"
when 0.66
"⅔"
when 0.125
"⅛"
when 0.325
"⅜"
when 0.625
"⅝"
when 0.875
"⅞"
end
end
end
In addition to pretty printing things that look like fractions, I also want to print things that look like integers:
  specify "1.0.to_s == '1'" do
1.0.to_s.should == "1"
end
I get that to pass with another condition on the case statement:
    case fraction
...
when 0
""
end
Last up is a need to account for the cases when the author goes overboard with precision. That is, what happens if I entered 0.67 or 0.667 to mean two-thirds. In RSpec:
  specify "0.33.to_s == '⅓'" do
0.33.to_s.should == "⅓"
end
specify "0.333.to_s == '⅓'" do
0.333.to_s.should == "⅓"
end
specify "0.66.to_s == '⅔'" do
0.66.to_s.should == "⅔"
end
specify "0.667.to_s == '⅔'" do
0.667.to_s.should == "⅔"
end
Rather than account for all possible cases, I use a range in my case statement:
  def pretty_fraction(fraction)
case fraction
...
when 0.33..0.34
"⅓"
when 0.66..0.67
"⅔"
...
end
end
This makes use of case statements performing a triple equality operation on the target and that:
>> (0.66..0.67) === 0.666
=> true
With my floats printing nicely, I have cleared up the most bothersome of the minor issues that I found on the beta site. Tomorrow, I will work through a few more issues and redeploy.

No comments:

Post a Comment