escargot agile
escargot agile

Reputation: 22379

Aggregation on a ruby enumerable

I have an array of Salary objects, such as:

Person 1, Job 1, 400
Person 1, Job 1, 300
Person 1, Job 1, 300
Person 1, Job 2, 500

I would like to aggregate them and sum the numbers, so that the result would be:

Person 1, Job 1, 1000
Person 1, Job 2, 500

How should I go about it?

Upvotes: 1

Views: 135

Answers (2)

Cary Swoveland
Cary Swoveland

Reputation: 110685

Your array contains instances of a class. Given that class, @Rafa has answered your question. You probably have control over the class, but suppose you didn't? Suppose you were passed that array and someone else maintained the class. I'm not sure if that is good practice, but that is the situation I'd like to address.

Further, suppose you just want to determine the total salary for each :person/:job pair (and not create instances of the class with instance variables having the desired values). Your array might look like this:

p salaries
  #=> #<Salary:0x000001010cb230 @person=1, @job=1, @salary=400>,
  #=> #<Salary:0x000001010cb1e0 @person=1, @job=1, @salary=300>,
  #=> #<Salary:0x000001010cb190 @person=1, @job=1, @salary=300>,
  #=> #<Salary:0x000001010cb140 @person=1, @job=2, @salary=500>]

It is evident that the class Salary has the three instance variables needed. (I generated this using @Rafa's class definition, but without attr_reader and inspect.)

Salaries.instance_methods(false) #=> []

tells us that there are no getters for these instance variables. We can retrieve their values, however, with Object#instance_variable_get. It is then a simple matter to aggregate salaries by person/job pair:

def aggregate_salaries(salaries)
  salaries
    .group_by { |e| [e.instance_variable_get(:@person),
      e.instance_variable_get(:@job)] }
    .map do |(person,job),ae|
      tot_sal = ae.reduce(0) { |t,e| t + e.instance_variable_get(:@salary) }
      {person: person, job: job, salary: tot_sal}
    end
end

aggregate_salaries(salaries)
  #=> [{:person=>1, :job=>1, :salary=>1000},
  #    {:person=>1, :job=>2, :salary=>500}]

Upvotes: 1

Rafa Paez
Rafa Paez

Reputation: 4860

class Salary
  attr_reader :person, :job, :salary

  def initialize(args={})
    @person = args[:person]
    @job = args[:job]
    @salary = args[:salary] 
  end

  def inspect
    "<Person #{person}, Job #{job}, #{salary}>"
  end
end


salaries = [
  Salary.new({:person => 1, :job => 1, :salary => 400}),
  Salary.new({:person => 1, :job => 1, :salary => 300}),
  Salary.new({:person => 1, :job => 1, :salary => 300}),
  Salary.new({:person => 1, :job => 2, :salary => 500})]
 # => [<Person 1, Job 1, 400>, <Person 1, Job 1, 300>, 
 #     <Person 1, Job 1, 300>, <Person 1, Job 2, 500>] 


grouped_salaries = salaries.group_by do |salary| 
  [salary.person, salary.job]
end.map do |key, value| 
  Salary.new(person: key[0], 
             job: key[1], 
             salary: value.reduce(0) { |acc, s| acc + s.salary })
end
# => [<Person 1, Job 1, 1000>, <Person 1, Job 2, 500>] 

Upvotes: 2

Related Questions