Given a hash of items (also a hash), return a hash that consolidates duplicates and provide counts for each item

338 views Asked by At

I'm working to better understand hashes, and I've come across problems in which I have a collection with duplicate items and I need to return a hash of those items de-duped while adding a key that counts those items. For example...

I have a hash of grocery items and each item points to another hash that describes various attributes of each item.

groceries = [
    {"avocado" => {:price => 3.0, :on_sale => true}},
    {"tomato" => {:price => 1.0, :on_sale => false}},
    {"avocado" => {:price => 3.0, :on_sale => true}},
    {"kale" => {:price => 5.0, :on_sale => false}}
]

And I want my updated groceries to be...

groceries_updated = {
    "avocado" => {:price => 3.0, :on_sale => true, :count => 2},
    "tomato" => {:price => 1.0, :on_sale => false, :count => 1},
    "kale" => {:price => 5.0, :on_sale => false, :count => 1}
}

My initial approach was first create my new hash by iterating through the original hash so I would have something like this. Then I would iterate through the original hash again and increase the counter in my new hash. I'm wondering if this can be done in one iteration of the hash. I've also tried using the #each_with_object method, but I also need a better understanding of the parameters. My attempt with #each_with_object results in an array of hashes with the :count key added, but no consolidation.

def consolidate_cart(array)
  array.each do |hash|
    hash.each_with_object(Hash.new {|h,k| h[k] = {price: nil, clearance: nil, count: 0}}) do |(item, info), obj|
      puts "#{item} -- #{info}"
      puts "#{obj[item][:count] += 1}"
      puts "#{obj}"
    end 
  end
end 
2

There are 2 answers

0
phss On

You can use inject to build the consolidated groceries in the following way:

groceries = [
    {"avocado" => {:price => 3.0, :on_sale => true}},
    {"tomato" => {:price => 1.0, :on_sale => false}},
    {"avocado" => {:price => 3.0, :on_sale => true}},
    {"kale" => {:price => 5.0, :on_sale => false}}
]

groceries_updated = groceries.inject({}) do |consolidated, grocery|
  item = grocery.keys.first
  consolidated[item] ||= grocery[item].merge(count: 0)
  consolidated[item][:count] += 1
  consolidated
end

inject takes the initial state of the object you want to build (in this case a {}) and a block that will be called for each element of the array. The purpose of the block is to modify/populate the object. A good description on how to use inject can be found here.

In your case, the block will either add a new item to a hash or increment its count if it already exists. The code above will add a new item to the hash, with a count of 0, only if it doesn't exist (that's what ||= do). Then it will increment the count.

One thing to note is that the values in the original groceries array might be different (for instance, one avocado entry might have a price of 3.0, and another a price of 3.5). The values in groceries_updated will contain whichever was first in the original array.

0
rewritten On

One-liner (almost, just split in some chained blocks):

groceries.
  group_by { |h| h.keys.first }.
  transform_values { |v| v.first.values.first.merge(count: v.size) }

Explanation:

First block splits the list into several values based on the (supposed single) hash key:

{
  "avocado" => [
    {"avocado" => {:price => 3.0, :on_sale => true}},
    {"avocado" => {:price => 3.0, :on_sale => true}}
  ],
  "tomato" => [
    {"tomato" => {:price => 1.0, :on_sale => false}}
  ], ...
}

The second block takes one value

[
  {"avocado" => {:price => 3.0, :on_sale => true}},
  {"avocado" => {:price => 3.0, :on_sale => true}}
]

reads the supposedly single value of the first item, and merges with the total count.

:tada: