Friday, April 11, 2008

Advanced sum() usage in Rails

Active Support adds a nice sum() method to Enumerable, which is mixed into Array. Rails also has a nice extension to Ruby symbols that lets you call a method that normally takes a block like this:

total_price = items.sum(&:price)

You may be thinking that this only works for numbers. However, because Ruby is dynamically typed, we can actually take the sum of any type that implements the + method. (In Ruby, even mathematical operators are methods, just as numbers are objects) That means we can use it to concatenate arrays (array.+ is concatenation). This can come in handy when working with multiple Active Record has_many relationships:

firm_invoices = @firm.clients.to_a.sum(&:invoices)

To be fair, this probably isn't the most efficient way to do this. The example above is equivalent to the following usage of has_many :through from the Rails API:

@firm.clients.collect { |c| c.invoices }.flatten
@firm.invoices # defined by has_many :through the Client join model

Sometimes life isn't as easy as examples. In my case, we have customers that can belong to multiple offices (they are mobile and do work from each). So a customer has_and_belongs_to_many (habtm) offices (in SQL terms, this is a many-to-many relationship via a join table). In our Rails project, the join table does not have a model object, as it contains only the data to define the relationship. An office, in turn, has_many projects (our main unit of work; a single request from a customer). My goal was to report all of the projects for the offices of the logged-in customer. With sum(), it's easy:

projects = @customer.offices.to_a.sum(&:active_projects)

One thing I don't like about this is that it is hard to read. Summing projects doesn't make verbal sense. On the other hand

projects = @customer.office_active_projects

smells funny. Why should Customer have specific knowledge of how to get Projects for Offices?

In the end, summing arrays is a useful method to have in your tool belt. It can be used to quickly drill through relationships (without altering model objects) to make sure you get the results you are looking for, before making your code more efficient/elegant/permanent.