3

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?

Nakilon
  • 34,866
  • 14
  • 107
  • 142
Backo
  • 18,291
  • 27
  • 103
  • 170
  • Please show the code you've written toward answering this question. It's better for us to work with something you've started, than it is for us to start from scratch and you shoehorn it into place. – the Tin Man Nov 27 '13 at 20:05
  • If this were [Code Golf](http://codegolf.stackexchange.com/questions), I would suggest `eval(h_before.gsub("aaa.",""))`. – Cary Swoveland Nov 27 '13 at 20:29

6 Answers6

4

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}
bjhaid
  • 9,592
  • 2
  • 37
  • 47
3
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}
sawa
  • 165,429
  • 45
  • 277
  • 381
  • Can you change (explicit) names so that I understand what is what? – Backo Nov 27 '13 at 19:50
  • You are right but, for example, I don't understand the `h` to what refers in `h.keys.each{|k| h[k.to_s.split(".").last.to_sym] = h.delete(k)}`. `h` refers to the duplicated hash? Where? – Backo Nov 27 '13 at 19:52
  • 2
    What about `each_with_object` rather than `inject` to get rid of that pesky `;h` at the end? – Cary Swoveland Nov 27 '13 at 20:21
  • I thought `each_with_object` was deprecated. But any solution should use `HashWithIndiferenceAccess`; this being a Rails-tagged question... – Phlip Nov 27 '13 at 20:28
  • @Philip, re deprecation, see [this](http://stackoverflow.com/questions/5481009/why-is-enumerableeach-with-object-deprecated), especially sawa's and chuck's answers. – Cary Swoveland Nov 27 '13 at 20:36
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.

the Tin Man
  • 158,662
  • 42
  • 215
  • 303
1
old_hash = {:"aaa.bbb" => 1, :c => 2 }
new_hash = Hash[old_hash.map { |k,v| [ k.to_s.sub(/.*\./,'').to_sym, v ] }]
Matt
  • 20,108
  • 1
  • 57
  • 70
0
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} 
CDub
  • 13,146
  • 4
  • 51
  • 68
  • 1
    Can you write a `RegEx`, to extract `"bbb"` only from `:"aaa.bbb"`. Then code can be written as `Hash[hash.map {|k, v| [k.to_s[/your regex/].to_sym, v]}]` – Arup Rakshit Nov 27 '13 at 20:03
  • I am asking you to do this,as I am sooooo week in `RegEx`. :) – Arup Rakshit Nov 27 '13 at 20:11
  • I sense some sarcasm... The regex is within the gsub above will handle the gsub.... `/^.*\./` – CDub Nov 27 '13 at 20:14
  • That said, if you're serious... The regex for your answer would be `/(?<=\.).*/` ... `hash.keys.first.to_s[/(?<=\.).*/] => "bbb"` – CDub Nov 27 '13 at 20:20
  • `sub` should be used instead of `gsub`. The first will only do one search and replace, unlike `gsub` which will try to find additional matches to replace. It's a minimal difference for short strings, but does add up over time. For long strings it can be a significant difference. – the Tin Man Nov 28 '13 at 09:48
0

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 ".".'"

Phlip
  • 5,253
  • 5
  • 32
  • 48