Reputation: 1264
Lets say I have something like this:
class FruitCount
attr_accessor :name, :count
def initialize(name, count)
@name = name
@count = count
end
end
obj1 = FruitCount.new('Apple', 32)
obj2 = FruitCount.new('Orange', 5)
obj3 = FruitCount.new('Orange', 3)
obj4 = FruitCount.new('Kiwi', 15)
obj5 = FruitCount.new('Kiwi', 1)
fruit_counts = [obj1, obj2, obj3, obj4, obj5]
Now what I need, is a function build_fruit_summary
which due to a given fruit_counts
array, it returns the following summary:
fruits_summary = {
fruits: [
{
name: 'Apple',
count: 32
},
{
name: 'Orange',
count: 8
},
{
name: 'Kiwi',
count: 16
}
],
total: {
name: 'AllFruits',
count: 56
}
}
I just cannot figure out the best way to do the aggregations.
Edit:
In my example I have more than one count.
class FruitCount
attr_accessor :name, :count1, :count2
def initialize(name, count1, count2)
@name = name
@count1 = count1
@count2 = count2
end
end
Upvotes: 1
Views: 3099
Reputation: 13531
Ruby's Enumerable is your friend, particularly each_with_object which is a form of reduce.
You first need the fruits
value:
fruits = fruit_counts.each_with_object([]) do |fruit, list|
aggregate = list.detect { |f| f[:name] == fruit.name }
if aggregate.nil?
aggregate = { name: fruit.name, count: 0 }
list << aggregate
end
aggregate[:count] += fruit.count
aggregate[:count2] += fruit.count2
end
UPDATE: added multiple counts within the single fruity loop.
The above will serialize each fruit object - maintaining a count for each fruit - into a hash and aggregate them into an empty list
array, and assign the aggregate array to the fruits
variable.
Now, get the total value:
total = { name: 'AllFruits', count: fruit_counts.map { |f| f.count + f.count2 }.reduce(:+) }
UPDATE: total taking into account multiple count attributes within a single loop.
The above map
s the fruit_counts
array, plucking each object's count
attribute, resulting in an array of integers. Then, reduce
is getting the sum of the array's integers.
Now put it all together into the summary:
fruits_summary = { fruits: fruits, total: total }
You can formalize this in an OOP style by introducing a FruitCollection
object that uses the Enumerable
module:
class FruitCollection
include Enumerable
def initialize(fruits)
@fruits = fruits
end
def summary
{ fruits: fruit_counts, total: total }
end
def each(&block)
@fruits.each &block
end
def fruit_counts
each_with_object([]) do |fruit, list|
aggregate = list.detect { |f| f[:name] == fruit.name }
if aggregate.nil?
aggregate = { name: fruit.name, count: 0 }
list << aggregate
end
aggregate[:count] += fruit.count
aggregate[:count2] += fruit.count2
end
end
def total
{ name: 'AllFruits', count: map { |f| f.count + f.count2 }.reduce(:+) }
end
end
Now pass your fruit_count
array into that object:
fruit_collection = FruitCollection.new fruit_counts
fruits_summary = fruit_collection.summary
The reason the above works is by overriding the each
method which Enumerable
uses under the hood for every enumerable
method. This means we can call each_with_object
, reduce
, and map
(among others listed in the enumerable docs above) and it will iterate over the fruits
since we told it to in the above each
method.
Here's an article on Enumerable.
UPDATE: your multiple counts can be easily added by adding a total attribute to your fruit object:
class FruitCount
attr_accessor :name, :count1, :count2
def initialize(name, count1, count2)
@name = name
@count1 = count1
@count2 = count2
end
def total
@count1 + @count2
end
end
Then just use fruit.total
whenever you need to aggregate the totals:
fruit_counts.map(&:total).reduce(:+)
Upvotes: 2
Reputation: 110685
counts = fruit_counts.each_with_object(Hash.new(0)) {|obj, h| h[obj.name] += obj.count}
#=> {"Apple"=>32, "Orange"=>8, "Kiwi"=>16}
fruits_summary =
{ fruits: counts.map { |name, count| { name: name, count: count } },
total: { name: 'AllFruits', count: counts.values.reduce(:+) }
}
#=> {:fruits=>[
# {:name=>"Apple", :count=>32},
# {:name=>"Orange", :count=> 8},
# {:name=>"Kiwi", :count=>16}],
# :total=>
# {:name=>"AllFruits", :count=>56}
# }
Upvotes: 0
Reputation: 1618
fruits_summary = {
fruits: fruit_counts
.group_by { |f| f.name }
.map do |fruit_name, objects|
{
name: fruit_name,
count: objects.map(&:count).reduce(:+)
}
end,
total: {
name: 'AllFruits',
count: fruit_counts.map(&:count).reduce(:+)
}
}
Not very efficient way, though :)
UPD: fixed keys in fruits
collection
Or slightly better version:
fruits_summary = {
fuits: fruit_counts
.reduce({}) { |acc, fruit| acc[fruit.name] = acc.fetch(fruit.name, 0) + fruit.count; acc }
.map { |name, count| {name: name, count: count} },
total: {
name: 'AllFruits',
count: fruit_counts.map(&:count).reduce(:+)
}
}
Upvotes: 0