0

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"]
  }
}
2
  • 1
    Can you please post the true Array? Is this an Array of Hashes or is it a multi dimensional Array? With True data it will be easier to determine a functional efficient solution to the problem. Commented Sep 1, 2015 at 20:50
  • 1
    ...and while you're at it, please assign it to a variable (e.g., cities = [ {city: "Bakersfield", state: "California", Interstates: ["I-5"] }, ... ], so readers can refer to cities without having to define it. Commented Sep 1, 2015 at 22:08

5 Answers 5

1

try this

mapping = {}

cities.each do |city|
    city.interstates.each do |interstate|
      mapping[interstate] ||= []
      mapping[interstate] << city
    end
end
Sign up to request clarification or add additional context in comments.

3 Comments

consider (mapping[interstate] ||= []) << city. Looks weird first, but once you got the hang of it it becomes useful in probably many places.
@Felix In that case why not consider mapping = Hash.new {|h,k| h[k] = []} Then it would just be mapping[interstate] << city.
@engineersmnky Yes, thats another possibility.
1

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"]}

Comments

0
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"]
#=> }

Comments

0

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] }.

Comments

0

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.

3 Comments

why not interstates = cities.values.flatten? I don't see a need to inject a Set
Is it good in terms of performance? I have feeling Set should perform it much faster. Your version is much shorter though.
@engineersmnky btw, the full version is interstates = cities.values.flatten.uniq

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.