Friday, February 29, 2008

Counting Records in Ruby on Rails

I was looking through our RoR code and I came across

  def estimate_remaining
    documents.inject(0.0) {sum, d sum + d.estimate_remaining}
  end
It turns out that this can be changed to
  def estimate_remaining
    documents.to_a.sum(&:estimate_remaining)
  end
thanks to some extensions to Enumerable in Active Support. I find this much easier to read; the former requires you to know some serious Ruby, though the latter takes advantage of a symbol trick that I don't really understand (&:estimate_remaining). (Thanks to this blog for the .to_a trick)

I was hoping to do something similar to another method,

  def documents_completed
    i = 0; documents.each {d i += 1 if d.completed}; i #.nitems
  end
which counts the number of documents under the parent project that are completed. A quick look in the Rails API documentation shows that sum() is the only aggregate method they added to Enumerable. So I decided to add count() myself. I shamelessly borrowed the sum() code and modified it to look like so:
module Enumerable

  # Calculates a count by evaluating elements. Examples:
  #
  #  payments.count { p p.price > 50 }
  #  payments.count(&:overdue)
  #
  # This is instead of payments.inject { sum, p p.overdue ? sum + 1 : sum  }
  #
  # The default identity (sum of an empty list) is zero.
  # However, you can override this default:
  #
  # [].count(Payment.new(0)) { p p.price > 50 } # => Payment.new(0)
  #
  def count(identity = 0, &block)
    return identity unless size > 0

    if block_given?
      map(&block).count
    else
      inject(0) { sum, element element ? sum + 1 : sum }
      #i = 0; map { element i += 1 if element}; i #This may be faster
    end
  end


end
I saved this in [PathToRailsApp]/config/initializers/count.rb (Rails 2 environment). Now my code to count completed documents looks like this:
  def documents_completed
    documents.to_a.count(&:completed)
  end

Much cleaner! Obviously, this method is only useful if you need to count records that satisfy some sort of criteria (otherwise .length would suffice). Also, this might be more efficient done via SQL, but in my case Document.completed is a Ruby method (or "virtual column"), not an SQL column.

1 comments:

Red M@ said...

Comment from Wordpress

Rabbit:

Hi. The symbol trick you mentioned is explained pretty well here:

http://blog.hasmanythrough.com/2006/3/7/symbol-to-proc-shorthand