0

Need to achieve the given output based on the following hash value

foos = [ { :key => 'Foo', :value => 1, :revenue => 2 },
         { :key => 'Foo', :value => 1, :revenue => 4 },
         { :key => 'Bar', :value => 2, :revenue => 7 },
         { :key => 'bar', :value => 2, :revenue => 9 },
         { :key => 'Zampa', :value => 4, :revenue => 9 }]

Output should be :

{ :key => 'Foo', :value => 1, :revenue => [2,4] } #Merging row 1 & 2 as they share same :key 'Foo'
{ :key => 'Bar', :value => 2, :revenue => [7,9] } #Merging row 3 & 4 as they share same :key 'Bar'
{ :key => 'Zampa', :value => 4, :revenue => 9 } 

Merging should be based on the value of the :Key field How to achieve this in ruby as I am new to ruby.

  • What happens if they have a different value for the `value` key? – Sebastián Palma Nov 19 '19 at 21:51
  • Then this will also be merged to array like revenue. But in my application only one key's value (revenue) will be different. But my approach should be generic for all the other keys value and revenue. – Avinaba GHOSH HAJRA Nov 19 '19 at 22:05
  • Does the final order matter in the collect arrays? – G4143 Nov 19 '19 at 22:10
  • No..ordering does not matter – Avinaba GHOSH HAJRA Nov 19 '19 at 22:16
  • Avinaba, I don't follow your first comment. Can the array also contain the hash `{ :key => 'Foo', :value => 2, :revenue => 6 }`? If yes, what would be your desired result? If no, your example would have been clearer if you had excluded the key `:value`. – Cary Swoveland Nov 20 '19 at 02:20
  • @Cary It can. Then the result would be `{ :key => 'Foo', :value => [1,2], :revenue => [2,4,6] }` – Avinaba GHOSH HAJRA Nov 20 '19 at 08:32
  • In that case you should have explained that in your question and modified your example to cover that case. It’s a little late to do that now, however. Note you are creating extra work for yourself later by having `:value=>1` in one case and `:value =>[2,3]` in another. It would be better for the value of `:value` to always be an array, even when it contains a single element (`:value=>[1]`). – Cary Swoveland Nov 20 '19 at 08:50
  • Please see "[ask]" and the linked pages and "[mcve](https://stackoverflow.com/help/minimal-reproducible-example)". Did you research this? Where? If it didn't help tell us why. What did you try? If you didn't try, why not? If you did, what did you do? We'd like to see your minimal attempt to solve it. – the Tin Man Nov 20 '19 at 10:02
  • Is the `:key => 'Bar'` and `:key => 'bar'` part a typo? Or do you want to compare the keys case-insensitive? – 3limin4t0r Nov 20 '19 at 14:39

3 Answers3

1

You can use group_by to group the foos array by :key. However I would first downcase the :key value, since you want 'Bar' and 'bar' to end up in the same group.

# We first need to unify the keys of the hashes before we can start
# grouping. 'Bar' != 'bar' so they would be split up in two separate
# groups. Judging from the output you don't want this.
foos.each { |foo| foo[:key].downcase! }

# Now that all keys are downcased we can group based upon the value of
# the :key key.
grouped_foos = foos.group_by { |foo| foo[:key] }

# Now we need to map over the resulting hash and create a single result
# for each group.
grouped_foos.transform_values! do |foos|
  # First I'll transform the structure of `foos`, from:
  #
  #     [{a: 1, b: 2}, {a: 3, b: 4}]
  #
  # into:
  #
  #     [[:a, 1], [:b, 2], [:a, 3], [:b, 4]]
  #
  tmp = foos.flat_map(&:to_a)

  # Then I'll group the above structure based upon the first value in
  # each array, simultaneously removing the first element. Resulting in:
  #
  #     {a: [[1], [3]], b: [[2], [4]]}
  #
  tmp = tmp.group_by(&:shift)

  # We now need to flatten the values by one level. Resulting in:
  #
  #     {a: [1, 3], b: [2, 4]}
  #
  tmp.transform_values! { |values| values.flatten(1) }

  # The next step is remove duplicate values. We currently have:
  #
  #     {key: ['foo', 'foo'], value: [1, 1], revenue: [2, 4]}
  #
  # whereas we want:
  #
  #     {key: ['foo'], value: [1], revenue: [2, 4]}
  #
  tmp.transform_values!(&:uniq)

  # Lastly if the array only contains a single value we want to use the
  # value instead of an array. Transforming the above structure into:
  #
  #     {key: 'foo', value: 1, revenue: [2, 4]}
  #
  tmp.transform_values! { |head, *tail| tail.empty? ? head : [head, *tail] }

  # Finally we need to return our new hash.
  tmp 
end

Combining the above steps we get the following result:

foos.each { |foo| foo[:key].downcase! }
grouped_foos = foos.group_by { |foo| foo[:key] }

grouped_foos.transform_values! do |foos|
  foos.flat_map(&:to_a).group_by(&:shift)
      .transform_values { |values| values.flatten(1).uniq }
      .transform_values { |head, *tail| tail.empty? ? head : [head, *tail] }
end

If you don't want to modify the (capitalisation of the) original foos structure you'll have to replace:

foos.each { |foo| foo[:key].downcase! }
# with
unified_keys = foos.map(&:dup).each { |foo| foo[:key] = foo[:key].downcase }

Then use the new unified_keys structure from that point on.

The above solution produces the following result:

grouped_foos
#=> {"foo"  =>{:key=>"foo",   :value=>1, :revenue=>[2, 4]},
#    "bar"  =>{:key=>"bar",   :value=>2, :revenue=>[7, 9]},
#    "zampa"=>{:key=>"zampa", :value=>4, :revenue=>9}}

You can get the result you want by requesting the values of the grouped_foos:

grouped_foos.values
#=> [{:key=>"foo",   :value=>1, :revenue=>[2, 4]},
#    {:key=>"bar",   :value=>2, :revenue=>[7, 9]},
#    {:key=>"zampa", :value=>4, :revenue=>9}]
3limin4t0r
  • 19,353
  • 2
  • 31
  • 52
0

You can try grouping by key and value and then map the revenue values:

foos
  .group_by { |e| e.values_at(:key, :value) }
  .map do |(key, value), values|
    { key: key, value: value, revenue: values.map { |e| e[:revenue] } }
  end
# [{:key=>"Foo", :value=>1, :revenue=>[2, 4]}, {:key=>"Bar", :value=>2, :revenue=>[7]}, {:key=>"bar", :value=>2, :revenue=>[9]}, {:key=>"Zampa", :value=>4, :revenue=>[9]}]
Sebastián Palma
  • 32,692
  • 6
  • 40
  • 59
  • Alternatively use `.group_by { |foo| foo.slice(:key, :value) }` then change `map` to `.map { |foo, foos| foo.merge revenue: foos.map { |foo| foo[:revenue] } }` – 3limin4t0r Nov 20 '19 at 01:16
  • That can be a different answer @3limin4t0r. Just updated to use `values_at` with the `slice` hint. – Sebastián Palma Nov 20 '19 at 06:44
  • But if the number of keys in hash is not limited to 3. If It is 10 or 15 or many more. But it should merge based on the value contain through :key. Then what should be the approach. @Sebastian Palma – Avinaba GHOSH HAJRA Nov 20 '19 at 08:31
0

use reduce

result=foos.group_by { |x| x[:key] }.values.map do |arr|
  arr.reduce do |h1, h2|
    h1.merge(h2) do |k, v1, v2|
      k.eql?(:revenue) ? [v1, v2] : v1
    end
  end
end

p result

[{:key=>"Foo", :value=>1, :revenue=>[2, 4]}, {:key=>"bar", :value=>2, :revenue=>[7, 9]}, {:key=>"Zampa", :value=>4, :revenue=>9}]
Rajagopalan
  • 5,465
  • 2
  • 11
  • 29