I am using Ruby on Rails 4 and I would like to replace all hash keys so to change the hash from
h_before = {:"aaa.bbb" => 1, :c => 2, ...}
to
h_after = {:bbb => 1, :c => 2, ...}
That is, I would like to someway "demodulize" all hash keys having the .
. How can I make that?
h_before = {:"aaa.bbb" => 1, :c => 2}
h_after =
h_before.inject({}){|h, (k, v)| h[k.to_s.split(".").last.to_sym] = v; h}
# => {:bbb = > 1, :c => 2}
each_with_object is a cleaner and shorter approach than inject from the answer:
h_before.each_with_object({}){|(k, v),h| h[k.to_s.split(".").last.to_sym] = v}
=> {:bbb=>1, :c=>2}
Since there are a bunch of answers claiming to do the same thing, I thought it was time to post some benchmarks:
require 'fruity'
h_before = {:"aaa.bbb" => 1, :c => 2}
def cdub_test(hash)
Hash[hash.map {|k, v| [k.to_s.gsub(/^.*\./,"").to_sym, v]}]
end
def matt_test(old_hash)
Hash[old_hash.map { |k,v| [ k.to_s.sub(/.*\./,'').to_sym, v ] }]
end
class Hash
require 'active_support/core_ext/hash/indifferent_access'
def grep_keys(pattern)
return inject(HashWithIndifferentAccess.new){|h, (k, v)|
h[$1 || k] = v if pattern =~ k.to_s ; h }
end
end
def phlip_test(hash)
hash.grep_keys(/\.(\w+)$/)
end
def bjhaid_test(hash)
hash.each_with_object({}){|(k, v),h| h[k.to_s.split(".").last.to_sym] = v}
end
def sawa_test(hash)
hash.inject({}){|h, (k, v)| h[k.to_s.split(".").last.to_sym] = v; h}
end
compare do
cdub { cdub_test(h_before) }
matt { matt_test(h_before) }
phlip { phlip_test(h_before) }
bjhaid { bjhaid_test(h_before) }
sawa { sawa_test(h_before) }
end
Which outputs:
Running each test 1024 times. Test will take about 1 second.
bjhaid is similar to sawa
sawa is faster than matt by 60.00000000000001% ± 10.0%
matt is faster than phlip by 30.000000000000004% ± 10.0% (results differ: {:bbb=>1, :c=>2} vs {"bbb"=>1})
phlip is similar to cdub (results differ: {"bbb"=>1} vs {:bbb=>1, :c=>2})
Notice that phlip's code doesn't return the desired results.
old_hash = {:"aaa.bbb" => 1, :c => 2 }
new_hash = Hash[old_hash.map { |k,v| [ k.to_s.sub(/.*\./,'').to_sym, v ] }]
1.9.3p448 :001 > hash = {:"aaa.bbb" => 1, :c => 2 }
=> {:"aaa.bbb"=>1, :c=>2}
1.9.3p448 :002 > Hash[hash.map {|k, v| [k.to_s.gsub(/^.*\./,"").to_sym, v]}]
=> {:bbb=>1, :c=>2}
My grep_keys
has never failed me here:
class Hash
def grep_keys(pattern)
return inject(HashWithIndifferentAccess.new){|h, (k, v)|
h[$1 || k] = v if pattern =~ k.to_s ; h }
end
end
It returns a shallow-copy of the Hash, but only with the matched keys. If the input regular expression contains a ()
match, the method replaces the key with the matched value. (Note this might merge two or more keys, and discard all but a random value for them!) I use it all the time to cut up a Rails param
into sub-params containing only the keys that some module needs.
{:"aaa.bbb" => 1, :c => 2 }.grep_keys(/\.(\w+)$/)
returns {"bbb"=>1}
.
This method upgrades your actual problem to "how to define a regexp that matches what you mean by 'having a ".".'"