kavin kavin -4 years ago 116
Ruby Question

How to modify arrays of hashes in ruby 2.1.2?

I have an array of hashes called

array_of_hash
:

array_of_hash = [
{:name=>"1", :address=>"USA", :collection=>["LAND", "WATER", "OIL", "TREE", "SAND"], :sequence=>"AB"},
{:name=>"5", :address=>"UK", :collection=>["LAND", "WATER", "OIL", "TREE", "SAND"], :sequence=>"BC"},
{:name=>"6", :address=>"CANADA", :collection=>["LAND", "WATER", "OIL", "TREE", "SAND"], :sequence=>"CD"},
{:name=>"29", :address=>"GERMANY", :collection=>["LAPTOP", "SHIP", "MOUNTAIN"], :sequence=>"DE"},
{:name=>"30", :address=>"CHINA", :collection=>["LAPTOP", "SHIP", "MOUNTAIN"], :sequence=>"FG"}
]


My desired array of hashes should look like this:

[
{:name=>"1", :address=>"USA", :collection=>["LAND", "WATER", "OIL", "TREE", "SAND"], :sequence=>"AB"},
{:name=>"5-6", :address=>"UK,CANADA", :collection=>["LAND", "WATER", "OIL", "TREE", "SAND"], :sequence=>"BC,CD"},
{:name=>"29-30", :address=>"GERMANY,CHINA", :collection=>["LAPTOP", "SHIP", "MOUNTAIN"], :sequence=>"DE, FG"},
]


Use case II

array_of_hash = [
{:name=>"1", :address=>"USA", :collection=>["LAND", "WATER", "OIL", "TREE", "SAND"], :sequence=>"AB"},
{:name=>"2", :address=>"UK", :collection=>["LAND", "WATER", "OIL", "TREE", "SAND"], :sequence=>"BC"},
{:name=>"3", :address=>"CANADA", :collection=>["LAND", "WATER", "OIL", "TREE", "SAND"], :sequence=>"CD"},
{:name=>"29", :address=>"GERMANY", :collection=>["LAPTOP", "SHIP", "MOUNTAIN"], :sequence=>"DE"},
{:name=>"30", :address=>"CHINA", :collection=>["LAPTOP", "SHIP", "MOUNTAIN"], :sequence=>"FG"}
]


Desired result for use case II

[
{:name=>"1-3", :address=>"USA,UK,CANADA", :collection=>["LAND", "WATER", "OIL", "TREE", "SAND"], :sequence=>"AB,BC,CD"},
{:name=>"29-30", :address=>"GERMANY,CHINA", :collection=>["LAPTOP", "SHIP", "MOUNTAIN"], :sequence=>"DE, FG"},
]


What I did so far:

new_array_of_hashes = []
new_array_of_hashes << { name: array_of_hashes.map {|h| h[:name].to_i}} << {address: array_of_hashes.map {|h| h[:address]}} << {collection: array_of_hashes.map {|h| h[:collection]}} << {sequence: array_of_hashes.map {|h| h[:sequence]}}

[{:name=>[1, 5, 6, 29, 30]},
{:address=>["USA", "UK", "CANADA", "GERMANY", "CHINA"]},
{:collection=>
[["LAND", "WATER", "OIL", "TREE", "SAND"],
["LAND", "WATER", "OIL", "TREE", "SAND"],
["LAND", "WATER", "OIL", "TREE", "SAND"],
["LAPTOP", "SHIP", "MOUNTAIN"],
["LAPTOP", "SHIP", "MOUNTAIN"]]},
{:sequence=>["AB", "BC", "CD", "DE", "FG"]}]


I am only able to combine it.

Answer Source

First, let's make an array of the groups that we ultimately want. We'll use Ruby's Array#slice_when method, which iterates over an array with the current and next array element, allowing us to compare the two. Our conditional will instruct Ruby to slice the array if the names (converted to integers) are not sequential or if the collections are not identical.

>> groups = array_of_hash.slice_when { |i, j| i[:name].to_i + 1 != j[:name].to_i || i[:collection] != j[:collection] }.to_a

But because you are using Ruby 2.1, you'll need to use slice_before and use local variables to keep track of previous elements. Per the documentation, we can accomplish this by first priming a local variable:

>> prev = array_of_hash[0]

and then resetting it and a second local variable as we iterate over the array:

>> groups = array_of_hash.slice_before { |e| prev, prev2 = e, prev; prev2[:name].to_i + 1 != prev[:name].to_i || prev2[:collection] != prev[:collection] }.to_a

In either case, groups should now look like this:

=> [[{:name=>"1",
   :address=>"USA",
   :collection=>["LAND", "WATER", "OIL", "TREE", "SAND"],
   :sequence=>"AB"}],
 [{:name=>"5",
   :address=>"UK",
   :collection=>["LAND", "WATER", "OIL", "TREE", "SAND"],
   :sequence=>"BC"},
  {:name=>"6",
   :address=>"CANADA",
   :collection=>["LAND", "WATER", "OIL", "TREE", "SAND"],
   :sequence=>"CD"}],
 [{:name=>"29",
   :address=>"GERMANY",
   :collection=>["LAPTOP", "SHIP", "MOUNTAIN"],
   :sequence=>"DE"},
  {:name=>"30",
   :address=>"CHINA",
   :collection=>["LAPTOP", "SHIP", "MOUNTAIN"],
   :sequence=>"FG"}]]

Now we take the resulting array and map its elements to a new hash, formatted as you specified.

For :name, we take the first and last elements of the group, call .uniq to eliminate duplicates, and join them with a hyphen. (If only one element exists, join returns the single element unchanged.)

For :collection, we simply use the collection found in the first element of the group.

For :sequence, we join the sequences of each element of the group with a comma. (Again, single elements are returned unchanged.)

>> groups.map { |group| {name: [group.first[:name], group.last[:name]].uniq.join('-'), 
                         collection: group.first[:collection], 
                         sequence: group.map { |e| e[:sequence] }.join(',') } }

=> [{:name=>"1",
  :collection=>["LAND", "WATER", "OIL", "TREE", "SAND"],
  :sequence=>"AB"},
 {:name=>"5-6",
  :collection=>["LAND", "WATER", "OIL", "TREE", "SAND"],
  :sequence=>"BC,CD"},
 {:name=>"29-30",
  :collection=>["LAPTOP", "SHIP", "MOUNTAIN"],
  :sequence=>"DE,FG"}]
Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download