24

I have the following method to compute an average:

def compute_average(a,b,c,d,e)
  total = [a,b,c,d,e].sum.to_f
  average = [a, 2*b, 3*c, 4*d, 5*e].sum / total
  average.round(2)
end

It's nothing special, but it has a problem that I expect all average equations have: it might divide by zero if inputs are all zero.

So, I thought of doing this:

def compute_average(a,b,c,d,e)
  total = [a,b,c,d,e].sum.to_f
  if total==0
    average = 0.00
  else
    average = [a, 2*b, 3*c, 4*d, 5*e].sum / total
    average.round(2)
  end
end

... and that works, but it feels kludgy to me. Is there a more elegant, "Ruby Way" to avoid this division by zero problem?

What I'm wishing I had was an "unless then" operator, like...

average = numerator / denominator unless denominator == 0 then 0

Any suggestions?

Andrew
  • 42,517
  • 51
  • 181
  • 281
  • Is there `Array#sum`? I don't have it. – sawa Apr 03 '11 at 16:51
  • 4
    that's a strange average function. more normal averages (arithmetic/geometric) divide by the number of elements, so they don't really have that problem unless you try to take the average of an empty set. – Mat Apr 03 '11 at 16:53
  • Well, yeah, the reason is I'm using this to compute the average of a number of votes, so a, b, c, d, and e are the number of 1 star, 2 star, 3 star, 4 star, and 5 star votes total, so I'm computing the "average number of stars" from all the votes. Yes it's a little bit of an odd case I suppose. – Andrew Apr 03 '11 at 17:06
  • @sawa -- that's funny, I just tried it once and it worked so I use it now when I would otherwise need or want lots of parenthesis. I'm on Ruby 1.9.2 ... – Andrew Apr 03 '11 at 17:09
  • @Andrew I use ruby1.9.2 as well, but I don't have it. I agree; it would be convinient if I had that. Maybe it is in some external gem or something? – sawa Apr 03 '11 at 17:17
  • It must be a rails thing then. I'm using this particular function within a rails app, and I haven't had the occasion to try something like this outside of rails yet. I suppose you've spared me the disappointment of finding out this was rails-specfic later on, but I think it would be relatively easy to mixin to your array class if you wanted to. – Andrew Apr 03 '11 at 17:22
  • Thanks, I just wasn't sure about the specifics of it. The question is not essentially rails-specific. Good to know there is such thing. – sawa Apr 03 '11 at 17:25
  • 1
    Yeah rails adds it in ActiveSupport. It's easy to add yourself though `reduce 0, &:+`. – Jakub Hampl Apr 03 '11 at 17:40
  • @Jakub: it's enough with reduce(0, :+) ;-) – tokland Apr 03 '11 at 17:58
  • @tokland Ah yes, the little magic trick :D – Jakub Hampl Apr 03 '11 at 18:01

8 Answers8

71

You can use nonzero?, as in:

def compute_average(a,b,c,d,e)
  total = [a,b,c,d,e].sum.to_f
  average = [a, 2*b, 3*c, 4*d, 5*e].sum / (total.nonzero? || 1)
end

More people would be more familiar with using the ternary operator (total == 0 ? 1 : total), so that's another possibility.

Marc-André Lafortune
  • 78,216
  • 16
  • 166
  • 166
  • I like this and also Jakub's method, but yours keeps the assignment at the front -- which I realize isn't really necessary, but which I like because it helps me remember what I'm doing when I look at this later on. – Andrew Apr 03 '11 at 17:25
  • This can give different results in the case of `compute_average(1,0,0,0,-1) #=> -4` whereas @Andrew's and mine will give `0` at that point. – Jakub Hampl Apr 03 '11 at 17:48
  • A bit too subtle... it relies on the fact that the numerator is also 0 when total is 0, which is an orthogonal fact. A plain usage of the ternary operation would seem more clear: average = (total.zero? ? 0 : [a, 2*b, 3*c, 4*d, 5*e].sum / total).round(2) – tokland Apr 03 '11 at 17:49
  • This function is only correct when assuming positive input - which considering it's counting stars may be a sound assumption. – Jakub Hampl Apr 03 '11 at 18:03
  • That's a good point - although in this case there's no possibility of negative input. Thanks for the tip! – Andrew Apr 03 '11 at 18:42
  • This function will still raise an exception if any parameter is `nil` because `sum` raises `TypeError: NilClass can't be coerced into Fixnum` in this case. – Nate Nov 10 '14 at 21:29
  • just wanted to let you know this fails in case of floating point i.e. 0.0/0.0 – Akshat Nov 25 '15 at 09:17
8

While this is an outdated thread I thought I would chime in with a simple one liner you can use...

@average = variable1 / variable2 rescue 0
Jonathan Reyes
  • 1,387
  • 1
  • 10
  • 23
  • Very elegant solution. – Daniel Bonnell Sep 13 '16 at 16:36
  • 4
    Don't do this, inline rescues are bad. They will mask every error that happens in that line (consider `variable1` is a method name that does some calculations), and are very hard to debug. – 23tux Feb 26 '17 at 14:17
8

It's common to rely on rescue to capture the exception, then to return the default value:

def compute_average(a, b, c, d, e)
  total = [a, b, c, d, e].sum.to_f
  average = [ a, 2*b, 3*c, 4*d, 5*e ].sum / total
  average.round(2)
  rescue ZeroDivisionError
    0.0
end

Also I'd write:

average = numerator / denominator unless denominator == 0 then 0

as

average = (denominator == 0) ? 0 : numerator / denominator
the Tin Man
  • 158,662
  • 42
  • 215
  • 303
  • This is extremely inefficient if the rescue is triggered more than [5% of the time](http://technicaldebt.com/the-cost-of-using-rubys-rescue-as-logic/) but at least you aren't hiding other errors. One note is if any parameter is nil, the function will still throw `TypeError: NilClass can't be coerced into Fixnum` (Sorry about the delete and commend. I accidently pated the Google search URL.) – Nate Nov 10 '14 at 21:25
3

To me, the cleanest way is:

numerator / denominator rescue 0

It also saves you from handling 0 / 0.

As @Andrew points out, this is only valid for integers. See the comments to this answer for more info.

espinchi
  • 9,144
  • 6
  • 58
  • 65
  • Just checking, if you use rescue in postfix it will only save the current expression? ie. would this work? `a = raise 'not this' rescue 'this'; a #=> 'this'` – Andrew Dec 20 '13 at 16:41
  • 2
    So, just fiddling with this, if one of your args is a float, then this won't work as it will return infinity, so this is only viable if you're doing integers. With division I figure you're often going to be interested in fractional results, so floats are likely to be part of the equation. Nice idea, though! – Andrew Dec 20 '13 at 16:46
  • 1
    I just got burn't by using this technique on a float. After the division it returns NaN rather that triggering the rescue. Grrr. – TJChambers Sep 11 '14 at 16:03
  • This is extremely inefficient if the rescue is triggered more than [5% of the time](http://technicaldebt.com/the-cost-of-using-rubys-rescue-as-logic/) and it can [hide other errors](http://devblog.avdi.org/2012/11/19/rubytapas-022-inline-rescue/) making debugging difficult. (Sorry about the delete and commend. I accidently pated the Google search URL.) – Nate Nov 10 '14 at 21:15
3
def compute_average(a,b,c,d,e)
  total = (a+b+c+d+e).to_f
  total.zero? ? 0 : ((a + 2*b + 3*c + 4*d + 5*e) / total).round(2)
end
Jakub Hampl
  • 39,863
  • 10
  • 77
  • 106
  • wouldn't the ternary operator be more idiomatic here? – tokland Apr 03 '11 at 17:42
  • Yup, the double question mark isn't visually nice (and you always forget to write the second one!). But I guess at the end you just get use to it. – tokland Apr 03 '11 at 17:54
2

TL;DR: One possible solution

def compute_average(*values)

  # This makes sure arrays get flattened to a single array.
  values.flatten!

  # Throws away all nil values passed as arguments.
  values.reject!(&:nil?)

  # Throws away all non-numeric values.
  # This includes trashing strings that look like numbers, like "12".
  values.keep_if{ |v| v.is_a? Numeric }

  total = values.sum.to_f
  return Float::NAN if total.zero?

  # I'm not sure what this business is
  #   average = [a, 2*b, 3*c, 4*d, 5*e].sum / total
  # but it can be translated to
  average = values.each_with_index.map{ |v,i| v*(i+1) }.sum / total

  average.round(2)
end

This protects against all cases:

compute_average(1,2,3,4,5)
=> 3.67

compute_average(0,0,0,0,0)
=> NaN

compute_average(1,2,nil,4,5)
=> 3.08

compute_average(1,2,"string",4,5)
=> 3.08

compute_average(1)
=> 1.0

compute_average([1,2,3,4,5])
=> 3.67

compute_average
=> NaN

Original function:

def compute_average(a,b,c,d,e)
  total = [a,b,c,d,e].sum.to_f
  average = [a, 2*b, 3*c, 4*d, 5*e].sum / total
  average.round(2)
end

Consider checking for zero:

def compute_average(a,b,c,d,e)
  total = [a,b,c,d,e].sum.to_f
  return if total.zero?
  average = [a, 2*b, 3*c, 4*d, 5*e].sum / total
  average.round(2)
end

This change only protects against one case:

compute_average(1,2,3,4,5)
# => 3.67

compute_average(0,0,0,0,0)
# => nil

compute_average(1,2,nil,4,5)
# => TypeError: NilClass can't be coerced into Fixnum

compute_average(1,2,"string",4,5)
# => TypeError: String can't be coerced into Fixnum

compute_average(1)
# => ArgumentError: wrong number of arguments calling `compute_average` (1 for 5)

compute_average([1,2,3,4,5])
# => ArgumentError: wrong number of arguments calling `compute_average` (1 for 5)

compute_average
# => ArgumentError: wrong number of arguments calling `compute_average` (0 for 5)

Consider using an inline rescue

def compute_average(a,b,c,d,e)
  total = [a,b,c,d,e].sum.to_f
  average = [a, 2*b, 3*c, 4*d, 5*e].sum / total rescue 0
  average.round(2)
end

This change only protects against one case, also:

compute_average(1,2,3,4,5)
# => 3.67

compute_average(0,0,0,0,0)
# => NaN

compute_average(1,2,nil,4,5)
# => TypeError: NilClass can't be coerced into Fixnum

compute_average(1,2,"string",4,5)
# => TypeError: String can't be coerced into Fixnum

compute_average(1)
# => ArgumentError: wrong number of arguments calling `compute_average` (1 for 5)

compute_average([1,2,3,4,5])
# => ArgumentError: wrong number of arguments calling `compute_average` (1 for 5)

compute_average
# => ArgumentError: wrong number of arguments calling `compute_average` (0 for 5)

Using an inline rescue has another consequence. Consider this typo:

def compute_average(a,b,c,d,e)
  total = [a,b,c,d,e].sum.to_f
  average = [a, 2*b, 3*c, 4*d, 5*e].smu / total rescue 0
  #                                 ^^^
  average.round(2)
end

compute_average(1,2,3,4,5)
# => 0.0

compute_average(0,0,0,0,0)
# => 0.0

Consider using a rescue

def compute_average(a,b,c,d,e)
  total = [a,b,c,d,e].sum.to_f
  average = [a, 2*b, 3*c, 4*d, 5*e].sum / total
  average.round(2)
rescue ZeroDivisionError
  0.0
end

This is better, as it does not hide errors, but protects against the same scenario as the incline rescue above.

Another version with what I would call a normal average calculation

As a side note, the average operation I am familiar with is calculated using total/count, so here is a version that does that.

def compute_average(*values)

  # This makes sure arrays get flattened to a single array.
  values.flatten!

  # Throws away all nil values passed as arguments.
  values.reject!(&:nil?)

  # Throws away all non-numeric values.
  # This includes trashing strings that look like numbers, like "12".
  values.keep_if{ |v| v.is_a? Numeric }

  total = values.sum.to_f
  count = values.count
  return Float::NAN if count.zero?

  total / count
end

This protects against all cases:

compute_average(1,2,3,4,5)
=> 3.0

compute_average(0,0,0,0,0)
=> 0.0

compute_average(1,2,nil,4,5)
=> 3.0

compute_average(1,2,"string",4,5)
=> 3.0

compute_average(1)
=> 1.0

compute_average([1,2,3,4,5])
=> 3.0

compute_average
=> NaN
Nate
  • 12,963
  • 4
  • 59
  • 80
0

I'm not much of a Ruby-ist, but I'd do it like this:

average = denominator.nonzero? ? numerator/denominator : 0

There is probably a better answer, but this might suffice.

Marlies
  • 927
  • 1
  • 5
  • 18
0

/ does not return a zero division error if either the number to be divided or the denominator is a float.

def compute_average(a,b,c,d,e)
  total = [a,b,c,d,e].sum.to_f
  average = [a, 2*b, 3*c, 4*d, 5*e].sum / total
  average.finite? ? average.round(2) : 0.0
end

More generally, under ruby1.9,

def compute_average *args
  average = args.to_enum.with_index.map{|x, w| x * w}.sum / args.sum.to_f
  average.finite? ? average.round(2) : 0.0
end
sawa
  • 165,429
  • 45
  • 277
  • 381