1

I DID checkout both this question, and that other similar question, none of them offers a solution to replace elements of an array with multiple test values.

I have a Ruby array:

array = ["america", "europe", "asia", "africa", "france", "usa", "spain", "paris", "los angeles"]

I would like to transform this array, to get the following result:

array = ["continent", "continent", "continent", "continent", "country", "country", "country", "city", "city"]

My first attempt was to do something like that:

array.collect! do |element|
  (element == "america") ? "continent" : element
  (element == "europe") ? "continent" : element
  (element == "asia") ? "continent" : element
  (element == "africa") ? "continent" : element
  (element == "france") ? "country" : element
  (element == "usa") ? "country" : element
  (element == "spain") ? "country" : element
  (element == "paris") ? "city" : element
  (element == "los angeles") ? "city" : element
end

I have two problems with this code:

  1. I am not sure this is the way to use a Ruby block with do end.

  2. This code is not DRY and I believe I could use a set of three case loops instead, one for continents, one for countries and one for cities.

Community
  • 1
  • 1
Thibaud Clement
  • 6,607
  • 10
  • 50
  • 103

4 Answers4

9

Here's a better way

array.collect! do |element|
  case element
  when 'america', 'europe', 'asia', 'africa'
    'continent'
  when 'france', 'spain'
    'country'
  when 'paris', 'los angeles'
    'city'
  else
    element
  end
end
Sergio Tulentsev
  • 226,338
  • 43
  • 373
  • 367
4

I'd use a hash to do the lookups. It's O(1) and very simple. The second argument to fetch is the default if a key doesn't exist.

INDEX = {
  "america" => "continent", 
  "europe" => "continent", 
  "asia" => "continent", 
  "africa" => "continent", 
  "france" => "country", 
  "usa" => "country", 
  "spain" => "country", 
  "paris" => "city", 
  "los angeles" => "city"
}

[
  "america", 
  "europe", 
  "asia", 
  "africa", 
  "france", 
  "usa", 
  "spain", 
  "paris", 
  "los angeles", 
  "not indexed"
].map{|key| INDEX.fetch(key, key) }
TJ Singleton
  • 750
  • 6
  • 12
2

You can not get any more DRY than this. (also notice this is very similar to what TJ Singleton does)

array = ["america", "europe", "asia", "africa", "france", "usa", "spain", "paris", "los angeles"]

definitions = {
  "continent" => ["america", "europe", "asia", "africa"],
  "country" => ["france", "usa", "spain"],
  "city" => ["paris", "los angeles"],
  "planet" => ["mars","earth"]
}

inverse_array = definitions.map {|k,v| v.map { |e| [e, k]}}.flatten(1)
inverse_hash = Hash[inverse_array]

output = array.map { |e| inverse_hash[e] }
puts output.inspect
Damiano Stoffie
  • 897
  • 5
  • 8
  • 1
    I like your approach, but would suggest `definitions.flat_map { |k,v| v.product [k] }.to_h.values_at *array`. – Cary Swoveland Nov 19 '15 at 20:03
  • Ahah realy nice code man! I was just thinking now "uhm, sure there is something more elegant than this `flatten(1)` ..." big up! – Damiano Stoffie Nov 19 '15 at 20:05
  • One difference from the original code is it won't handle a value , but you use Hash.new to provide a default. inverse_hash = Hash.new {|hash, key| key }; inverse_array = definitions.each {|k,v| v.each {|e| inverse_hash[e] = k }} – TJ Singleton Nov 19 '15 at 20:20
1

What about cleaning it up by using Set to keep track of what a continent, country and city is? Lookups in a set are O(1) so you're no worse for wear here:

continents = Set.new ["america", "europe", "asia", "africa"]
countries = Set.new ["france", "usa", "spain"]
cities = Set.new ["paris", "los angeles"]

array = ["america", "europe", "asia", "africa", "france", "usa", "spain", "paris", "los angeles"]

newArray = array.map{ |c|
puts c
  if continents.include? c
    'continent' 
  elsif countries.include? c
    'country' 
  elsif cities.include? c
    'city'
  else
    'unknown'
  end
}

another solution with maps where we create a lookup tables where the keys are continents/countries/cities and the values are the corresponding strings "continent", "country" or "city":

continents = ["america", "europe", "asia", "africa"].each_with_object({}) { |k,h| h[k] = 'continent' }
countries = ["france", "usa", "spain"].each_with_object({}) { |k,h| h[k] = 'country' }
cities = ["paris", "los angeles"].each_with_object({}) { |k,h| h[k] = 'city' }

array = ["america", "europe", "asia", "africa", "france", "usa", "spain", "paris", "los angeles"]

newArray = array.map{ |c| continents[c] || countries[c] || cities[c] || c }
David Zorychta
  • 13,039
  • 6
  • 45
  • 81