Creating a dynamic method at runtime in Ruby

I've had an issue with a project I'm working on, where I am creating a nested hash of records to allow quick lookup by certain fields, the reason for this is as follows. Say for example I have 100,000 despatch lines, and 100,000 self bill lines, and I have a process which will loop through each despatch line, and find the corresponding self bill line. I could easily do this with a simple find:

SelfBillLine.find(:first, :conditions => ["manifest_number = ? and order_number = ? and part_number = ?", despatch.manifest_number, despatch.order_number, despatch.part_number])

But this means running 100,000 SQL queries, and experience tells me this will be sloooowww.

I could also just get a list of all the self bill lines, and then use Ruby's select method to find the one I want, but this will be quite demanding on Ruby. So my solution was as follows, create a nested hash:

h = {}
self_bill_lines.each do |line|
  h[line.manifest_number] ||= {}
  h[line.manifest_number][line.order_number] ||= {}
  h[line.manifest_number][line.order_number][line.part_number] ||= line
end

Then I could find a matching line by doing:

matching_line = h[despatch.manifest_number][despatch.order_number][despatch.part_number]

Bonzer, works a treat. Now my next problem was that I want this to be easily re-usable in a new project, that may have different fields that are used to match with. Ideally I wanted to be able to define the fields used in an array, ie. ["manifest_number","order_number","part_number"]. Then maybe for the next project, it could be ["manifest_number","invoice_number","part_number","price"].

So I came up with this, I need to create a method (I'll call it hashify) at runtime, based on the contents of that array. Here's the resulting code:

f = Customize::matching_fields
str = "
   def hashify(lines)
   h = {}
   lines.each do |line|
     h[line.#{f[0]}] ||= {}
"
if f.length > 1
   1.upto(f.length - 1) do |i|
     str_line = " h[line.#{f[0]}]"
     1.upto(i) do |j|
       str_line = str_line + "[line.#{f[j]}]"
     end
     if i == f.length-1
       str = str + str_line + " ||= line"
     else
       str = str + str_line + " ||= {}\n"
     end
   end
end

str = str + "
  end
  h
end
"
eval(str)

This basically creates a string of the same method as before, based on whats in the matching_fields array, so if I change the array to ["manifest_number","invoice_number","part_number","price"], it produces:

def hashify(lines)
h = {}
lines.each do |line|
h[line.manifest_number] ||= {}
h[line.manifest_number][line.invoice_number] ||= {}
h[line.manifest_number][line.invoice_number][line.part_number] ||= {}
h[line.manifest_number][line.invoice_number][line.part_number][line.price] ||= line
end
h
end

Then the eval method basically executes the code in the string, and the hashify method is born. And by golly it works, and is being used in my app, I love Ruby!

<!--break-->

dfitzgibbon on January 4, 2007 - 2:21pm. read more | dfitzgibbon's blog | comment?

Reply