0

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 
jamesvphan
  • 1,825
  • 6
  • 23
  • 30
  • This is a classic case for `inject` – user229044 Jan 06 '17 at 16:20
  • 4
    What is `avocado` in the above code? It looks like a variable or method name (rather than, say, a Symbol or String key). If so, what does it return? Keep in mind that a Hash can only have one instance of each key, so the code `{ avocado: "foo", avocado: "bar" }` (in which `:avocado` is a duplicated Symbol key) would simply return `{ avocado: "bar" }`. Before you solve your stated problem, you're going to need to rethink your data structure. – Jordan Running Jan 06 '17 at 16:27
  • “I've also tried using the `#each_with_object` method”—this is a lie. You could not try anything with the input you’ve provided: it’s not a valid ruby object. – Aleksei Matiushkin Jan 06 '17 at 16:53
  • 1
    Please include realistic test data. The code snippets you've provided now are *not* valid Ruby code, and the equivalent hashes would also not be usable, as hashes cannot have duplicate keys. – user229044 Jan 06 '17 at 18:43
  • My mistake, I've updated my code snippets to be representative of what I was working with. Original variable was an array, which is why there are duplicates and I'm returning a hash. And my keys are strings – jamesvphan Jan 06 '17 at 19:46
  • @user3059274 A hash cannot have duplicate keys, so you can't just change the array into a hash like that. This question might be better if you show the input data as the arrays you have in hand. – Wayne Conrad Jan 06 '17 at 21:13

2 Answers2

0

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.

Graham
  • 7,431
  • 18
  • 59
  • 84
phss
  • 1,012
  • 10
  • 22
0

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:

rewritten
  • 16,280
  • 2
  • 47
  • 50