简体   繁体   中英

How to convert array of hashes into a single hash and count repeated items?

I have an array of hashes, that I basically want to merge and convert to a single hash and at the same time I want to count the number of times a key:value pair occurs.

The original array is

cart_items = [
  {"AVOCADO" => {:price => 3.0, :clearance => true }},
  {"AVOCADO" => {:price => 3.0, :clearance => true }},
  {"KALE"    => {:price => 3.0, :clearance => false}}
]

I have tried this however I am not getting what I want. My attempt at this is below, if anyone could explain where I am going wrong, that would be great.

My attempt at this problem is this.

def consolidate_cart(items)
  ### the cart starts as an array of items
  ## convert the array into a hash`

 hashed_items = items.inject(:merge!)

 hashed_items.map{|k,v| {k => v, :count => v.length}}

end

consolidate_cart(cart_items)

I expect the output to be

{
  "AVOCADO" => {:price => 3.0, :clearance => true, :count => 2},
  "KALE"    => {:price => 3.0, :clearance => false, :count => 1}
}

But I get an output of

[{"AVOCADO"=>{:price=>3.0, :clearance=>true}, :count=>2}, {"KALE"=>{:price=>3.0, :clearance=>false}, :count=>2}]

You can merge to v (within the map call) the value of count ( v.merge(:count => v.length) ), so this will add the count key to the v hash, you'll get something like:

[
  {"AVOCADO"=>{:price=>3.0, :clearance=>true, :count=>2},
  {"KALE"=>{:price=>3.0, :clearance=>false, :count=>2}
]

But anyways the values for :count are going to be wrong.

In the other hand, you can get all the keys from each hash in cart_items, merge the hashes, and then merge a new key with the count of that key in the stored keys array:

def consolidate_cart(items)
  items_keys = items.flat_map(&:keys)
  items.inject(:merge).map do |key, value|
    { key => value.merge(count: items_keys.count(key)) }
  end
end

p consolidate_cart(cart_items)
# [{"AVOCADO"=>{:price=>3.0, :clearance=>true, :count=>2}}, {"KALE"=>{:price=>3.0, :clearance=>false, :count=>1}}]

A part by part view of the method functioning:

You map the keys of each hash item ( items.flat_map(&:keys) ):

["AVOCADO", "AVOCADO", "KALE"]

You merge the hash within items ( items.inject(:merge) ):

{"AVOCADO"=>{:price=>3.0, :clearance=>true}, "KALE"=>{:price=>3.0, :clearance=>false}}

When you iterate over the previous generated hash, you merge to each hash value the count key ( { key => value.merge(count: items_keys.count(key)) } ):

# {:price=>3.0, :clearance=>true}
# {:count=>2}
# => {:price=>3.0, :clearance=>true, :count => 2}

I've already seen my answer doesn't correspond with the expected output. This does:

def consolidate_cart(items)
  items.inject(:merge).each_with_object(items: items.flat_map(&:keys)) do |(k, v), hash|
    hash[k] = v.merge(count: hash[:items].count(k))
  end.reject { |k, _| k == :items }
end

I would like to suggest a way to consider also the case where price or clearance of the same product ( String ) can be different (since you are not dealing with database ids):

cart_items = [
  {"AVOCADO" => {:price => 3.0, :clearance => true }},
  {"AVOCADO" => {:price => 4.0, :clearance => false }},
  {"AVOCADO" => {:price => 3.0, :clearance => true }},
  {"AVOCADO" => {:price => 4.0, :clearance => true }},
  {"KALE"    => {:price => 3.0, :clearance => false}},
  {"AVOCADO" => {:price => 4.0, :clearance => true }},
  {"AVOCADO" => {:price => 4.0, :clearance => true }}
]

In this case this is a possible way to consolidate:

cart_items.map{ |h| h.values.first.merge(product: h.keys.first) }
  .group_by(&:itself)
  .transform_values { |v| v.first.merge(count: v.size)}.values

It is returning:

#=> [{:price=>3.0, :clearance=>true, :product=>"AVOCADO", :count=>2}, {:price=>4.0, :clearance=>false, :product=>"AVOCADO", :count=>1}, {:price=>4.0, :clearance=>true, :product=>"AVOCADO", :count=>3}, {:price=>3.0, :clearance=>false, :product=>"KALE", :count=>1}]

You can always append .group_by{ |h| h[:product] } .group_by{ |h| h[:product] } to get

#=> {"AVOCADO"=>[{:price=>3.0, :clearance=>true, :product=>"AVOCADO", :count=>2}, {:price=>4.0, :clearance=>false, :product=>"AVOCADO", :count=>1}, {:price=>4.0, :clearance=>true, :product=>"AVOCADO", :count=>3}], "KALE"=>[{:price=>3.0, :clearance=>false, :product=>"KALE", :count=>1}]}

Or for the cart in your post:

#=> {"AVOCADO"=>[{:price=>3.0, :clearance=>true, :product=>"AVOCADO", :count=>2}], "KALE"=>[{:price=>3.0, :clearance=>false, :product=>"KALE", :count=>1}]}

Not exactly the same output as required, but maybe this can be useful. Or not.

cart_items.each_with_object(Hash.new(0)) { |g,h| h[g] += 1 }.
  map { |g,cnt| { g.keys.first=>g.values.first.merge(count: cnt) } }

  #=> [{"AVOCADO"=>{:price=>3.0, :clearance=>true, :count=>2}},
  #    {"KALE"=>{:price=>3.0, :clearance=>false, :count=>1}}]           

Hash.new(0) is sometimes called a counting hash . See the form of Hash::new that takes an argument equal to the hash's default value . We obtain:

cart_items.each_with_object(Hash.new(0)) { |g,h| h[g] += 1 }
  #=> {{"AVOCADO"=>{:price=>3.0, :clearance=>true}}=>2,
  #    {"KALE"=>{:price=>3.0, :clearance=>false}}=>1} 
cart_items.group_by(&:itself).map{ |item, group| item[item.keys.first][:count] = group.size; item} 

对于演示https://rextester.com/MFH44079

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM