Reputation: 87
How can I define custom getter functions for ActiveRecord objects in Ruby that will perform operations on an array of the ActiveRecord objects?
For example, I would like to return weighted averages on an array of objects. So if I have Loan objects (1,2,3) with fields amount (100, 200, 300) and default_rate (.1, .2, .3), then with the normal ActiveRecord functions Loan.find(1).amount should return 100, Loan.find(2).default_rate should return .2.
But if I had Loan.find(2,3).default_rate, I would like it to return the weighted average of the default rates, which is .26. I know I can do this using SQL select statements but how can I "overload" the ActiveRecord getter to allow be to define a function when I call the getter on an array of Loan objects rather than a single Loan object?
The loan table has fields amount id, amount, and default_rate
class Loan < ActiveRecord::Base
module Collection
def default_rate
sum(:default_rate * :amount) / sum(:amount)
end
end
end
class LoanGroup
has_many :loans, :extend => Loan::Collection
end
#Then I try
obj = LoanGroup.where('id < 10')
This gives me the error that has_many is undefined in LoanGroup
Upvotes: 4
Views: 610
Reputation: 34072
To avoid polluting Array
's namespace with methods specific to collections of Loan
records, you might make a wrapper for such a collection and call your methods on that.
Something like:
class LoanArray < Array
def initialize(*args)
find(*args)
end
def find(*args)
replace Array.wrap(Loan.find(*args))
self
end
def default_rate
if length == 1
self[0].default_rate
else
inject(0) {|mem,loan| mem + loan.defualt_rate } / count
end
end
end
# then
arr = LoanArray.new(2,3).default_rate #=> 0.26
arr.find(1,2,3).default_rate #=> 0.2
arr.length #=> 3
arr.find(1) #=> [<Loan id=1>]
arr.default_rate #=> 0.1
arr.length #=> 1
Original answer below: using an association extension
Use an association extension. This way the method will be the same whether on a loan, or a collection of loans.
class MyClass
has_many :loans do
def default_rate
sum(:default_rate) / count
end
end
end
obj = MyClass.find(1)
obj.loans.first.default_rate #=> 0.1
obj.loans.default_rate #=> 0.2
obj.loans.where(:id => [2,3]).default_rate #=> 0.26
If you wanted to keep the logic for loans in the Loan
class, you could also write the extension there, e.g.:
class Loan
module Collection
def default_rate
sum(:default_rate) / count
end
# plus other methods, as needed, e.g.
def average_amount
sum(:amount) / count
end
end
end
class MyClass
has_many :loans, :extend => Loan::Collection
end
Edit: as @Santosh points out association.find
does not return a relation, so it will not work here. You'd have to use where
or some other method which returns a relation.
Upvotes: 2
Reputation: 1261
If I understood your question correctly, you can write class method for this.
class Loan
def self.default_rate
sum = 0
all.each do |loan|
sum += loan.default_rate
end
sum / all.count
end
end
Then
Loan.where(:id => [2, 3]).default_rate
Other solution if you want to use Loan.find(2,3) then you need to override the Array class.
class Array
def default_rate
sum = 0
self.each do |loan|
sum += loan.default_rate
end
sum / self.count
end
end
Loan.find(2, 3).default_rate
Upvotes: 1
Reputation: 2483
If you accept introducing a named scope, than you can exploit the fact scopes have the methods of the classes they are defined on. This way, having
class Shirt < ActiveRecord::Base
scope :red, where(:color => 'red')
def self.sum
# consider #all as the records you are processing - the scope will
# make it return neccessary ones only
all.map(&:id).sum
end
end
Shirt.red.sum
will yield you a sum id red shirts' ids (as opposed to Shirt.sum will yield a sum of all ids).
Having your BL layed out this way not only makes it cleaner, but also enables it to be re-used (you can invoke the #sum method on any scope, no matter how complex it is and you will still get valid results). In addition to that, it appears to be easily readable and doesn't involve any magic (well, almost :)
Hope this would clean you code up a bit :)
Upvotes: 0
Reputation: 8657
You might have a bit more freedom by not doing this as a class method; you could have a method header of default_rate (*args)
to allow multiple arguments to be passed in using the splat operator, then check the args
length. It can be done quite nicely recursively:
def default_rate(*args)
sum = 0
if args.length == 1
return array_name[arg-1] # guessing that's how you store the actual var
else
args.each do |arg|
sum += default_rate arg
end
end
return sum/args.length
end
Upvotes: 0