Luis Grases
Luis Grases

Reputation: 27

Group array of objects by their array property elements

I have this array of cities:

Bakersfield, California
Interstates: ["I-5"]

Oakland, California
Interstates: ["I-80"]

Atlanta, Georgia
Interstates: ["I-20", "I-75", "I-86"]

Cleveland, Ohio
Interstates: ["I-71", "I-77", "I-80", "I-90"]

Arlington, Texas
Interstates: ["I-20", "I-30"]

The name, the state and the interstates array are properties of each city.

I want to group them by their interstates so the final result would look something like this:

I-20: [Arlington, Atlanta]
I-5: [Bakersfield]
I-86: [Atlanta]
...

Is there a quick way to do this?

EDIT: Here is the true array, just as @wurde example.

cities = {
  'Bakersfield' => {
    state: 'California',
    interstate: ['I-5']
  },
  'Oakland' => {
    state: 'California',
    interstate: ['I-80']
  },
  'Atlanta' => {
    state: 'Georgia',
    interstate: ["I-20", "I-75", "I-86"]
  },
  'Cleveland' => {
    state: 'Ohio',
    interstate: ["I-71", "I-77", "I-80", "I-90"]
  },
  'Arlington' => {
    state: 'Texas',
    interstate: ["I-20", "I-30"]
  }
}

Upvotes: 0

Views: 84

Answers (5)

dimakura
dimakura

Reputation: 7655

We have:

cities = {
  'Bakersfield' => ['I-5'],
  'Oakland' => ['I-80'],
  'Atlanta' => ["I-20", "I-75", "I-86"],
  'Cleveland' => ["I-71", "I-77", "I-80", "I-90"],
  'Arlington' => ["I-20", "I-30"]
}

Longer version We can get what you want by this operations:

require 'set'
interstates = cities.inject(Set.new){|all,item| all+item[1]} # => #<Set: {"I-5", "I-80", "I-20", "I-75", "I-86", "I-71", "I-77", "I-90", "I-30"}>
result = interstates.map{|inter| [inter, cities.select{|_,interstates| interstates.include?(inter)}.keys]}.to_h # => {"I-5"=>["Bakersfield"], "I-80"=>["Oakland", "Cleveland"], "I-20"=>["Atlanta", "Arlington"], "I-75"=>["Atlanta"], "I-86"=>["Atlanta"], "I-71"=>["Cleveland"], "I-77"=>["Cleveland"], "I-90"=>["Cleveland"], "I-30"=>["Arlington"]}

We use Set because it's efficient in given settings (for more elegant syntax, see note below). On line 2 we get all interstates using Enumerable's inject method. Final result is obtained on line 3, where we use mapping and filtering (map and select methods). Note, that to_h method is available from Ruby 2.1. If you are on older Ruby, you can convert mapped array to Hash using Hash[array].

One-liner It can even be written as one-liner:

cities.inject(Set.new){|all,item| all+item[1]}.map{|inter| [inter, cities.select{|_,interstates| interstates.include?(inter)}.keys]}.to_h

Note I included Set here for performance. If you prefer readability, then it's much shorter simply write interstates = cities.values.flatten.uniq, as @engineersmnky noted in comment.

Upvotes: 0

Cary Swoveland
Cary Swoveland

Reputation: 110685

Using @wurde's example (thank you, thank you, thank you):

cities.each_with_object({}) do |(k,g),h|
  g[:interstate].each { |i| h.update(i=>[k]) { |_,o,n| o+n } }
end
  #=> {"I-5" =>["Bakersfield"],
  #    "I-80"=>["Oakland", "Cleveland"],
  #    "I-20"=>["Atlanta", "Arlington"],
  #    "I-75"=>["Atlanta"],
  #    "I-86"=>["Atlanta"],
  #    "I-71"=>["Cleveland"],
  #    "I-77"=>["Cleveland"],
  #    "I-90"=>["Cleveland"],
  #    "I-30"=>["Arlington"]} 

This uses the form of Hash#update (aka merge!) that uses the block:

{ |_,o,n| o+n }

to determine values of keys that are present in both hashes being merged. The key is the intestate (written _ because it is not be used), o is an array of cities for the key in the hash h being constructed and n is an array of a single city for the merging hash { i=>[k] }.

Upvotes: 0

engineersmnky
engineersmnky

Reputation: 29328

Given the assumption that @wurde's Hash structure is true I would do something like this

cities = {
  'Bakersfield' => {
    state: 'California',
    interstate: ['I-5']
  },
  'Oakland' => {
    state: 'California',
    interstate: ['I-80']
  },
  'Atlanta' => {
    state: 'Georgia',
    interstate: ["I-20", "I-75", "I-86"]
  },
  'Cleveland' => {
    state: 'Ohio',
    interstate: ["I-71", "I-77", "I-80", "I-90"]
  },
  'Arlington' => {
    state: 'Texas',
    interstate: ["I-20", "I-30"]
  }
}


cities.each_with_object(Hash.new {|h,k| h[k] = []}) do |(city_name,data),h|
  data[:interstate].each do |interstate|
    h[interstate] << "#{city_name}, #{data[:state]}"
  end
end
#=> {"I-5"=>["Bakersfield, California"], 
     "I-80"=>["Oakland, California", "Cleveland, Ohio"], 
     "I-20"=>["Atlanta, Georgia", "Arlington, Texas"], 
     "I-75"=>["Atlanta, Georgia"], 
     "I-86"=>["Atlanta, Georgia"], 
     "I-71"=>["Cleveland, Ohio"], 
     "I-77"=>["Cleveland, Ohio"], 
     "I-90"=>["Cleveland, Ohio"], 
     "I-30"=>["Arlington, Texas"]}

Upvotes: 1

wurde
wurde

Reputation: 2617

cities = {
  'Bakersfield' => {
    state: 'California',
    interstate: ['I-5']
  },
  'Oakland' => {
    state: 'California',
    interstate: ['I-80']
  },
  'Atlanta' => {
    state: 'Georgia',
    interstate: ["I-20", "I-75", "I-86"]
  },
  'Cleveland' => {
    state: 'Ohio',
    interstate: ["I-71", "I-77", "I-80", "I-90"]
  },
  'Arlington' => {
    state: 'Texas',
    interstate: ["I-20", "I-30"]
  }
}

interstates = {}
cities.each do |city|
  city[1][:interstate].each do |road|
    interstates[road] ||= []
    interstates[road] << city[0]
  end
end

puts interstates.inspect
#=> {
#=>   "I-5"=>["Bakersfield"],
#=>   "I-80"=>["Oakland", "Cleveland"],
#=>   "I-20"=>["Atlanta", "Arlington"],
#=>   "I-75"=>["Atlanta"],
#=>   "I-86"=>["Atlanta"],
#=>   "I-71"=>["Cleveland"],
#=>   "I-77"=>["Cleveland"],
#=>   "I-90"=>["Cleveland"],
#=>   "I-30"=>["Arlington"]
#=> }

Upvotes: 0

djaszczurowski
djaszczurowski

Reputation: 4515

try this

mapping = {}

cities.each do |city|
    city.interstates.each do |interstate|
      mapping[interstate] ||= []
      mapping[interstate] << city
    end
end

Upvotes: 1

Related Questions