views:

499

answers:

5

I'm new to Rails (and ruby). What is the standard way of iterating through an array to total a variable.

e.g. for the total expenses in a month, first an Array:

expenses_this_month = expenses.find :all,
                                    :conditions => ['date >= ? and date <= ?',
                                      Date.today.beginning_of_month, Date.today.end_of_month]

I already of know of two ways of doing it:

total = 0.0
for expense in expenses_this_month
  total += expense.cost
end
return total

or with a block

total = 0.0
expenses_this_month.each do |expense|
  total += expense.cost
end
return total

I'm aware that the last line in a ruby method will be returned by default, so there must be a better way of writing this?

+2  A: 

Once you have returned the data, use the inject method:

total = expenses_this_month.inject { |total, expense| total + expense.cost }

However, you should just rewrite your query:

total = expenses.sum(:cost, :conditions => ['date >= ? and date <= ?',
                                  Date.today.beginning_of_month, Date.today.end_of_month])
Doug Neiner
Thanks for the suggestion. I not using sum, as cost is a method on the model (not a column in the DB), as it is calculated depending on the tax rate. Inject looks like it will work for me
the_snitch
+1  A: 

If you're using Rails, you can use the built-in sum class method (assuming Expense is the class name).

expenses_this_month = Expense.sum('cost', 
                                  :conditions => ['date >= ? and date <= ?',
                                                  Date.today.beginning_of_month,
                                                  Date.today.end_of_month])
kejadlen
+2  A: 

You're looking for the Enumerable#inject method:

expenses_this_month.inject(0.0) {|total, expense| total + expense }

This method (borrowed from Smalltalk) takes the value passed to it (0.0 in this case) and sets an internal variable to that. It then calls the block with the value of that variable (as total) and each successive element (as expense), and sets the variable to whatever the block returns (in this case the sum of the total and the current element).

You may want to offload this calculation to the database, though, as kejadlen suggests, by using the #sum method.

Kevin
+4  A: 

The inject method will work great, like Doug suggested. However, it's generally preferable to do things like this in the database when you can. ActiveRecord provides a simple interface for this.

total = Expenses.sum :cost, :conditions => {
  :date => (Date.today.beginning_of_month..Date.today.end_of_month)
}

Note that you can also use a Range object instead of SQL interpolation.

If you're loading all the Expense objects for another reason the inject method is, of course, fine.

stephenjudkins
+1 for showing a much cleaner way of passing the date range. Great answer!
Doug Neiner
+1  A: 
expenses_this_month.map(&:cost).sum

(shorter, although it creates an array in memory unlike reduce)

expenses_this_month.reduce(BigDecimal.new('0')) { |total, expense| total + expense.cost }

you need to remember to pass an initial value to reduce (otherwise it will return nil for empty array) and to use BigDecimal instead of regular floats when dealing with money.

psyho
I've been using Decimal type in the DB for the money amounts, but if I pass (0.0) to the method (reduce/inject), will it default to float?Also, what are the specific differences between inject and reduce?
the_snitch
reduce is an alias for inject, although I think it was introduced in ruby 1.8.7 so for backwards compatibility you might want to stick with inject. It is my personal preference to use map/reduce instead of collect/inject as this nomenclature is used across a wide variety of programming languages. If you pass a float as an initial value to reduce summing up Decimal/BigDecimal numbers then you'll be performing an operation of adding float + BigDecimal which results in a float.
psyho