16

In Ruby, 0.0 * -1 == -0.0.

I have an application where I multiply a bunch of Float objects with -1, but I don't like the -0.0 in the output, since it's confusing.

Is there a smart way of making Float#to_s output 0.0 instead of -0.0?

I'm completely fine with running every Float object through some kind of scrubber/helper method, but the following just tends to make me even more confused:

def clean_output(amount)
  if amount.zero?
    0.0
  else
    amount
  end
end

UPDATE:

To be more precise on what I'm looking for, I want a solution that I can run on a whole bunch of floats, some of which will be negative, some positive. The negative ones should remain negative unless they're negative zeroes, i.e. -0.0.

Examples:

clean_output(-0.0) #=>  0.0
clean_output(-3.0) #=> -3.0
clean_output(3.0)  #=>  3.0
mskfisher
  • 3,291
  • 4
  • 35
  • 48
Frost
  • 11,121
  • 3
  • 37
  • 44

5 Answers5

13

There is actually a solution which does not require a condition.

def clean_output(value)
  value + 0
end

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.

Frost
  • 11,121
  • 3
  • 37
  • 44
  • 1
    If people only ever used code they're familiar with, there wouldn't be much learning happening. Just saying.. – cvshepherd Jan 03 '12 at 23:14
12

If the code you wrote confuses you then this ought to really bend your mind:

def clean_output(amount)
  amount.zero? && 0.0 || amount
end

With some proof:

irb(main):005:0> f = 0.0
=> 0.0
irb(main):006:0> f.zero? && 0.0 || f
=> 0.0
irb(main):007:0> f = -0.0
=> -0.0
irb(main):008:0> f.zero? && 0.0 || f
=> 0.0
irb(main):009:0> f=1.0
=> 1.0
irb(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
  end
end

def clean_output2(amount)
  amount.zero? && 0.0 || amount
end

def clean_output3(value)
  value + 0
end

class Numeric
  def clean_to_s
    (nonzero? || abs).to_s
  end
end


n = 5_000_000
Benchmark.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        real
clean_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        real
clean_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
  end
end

def clean_output2(amount)
  amount.zero? && 0.0 || amount
end

def clean_output3(value)
  value + 0
end

class Numeric
  def clean_to_s
    (nonzero? || abs).to_s
  end

  def clean_no_to_s
    nonzero? || abs
  end

end


n = 5_000_000
Benchmark.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        real
clean_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        real
clean_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_000
Benchmark.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        real
nonzero?:  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        real
nonzero?:  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)
the Tin Man
  • 158,662
  • 42
  • 215
  • 303
  • Thank you very much for this answer. It's really informative. The benchmarks aren't really that relevant for my application, but it's very interesting to see that the clean_to_s is so much slower than the other solutions. – Frost Jan 03 '12 at 17:54
  • 1
    The difference won't be that big if you remove `to_s` call. If you're going to print that values it'll be called anyway, but in this benchmark it's called explicitly in `clean_to_s`. Though, `nonzero?` + `abs` is slower anyway most likely because it involves two method calls instead of one in other cases. – KL-7 Jan 03 '12 at 18:15
  • @KL-7, "The difference won't be that big if you remove to_s call." I added some more tests to see. – the Tin Man Jan 04 '12 at 05:34
4

I can't think of anything better than that:

def clean_output(value)
  value.nonzero? || value.abs
end

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
  end
end

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"]
KL-7
  • 46,000
  • 9
  • 87
  • 74
  • Hm... and here I was assuming that Numeric#nonzero? was supposed to return a boolean value. – Frost Jan 03 '12 at 12:18
  • No, it returns object itself if it's not zero and `nil` otherwise. See [docs](http://www.ruby-doc.org/core-1.9.3/Numeric.html#method-i-nonzero-3F). – KL-7 Jan 03 '12 at 12:21
  • I did. I guess I'm just a bit naive in thinking that methods ending in `?` should return an actual boolean, but it's obviously very usable this way. – Frost Jan 03 '12 at 12:57
0

To me, the intent of this code is a little clearer and at least in Ruby 1.9.3 it's slightly faster than @the Tin Man's

def clean_output4(amount)
  amount.zero? ? 0.0 : amount
end

                     user     system      total        real
clean_output:    0.860000   0.000000   0.860000 (  0.859446)
clean_output4:   0.830000   0.000000   0.830000 (  0.837595)
Josh
  • 8,329
  • 4
  • 36
  • 33
0

simply just check whether answer is zero then apply abs to the value. It will convert -0.0 into 0.0

fl_num = -0.0
fl_num = fl_num.abs

fl_num = 0.0
Darshit Gajjar
  • 746
  • 4
  • 13
  • 26
  • Sure, but that won't do for any other value. Since I have a whole bunch of floats, some of them may be actual negative numbers, and in that case they should stay that way. `-3.0.abs` is not `-3.0`. I'm looking for something that only affects a negative zero (`-0.0`). – Frost Jan 03 '12 at 10:58
  • @Martin okay, but you can check if **value** is zero then apply else it will remain same negative number, right ? – Darshit Gajjar Jan 03 '12 at 11:02
  • You mean `if f1_num.zero?; f1_num.abs; else; f1_num; end`? That's basically the same solution as I wrote in the question... I guess one part of my question is whether you can solve this without an `if`. – Frost Jan 03 '12 at 11:05
  • you could extend floats to have a function that'll just return zero if it's a zero. – Joseph Le Brech Jan 03 '12 at 11:10
  • had a look on whole documentation and also never think like you did. :) So, can't find any solution for that bcoz as per IEEE standards there are two zeros, +0 and -0. Hence, condition is required. – Darshit Gajjar Jan 03 '12 at 11:20
  • And by extension: `1.0/0.0 => Infinity` and `1.0/-0.0 => -Infinity`. – the Tin Man Jan 03 '12 at 16:50