How can I efficiently scrub Ruby's negative zero float?
There is actually a solution which does not require a condition.
def clean_output(value) value + 0end
output:
> clean_output(3.0)=> 3.0 > clean_output(-3.0)=> -3.0 > clean_output(-0.0)=> 0.0
I don't actually like this solution better than the one I accepted, because of lack of clarity. If I'd see this in a piece of code I didn't write myself, I'd wonder why you'd want to add zero to everything.
It does solve the problem though, so I thought I'd share it here anyway.
If the code you wrote confuses you then this ought to really bend your mind:
def clean_output(amount) amount.zero? && 0.0 || amountend
With some proof:
irb(main):005:0> f = 0.0=> 0.0irb(main):006:0> f.zero? && 0.0 || f=> 0.0irb(main):007:0> f = -0.0=> -0.0irb(main):008:0> f.zero? && 0.0 || f=> 0.0irb(main):009:0> f=1.0=> 1.0irb(main):010:0> f.zero? && 0.0 || f=> 1.0
I don't like using nonzero?
because its use-case is a bit confused. It's part of Numeric but the docs show it used as part of Comparable with the <=>
operator. Plus, I'd rather test for a zero condition for this purpose because it seems more straightforward.
And, though the OP's code might appear verbose, this is another of those cases where premature optimization doesn't pay off:
require 'benchmark'def clean_output(amount) if amount.zero? 0.0 else amount endenddef clean_output2(amount) amount.zero? && 0.0 || amountenddef clean_output3(value) value + 0endclass Numeric def clean_to_s (nonzero? || abs).to_s endendn = 5_000_000Benchmark.bm(14) do |x| x.report( "clean_output:" ) { n.times { a = clean_output(-0.0) } } x.report( "clean_output2:" ) { n.times { a = clean_output2(-0.0) } } x.report( "clean_output3:" ) { n.times { a = clean_output3(-0.0) } } x.report( "clean_to_s:" ) { n.times { a = 0.0.clean_to_s } }end
And the results:
ruby test.rb user system total realclean_output: 2.120000 0.000000 2.120000 ( 2.127556)clean_output2: 2.230000 0.000000 2.230000 ( 2.222796)clean_output3: 2.530000 0.000000 2.530000 ( 2.534189)clean_to_s: 7.200000 0.010000 7.210000 ( 7.200648)ruby test.rb user system total realclean_output: 2.120000 0.000000 2.120000 ( 2.122890)clean_output2: 2.200000 0.000000 2.200000 ( 2.203456)clean_output3: 2.540000 0.000000 2.540000 ( 2.533085)clean_to_s: 7.200000 0.010000 7.210000 ( 7.204332)
I added a version without the to_s
. These were run on my laptop, which is several years old, which is why the resulting times are higher than the previous tests:
require 'benchmark'def clean_output(amount) if amount.zero? 0.0 else amount endenddef clean_output2(amount) amount.zero? && 0.0 || amountenddef clean_output3(value) value + 0endclass Numeric def clean_to_s (nonzero? || abs).to_s end def clean_no_to_s nonzero? || abs endendn = 5_000_000Benchmark.bm(14) do |x| x.report( "clean_output:" ) { n.times { a = clean_output(-0.0) } } x.report( "clean_output2:" ) { n.times { a = clean_output2(-0.0) } } x.report( "clean_output3:" ) { n.times { a = clean_output3(-0.0) } } x.report( "clean_to_s:" ) { n.times { a = -0.0.clean_to_s } } x.report( "clean_no_to_s:" ) { n.times { a = -0.0.clean_no_to_s } }end
And the results:
ruby test.rb user system total realclean_output: 3.030000 0.000000 3.030000 ( 3.028541)clean_output2: 2.990000 0.010000 3.000000 ( 2.992095)clean_output3: 3.610000 0.000000 3.610000 ( 3.610988)clean_to_s: 8.710000 0.010000 8.720000 ( 8.718266)clean_no_to_s: 5.170000 0.000000 5.170000 ( 5.170987)ruby test.rb user system total realclean_output: 3.050000 0.000000 3.050000 ( 3.050175)clean_output2: 3.010000 0.010000 3.020000 ( 3.004055)clean_output3: 3.520000 0.000000 3.520000 ( 3.525969)clean_to_s: 8.710000 0.000000 8.710000 ( 8.710635)clean_no_to_s: 5.140000 0.010000 5.150000 ( 5.142462)
To sort out what was slowing down non_zero?
:
require 'benchmark'n = 5_000_000Benchmark.bm(9) do |x| x.report( "nonzero?:" ) { n.times { -0.0.nonzero? } } x.report( "abs:" ) { n.times { -0.0.abs } } x.report( "to_s:" ) { n.times { -0.0.to_s } }end
With the results:
ruby test.rb user system total realnonzero?: 2.750000 0.000000 2.750000 ( 2.754931)abs: 2.570000 0.010000 2.580000 ( 2.569420)to_s: 4.690000 0.000000 4.690000 ( 4.687808)ruby test.rb user system total realnonzero?: 2.770000 0.000000 2.770000 ( 2.767523)abs: 2.570000 0.010000 2.580000 ( 2.569757)to_s: 4.670000 0.000000 4.670000 ( 4.678333)
I can't think of anything better than that:
def clean_output(value) value.nonzero? || value.absend
but that's just a variation of your solution. Though, unlike yours this one doesn't change type of value
(if, for example, you pass -0
it'll return 0
). But looks like it's not important in your case.
If you're sure that'll make your code cleaner you can add method like that to Numeric
class (that will make that method available for Float
, Fixnum
, and other numeric classes):
class Numeric def clean_to_s (nonzero? || abs).to_s endend
and then use it:
-0.0.clean_to_s # => '0.0'-3.0.clean_to_s # => '-3.0'# same method for Fixnum's as a bonus-0.clean_to_s # => '0'
That will make easier to process an array of floats:
[-0.0, -3.0, 0.0, -0].map &:clean_to_s# => ["0.0", "-3.0", "0.0", "0"]