views:

88

answers:

4

I posted a similar question to this not too long ago in regards to formatting a MySQL query using a block and got very good responses, but they were very specific to the problem at hand. This time around, I'm dealing with getting the .sum()s of rows in a table. Here's what I've got now:

def balance
  balance = 0
  items.each do |item|
    balance = balance + item.charges.sum(:revenue, :conditions => ['created_at >= ?', Time.now.beginning_of_month])
  end
  balance
end

My goal here is to get the total of all charges for this month for a given user. Charges belong to items, which belong users. I'm sure there's a better way to do this in Ruby/Rails.

What would you do?

+5  A: 

A straight conversion can be done:

def balance
  conds = ["created_at > ?", Time.now.beginning_of_month]
  items.inject(0) do |total, item|
    total + item.charges.sum(:revenue, :conditions => conds)
  end
end

There might be more optimal approaches depending upon how your relationships are mapped out. For instance, you may be able to do something like:

def balance
  Charge.sum :revenue,
    :conditions => ["charges.item_id IN (?) AND created_at > ?",
      items.map { |item| item.id },
      Time.now.beginning_of_month]
end

For these kinds of situations, map, inject, select, and so forth, are invaluable tools. Here's a lengthy discussion on inject, and definitely consult the RDoc's for the Enumerable module for more information.

Ian
Might be more clear if you renamed the variable `acc` in your first example to `sum`.
Sarah Vessels
Also, could store `Time.now.beginning_of_month` in a variable outside the `inject` block so it doesn't get recalculated in each iteration.
Sarah Vessels
Your second solution worked wonderfully
Bloudermilk
@Sarah, `sum` probably would be clearer, `acc` is just the variable name I instinctively go for when using `inject`, and nice catch on the `Time.now.beginning_of_month` re-evaluation. It does add unnecessary cost, and in very rare circumstances could result in the wrong sum being calculated.
Ian
+2  A: 

There's no reason not to do this in the SQL query itself, e.g:

Charges.sum :revenue, :conditions => [ "created_at >= ?, items.user_id = ?",
                                        Time.now.beginning_of_month, some_user_id ],
                      :joins => :items

Edit: It's unclear from the docs whether sum will take a symbol for :joins like find does. If it doesn't, your :joins line should look like this instead:

:joins => "JOIN items ON charges.item_id = items.id"
Jordan
Jordan, unfortunately the user_id is not stored in the charges table, just the items table.
Bloudermilk
Bloudermilk: That's what the JOIN is for (I initially made it more complicated than it needed to be; I've fixed it). If charges belongs_to items and items belongs_to users you can do a JOIN with the items table to match against user_id. That's the whole point of joins, and you can do the entire calculation in SQL.
Jordan
+1  A: 

Just a couple of general comments:

  • I would suggest putting your code in the model rather than a controller, view, or helper, following the Skinny Controller, Fat Model idea.
  • Instead of writing balance = balance + item.charges.sum(:revenue, :conditions => ['created_at >= ?', Time.now.beginning_of_month]) you can use += and do: balance += item.charges.sum(:revenue, :conditions => ['created_at >= ?', Time.now.beginning_of_month]).
  • It's not technically a problem, but it seems bad form to have a variable the same name as your method (i.e., 'balance').
  • I would store Time.now.beginning_of_month in a variable outside your loop so it doesn't get recalculated every time.
Sarah Vessels
Thanks Sarah, that method is from the model. I didn't know Ruby supported +=! Good idea about storing Time.now.beginning_of_month outside the loop
Bloudermilk
+1  A: 

This is a very common recursion pattern. It is called a catamorphsim in category theory, a fold in mathematics and functional programming, it is also sometimes called reduce and in Smalltalk it is called inject:into:. In Ruby, it is called inject or reduce (these two methods are aliases).

The idea is that you have a collection of values and you want to "reduce" or "fold" that collection of multiple values into a single value. (The Smalltalk name inject:into: comes from the fact that you inject a starting value into a block which is called for each element of the collection.)

def balance
  this_month = Time.now.beginning_of_month
  items.reduce(0) { |balance, item|
    balance + item.charges.sum(:revenue, :conditions => ['created_at >= ?', this_month])
  }
end
Jörg W Mittag
Might follow Ruby convention of using `do` and `end` for a block you have on multiple lines.
Sarah Vessels
There's two camps on this: one camp uses `do` / `end` for multiline and `{` / `}` for single line. The other uses `do` / `end` for imperative and `{` / `}` for functional style blocks. I belong to the latter camp.
Jörg W Mittag