Ruby way: улов на деление на нула

Имам следния метод за изчисляване на средна стойност:

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

Не е нищо особено, но има проблем, който очаквам да имат всички средни уравнения: може да се дели на нула, ако всички входове са нула.

И така, реших да направя това:

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

... и това работи, но ми се струва тъпо. Има ли по-елегантен "Ruby Way" за избягване на този проблем с разделяне на нула?

Това, което ми се иска да имам, беше оператор "освен ако тогава", като...

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

Някакви предположения?


person Andrew    schedule 03.04.2011    source източник
comment
Има ли Array#sum? аз го нямам   -  person sawa    schedule 03.04.2011
comment
това е странна средна функция. по-нормалните средни стойности (аритметични/геометрични) се разделят на броя на елементите, така че всъщност нямат този проблем, освен ако не се опитате да вземете средната стойност на празен набор.   -  person Mat    schedule 03.04.2011
comment
Е, да, причината е, че използвам това, за да изчисля средната стойност на броя гласове, така че a, b, c, d и e са броят на 1 звезда, 2 звезди, 3 звезди, 4 звезди и Общо 5 звездни гласове, така че изчислявам средния брой звезди от всички гласове. Да, това е малко странен случай, предполагам.   -  person Andrew    schedule 03.04.2011
comment
@sawa -- това е смешно, току-що го опитах веднъж и се получи, така че го използвам сега, когато иначе бих се нуждаел или бих искал много скоби. Аз съм на Ruby 1.9.2 ...   -  person Andrew    schedule 03.04.2011
comment
@Andrew Използвам и ruby1.9.2, но го нямам. Съгласен съм; би било удобно, ако имах това. Може би е в някакъв външен скъпоценен камък или нещо подобно?   -  person sawa    schedule 03.04.2011
comment
Тогава трябва да е нещо с релси. Използвам тази конкретна функция в приложение за rails и все още не съм имал възможност да опитам нещо подобно извън rails. Предполагам, че сте ми спестили разочарованието да разбера, че това е специфично за релсите по-късно, но мисля, че ще бъде сравнително лесно да се смесите с вашия клас масив, ако искате.   -  person Andrew    schedule 03.04.2011
comment
Благодаря, просто не бях сигурен за спецификата на това. Въпросът не е по същество специфичен за релсите. Добре е да знам, че има такова нещо.   -  person sawa    schedule 03.04.2011
comment
Yeah rails го добавя в ActiveSupport. Въпреки това е лесно да добавите себе си reduce 0, &:+.   -  person Jakub Hampl    schedule 03.04.2011
comment
@Jakub: стига с намаление(0, :+) ;-)   -  person tokland    schedule 03.04.2011
comment
@tokland Ах, да, малкият магически трик :D   -  person Jakub Hampl    schedule 03.04.2011


Отговори (8)


Можете да използвате nonzero?, както в:

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

Повече хора биха били по-запознати с използването на троичния оператор (total == 0 ? 1 : total), така че това е друга възможност.

person Marc-André Lafortune    schedule 03.04.2011
comment
Харесвам това, а също и метода на Якуб, но вашият запазва задачата отпред -- което осъзнавам, че не е наистина необходимо, но което ми харесва, защото ми помага да си спомня какво правя, когато гледам това по-късно. - person Andrew; 03.04.2011
comment
Това може да даде различни резултати в случай на compute_average(1,0,0,0,-1) #=> -4, докато @Andrew's и моят ще дадат 0 в този момент. - person Jakub Hampl; 03.04.2011
comment
Малко твърде фино... разчита на факта, че числителят също е 0, когато общата сума е 0, което е ортогонален факт. Обикновеното използване на троичната операция би изглеждало по-ясно: средно = (общо.нула? ? 0 : [a, 2*b, 3*c, 4*d, 5*e].sum / total).round(2 ) - person tokland; 03.04.2011
comment
Тази функция е правилна само когато се приема положителен вход - което, като се има предвид, че брои звезди, може да е добро предположение. - person Jakub Hampl; 03.04.2011
comment
Това е добър момент - въпреки че в този случай няма възможност за отрицателен вход. Благодаря за бакшиша! - person Andrew; 03.04.2011
comment
Тази функция пак ще предизвика изключение, ако някой параметър е nil, защото sum повдига TypeError: NilClass can't be coerced into Fixnum в този случай. - person Nate; 11.11.2014
comment
просто исках да ви уведомя, че това се проваля в случай на плаваща запетая, т.е. 0.0/0.0 - person Akshat; 25.11.2015

Обичайно е да се разчита на 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

Също така бих написал:

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

as

average = (denominator == 0) ? 0 : numerator / denominator
person the Tin Man    schedule 04.04.2011
comment
Това е изключително неефективно, ако спасяването се задейства повече от 5% от времето, но поне не криете други грешки. Една забележка е, че ако някой параметър е нула, функцията пак ще изведе TypeError: NilClass can't be coerced into Fixnum (Съжалявам за изтриването и похвала. Случайно отбелязах URL адреса за търсене в Google.) - person Nate; 11.11.2014

Макар че това е остаряла тема, реших да се включа с една проста подложка, която можете да използвате...

@average = variable1 / variable2 rescue 0
person Jonathan Reyes    schedule 15.12.2014
comment
Много елегантно решение. - person Daniel Bonnell; 13.09.2016
comment
Не правете това, вградените спасявания са лоши. Те ще маскират всяка грешка, която се случва в този ред (помислете, че variable1 е име на метод, който прави някои изчисления) и са много трудни за отстраняване на грешки. - person 23tux; 26.02.2017

За мен най-чистият начин е:

numerator / denominator rescue 0

Освен това ви спестява работа с 0 / 0.

Както посочва @Andrew, това е валидно само за цели числа. Вижте коментарите към този отговор за повече информация.

person espinchi    schedule 20.12.2013
comment
Просто проверявам, ако използвате rescue в postfix, ще запази само текущия израз? т.е. това ще свърши ли работа? a = raise 'not this' rescue 'this'; a #=> 'this' - person Andrew; 20.12.2013
comment
Така че, просто си играете с това, ако един от вашите аргументи е плаваща единица, тогава това няма да работи, тъй като ще върне безкрайност, така че това е жизнеспособно само ако правите цели числа. С делението предполагам, че често ще се интересувате от дробни резултати, така че е вероятно числата с плаваща единица да бъдат част от уравнението. Хубава идея обаче! - person Andrew; 20.12.2013
comment
Току-що не изгорях, използвайки тази техника на плувка. След разделянето връща NaN, а не задейства спасяването. Гррр. - person TJChambers; 11.09.2014
comment
Това е изключително неефективно, ако спасяването се задейства повече от 5% от времето и може да скрива други грешки което затруднява отстраняването на грешки. (Съжалявам за изтриването и препоръчвам. Случайно забелязах URL адреса за търсене в Google.) - person Nate; 11.11.2014

TL;DR: Едно възможно решение

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

Това предпазва от всички случаи:

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

Оригинална функция:

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

Помислете за проверка за нула:

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

Тази промяна предпазва само от един случай:

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)

Помислете за използване на вграден 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

Тази промяна предпазва само от един случай, също така:

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)

Използването на вграден 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].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

Помислете за използване на 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

Това е по-добре, тъй като не крие грешки, но предпазва от същия сценарий като наклона rescue по-горе.

Друга версия с това, което бих нарекъл нормално средно изчисление

Като странична бележка, средната операция, с която съм запознат, се изчислява чрез общ/брой, така че ето версия, която прави това.

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

Това предпазва от всички случаи:

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
person Nate    schedule 10.11.2014

Не съм много любител на Ruby, но бих го направил така:

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

Вероятно има по-добър отговор, но това може да е достатъчно.

person Marlies    schedule 03.04.2011
comment
nonzero? е в класа Numeric, така че ако denominator е nil, това пак ще изведе грешка. - person Nate; 11.11.2014

/ не връща грешка при деление на нула, ако или числото, което трябва да се раздели, или знаменателят е число с плаваща единица.

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

По-общо, под 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
person sawa    schedule 03.04.2011

person    schedule
comment
няма ли троичният оператор да е по-идиоматичен тук? - person tokland; 03.04.2011
comment
Да, двойният въпросителен знак не е визуално хубав (и винаги забравяте да напишете втория!). Но предполагам, че накрая просто свикваш. - person tokland; 03.04.2011