Tuesday, 28 October 2008

Enumerable.average

Was looking around for a quickie function to do an average on an Enumerable/Array just like max and min - and found there isn't one. So here's a quick-n-dirty one that you can drop into config/environment.rb. Examples for use in the comments:

module Enumerable
  # returns a simple average of each item
  # If a block is passed - it will try to perform the operation on the item.
  # if the result is nil - it won't be counted towards the average.
  # Egs: 
  # [1,2,3].average == 2
  # ["a","ab","abc"].average &:length == 2
  # @purchases.average &:item_price == 10.45
  def average(&block)
    the_sum = 0
    total_count = 0
    self.each do |item|
      # either the actual item, or a method called on the item
      next_value = block_given? ? yield(item) : item
      unless next_value.nil?
        the_sum += next_value
        total_count += 1
      end
    end
    return nil unless total_count > 0
    the_sum / total_count
  end
end

I used it for displaying neat averages in the view thus:

    <p>Averages:
    <br />Number of items: <%= @purchases.average &:num_items -%>
    <br />Unit price: <%= @purchases.average &:price -%>
    <br />Total price: <%= @purchases.average {|p| p.num_items * p.price } -%> </p>

5 comments:

peter said...

Removed the temporary variables - the last line is a bit more golfed than I'd ideally imagine, though...

http://pastie.org/private/okipyyrpy4xuz9tcuwsg

Taryn said...

Cool! That's a nice refactor. I hadn't bothered to refactor this one for brevity yet.

Sometimes I wonder if it's beneficial to have the long-winded version on an explanatory blogpost - but most people that read this stuff know what they're doing... so yeah, yours is better ;)

Taryn said...

Hmm - on second thoughts - this code compacts before sending the items through the block-code (which may yield a nil)... which was why I did the whole bodgy thing with the total_count to start with.

If we fix this by returning an array of the items then compacting/summing - we'd be able to get the true "total_count" but we'd have potential memory issues... hmmm - guess it depends on what this will be used for.

Any ideas?

peter said...

Well... first we'd have to decide on the semantics of total_count - I thought it's the # of elements in the original array, it didn't even occur to me that some nils can pop up after yield()ing...

Taryn said...

Ya - I guess one of the examples I didn't show was something along the lines of:

Widget.find(:all).average &:price

Which is essentially what led to this implementation. In this instance there might not be a price specified - so it'd be nice if it didn't use that in generating the average.