gallant
gallant

Reputation: 59

How to collapse a multi-dimensional array of hashes in Ruby?

Background:

Hey all, I am experimenting with external APIs and am trying to pull in all of the followers of a User from a site and apply some sorting.

I have refactored a lot of the code, HOWEVER, there is one part that is giving me a really tough time. I am convinced there is an easier way to implement this than what I have included and would be really grateful on any tips to do this in a much more eloquent way.

My goal is simple. I want to collapse an array of arrays of hashes (I hope that is the correct way to explain it) into one array of hashes.

Problem Description:

I have an array named f_collectionswhich has 5 elements. Each element is an array of size 200. Each sub-element of these arrays is a hash of about 10 key-value pairs. My best representation of this is as follows:

f_collections = [ collection1, collection2, ..., collection5 ]
collection1 = [ hash1, hash2, ..., hash200]
hash1 = { user_id: 1, user_name: "bob", ...}

I am trying to collapse this multi-dimensional array into one array of hashes. Since there are five collection arrays, this means the results array would have 1000 elements - all of which would be hashes.

followers = [hash1, hash2, ..., hash1000]

Code (i.e. my attempt which I do not want to keep):

I have gotten this to work with a very ugly piece of code (see below), with nested if statements, blocks, for loops, etc... This thing is a nightmare to read and I have tried my hardest to research ways to do this in a simpler way, I just cannot figure out how. I have tried flatten but it doesn't seem to work.

I am mostly just including this code to show I have tried very hard to solve this problem, and while yes I solved it, there must be a better way!

Note: I have simplified some variables to integers in the code below to make it more readable.

for n in 1..5 do
        if n < 5
          (0..199).each do |j|
            if n == 1
              nj = j
            else
              nj = (n - 1) * 200 + j
            end

            @followers[nj] = @f_collections[n-1].collection[j]
          end
        else
          (0..199).each do |jj|
            njj = (4) * 200  + jj
            @followers[njj] = @f_collections[n-1].collection[jj]
          end
        end

      end

Upvotes: 0

Views: 870

Answers (1)

Pascal
Pascal

Reputation: 8646

Oh... so It is not an array objects that hold collections of hashes. Kind of. Lets give it another try:

flat = f_collection.map do |col|
  col.collection
end.flatten

which can be shortened (and is more performant) to:

flat = f_collection.flat_map do |col|
  col.collection
end

This works because the items in the f_collection array are objects that have a collection attribute, which in turn is an array.

So it is "array of things that have an array that contains hashes"


Old Answer follows below. I leave it here for documentation purpose. It was based on the assumption that the data structure is an array of array of hashes.

Just use #flatten (or #flatten! if you want this to be "inline")

flat = f_collections.flatten

Example

sub1 = [{a: 1}, {a: 2}]
sub2 = [{a: 3}, {a: 4}]
collection = [sub1, sub2]

flat = collection.flatten # returns a new collection
puts flat #> [{:a=>1}, {:a=>2}, {:a=>3}, {:a=>4}]

# or use the "inplace"/"destructive" version 
collection.flatten! # modifies existing collection
puts collection #> [{:a=>1}, {:a=>2}, {:a=>3}, {:a=>4}]

Some recommendations for your existing code:

Do not use for n in 1..5, use Ruby-Style enumeration:

["some", "values"].each do |value|
  puts value
end

Like this you do not need to hardcode the length (5) of the array (did not realize you removed the variables that specify these magic numbers). If you you want to detect the last iteration you can use each_with_index:

a = ["some", "home", "rome"]
a.each_with_index do |value, index|
  if index == a.length - 1
    puts "Last value is #{value}"
  else
    puts "Values before last: #{value}"
  end
end

While #flatten will solve your problem you might want to see how DIY-solution could look like:

def flatten_recursive(collection, target = [])
  collection.each do |item|
    if item.is_a?(Array)
      flatten_recursive(item, target)
    else
      target << item
    end
  end
  target
end

Or an iterative solution (that is limited to two levels):

def flatten_iterative(collection)
  target = []
  collection.each do |sub|
    sub.each do |item|
      target << item
    end
  end
  target
end

Upvotes: 1

Related Questions