I have an array of thing
objects, each of which has an array of categories. I'd like to build a category hash with a category ID for a key and an array of things for a value.
category_hash = Hash.new
things.each { |thing|
thing.categories.each { |category|
category_hash[category] = Array.new unless category_hash[category]
category_hash[category] << thing
}
}
Purely for education, how can this be shortened?
1 Answer 1
There are several ways of making this more idiomatic:
A trivial note, but don't use
Array.new
.Use
[]
, it's shorter, clearer, and more idiomaticDon't use
category_hash[category] = [] unless category_hash[category]
Instead, use
category_hash[category] ||= []
. In general you can usea ||= b
instead ofa = b unless a
. In the case of hashes, you can skip this completely and just give the hash an appropriate default value:category_hash = Hash.new { |hash,key| hash[key] = [] }
Don't initialize a collection to an empty state, and then iterate over another collection, appending to the new collection. Use
map
/each_with_object
/inject
/etc. to turn one collection into another collection
A more idiomatic solution might look like this:
category_hash = things.each_with_object(Hash.new {|h,k| h[k] = [] }) do |thing,hash|
thing.categories.each { |cat| hash[cat] << thing }
end
A completely different approach would be to map each thing
's categories into a new hash, and then merge the resulting hashes. The problem is that you need a "deep" merge, like the one that comes with ActiveSupport:
categories_hash = things.map do |thing|
Hash[thing.categories.map { |cat| [cat,thing] }]
end.reduce(&:deep_merge)