How can I efficiently scrub Ruby's negative zero float? How can I efficiently scrub Ruby's negative zero float? ruby ruby

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"]