Frederick Cheung
7/20/2008 5:25:00 PM
I've gone for a fairly simple proxy based approach that doesn't do any
real converting. When converting an array of records we return a
wrapper object.
When you call (for example) name on that object you get back a proxy
object that just stores that name.
When you then call [3] on that proxy object, the proxy retrieves the
object at index 3 and calls the previously remembered method name on it.
The reverse conversion is similar. the conversion function just
returns a wrapper object, and wrapper[2] is just a proxy that
remembers that index. When you call a method on the proxy
it forwards it to the record of arrays to obtain the appropriate array
and retrieves the index element. A limitation is that the wrapper
object just understands the [] method. It doesn't for example know how
many elements it contains.
Nothing particular about OpenStruct is relied upon (eg activerecord
objects work just fine, or just any old objects for example eg,
roa_to_aor(aor_to_roa(data)) works)
My wrappers also allow assigning (the assignment gets passed through
to the original object)
usage example:
#setup some test data
require 'ostruct'
data = []
data << OpenStruct.new( :name => 'fred', :color => 'red', :age => 22)
data << OpenStruct.new( :name => "paul", :color => "blue", :age => 30)
data << OpenStruct.new( :name => 'henry', :color => 'pink', :age => 23)
data << OpenStruct.new( :name => 'claire', :color => 'orange', :age =>
19)
rec = OpenStruct.new
rec.name = %w(fred paul henry)
rec.age = [10,20,30]
rec.food = %w(pizza cake burger)
#real stuff begins here
x = aor_to_roa data
x.name[2] #=> "henry"
x.name[2] = 'sarah'
data[2] #> => #<OpenStruct name="sarah", color="pink", age=23>
y = roa_to_aor rec
y[1].age #=> 20
y[2].food = 'ice-cream'
rec.food #=> ["pizza", "cake", "ice-cream"]
#Composing:
x = roa_to_aor(aor_to_roa(data))
x[1].name #=> "paul"
x[1].name = "charlie"
data[1].name => "charlie"
y= aor_to_roa(roa_to_aor(rec))
y.name[1] #=> "paul"
y.name[1] = "alice"
rec.name #=> ["fred", "alice", "henry"]
With aor_to_roa I succumbed to a desire for cuteness, so for example
x.name #=> ["fred", "paul", "henry", "claire"]
x.name.class #=> Array (Even though it isn't)
x.name.collect {|s| s.upcase} #=> ["FRED", "PAUL", "HENRY", "CLAIRE"]
ie it can really look like an array if you want it to (but this isn't
very efficient as it's materializing the array over and over again.
this could probably be improved on). I didn't do the same for
roa_to_aor.
If one was feeling particularly enthusiastic, sticking some
pluralization rules into method missing would allow one to do (given
the above data) x.names[2] rather than x.name[2] which reads slightly
nicer
Fred
CODE:
def roa_to_aor(rec)
RecordOfArraysWrapper.new(rec)
end
def aor_to_roa(arr)
ArrayOfRecordsWrapper.new(arr)
end
class ArrayOfRecordsWrapper
def initialize(array)
@array = array
end
def method_missing(name, *args)
FieldProxy.new(@array, name)
end
class FieldProxy
instance_methods.each { |m| undef_method m unless m =~ /(^__)/ }
def initialize array, name
@array, @name = array, name
end
def [](index)
@array[index].send @name
end
def []=(index,value)
@array[index].send(@name.to_s + '=', value)
end
def to_a
@field_array = @array.collect {|a| a.send @name}
end
def method_missing(*args, &block)
to_a.send *args, &block
end
end
end
class RecordOfArraysWrapper
def initialize(record)
@record = record
end
def [](index)
RecordProxy.new(@record, index)
end
class RecordProxy
def initialize(record, index)
@record, @index = record, index
end
def method_missing(name, *args)
if name.to_s =~ /(.*)=$/
@record.send($1)[@index]=args.first
else
@record.send(name)[@index]
end
end
end
end