rails nested hash with default values [duplicate] - ruby

This question already has answers here:
Strange, unexpected behavior (disappearing/changing values) when using Hash default value, e.g. Hash.new([])
(4 answers)
Closed 2 years ago.
I'd like to create a new Hash with nested default values. I thought it should be like
h = Hash.new(count: 0, rating: 0)
So I can do stuff like
h['a'][:count] += 1
h['a'][:rating] += 1
and so on. But when I try it in the console it looks like this:
2.3.1 :046 > h = Hash.new(count: 0, rating: 0)
=> {}
2.3.1 :047 > h["a"]
=> {:count=>0, :rating=>0}
2.3.1 :048 > h["a"][:count]
=> 0
2.3.1 :049 > h["a"][:count] += 1
=> 1
2.3.1 :050 > h["b"][:count] += 1
=> 2
2.3.1 :051 > h
=> {}
So my questions are:
Why is h["b"][:count] += 1 returning 2 and not 1?
why is h empty?
Thanks in advance!

The doc for Hash::new explains the three ways of initializing a hash and, in your case, you are using an object in the Hash constructor:
If obj is specified, this single object will be used for all default values.
If you want that each missing key creates it's own object, create the hash with a block, like this:
h = Hash.new { |h,k| h[k] = { count: 0, rating: 0 } }
Then:
2.6.3 :012 > h
=> {}
2.6.3 :013 > h['a'][:count] = 5
=> 5
2.6.3 :015 > h
=> {"a"=>{:count=>5, :rating=>0}}

You can find this behaviour documented in the Hash::new documentation:
new → new_hash
new(obj) → new_hash
new {|hash, key| block } → new_hash
Returns a new, empty hash. If this hash is subsequently accessed by a
key that doesn't correspond to a hash entry, the value returned
depends on the style of new used to create the hash. In the first
form, the access returns nil. If obj is specified, this single
object will be used for all default values. If a block is
specified, it will be called with the hash object and the key, and
should return the default value. It is the block's responsibility to
store the value in the hash if required.
h = Hash.new("Go Fish")
h["a"] = 100
h["b"] = 200
h["a"] #=> 100
h["c"] #=> "Go Fish"
# The following alters the single default object
h["c"].upcase! #=> "GO FISH"
h["d"] #=> "GO FISH"
h.keys #=> ["a", "b"]
# While this creates a new default object each time
h = Hash.new { |hash, key| hash[key] = "Go Fish: #{key}" }
h["c"] #=> "Go Fish: c"
h["c"].upcase! #=> "GO FISH: C"
h["d"] #=> "Go Fish: d"
h.keys #=> ["c", "d"]
In your example h["a"] and h["b"] both return the exact same default hash object. Meaning that if you change h["a"], h["b"] is also changed. Since you never set h["a"] or h["b"] the hash appears empty.
To assign a new hash on access you'll have to use the block syntax as shown by luis.parravicini.

Related

Does Ruby Hash's keep a separate list of read values vs assigned values? [duplicate]

This question already has answers here:
Strange, unexpected behavior (disappearing/changing values) when using Hash default value, e.g. Hash.new([])
(4 answers)
Closed 4 years ago.
This is related to Ruby hash default value behavior
But maybe the explanation there doesn't include this part: it seems that Ruby's Hash default value are separate whether you "read it", or see "what is set"?
One example is:
foo = Hash.new([])
foo[123].push("hi")
p foo # => {}
p foo[123] # => ["hi"]
p foo # => {}
How is it that foo[123] has a value, but foo is all empty, is somewhat beyond my comprehension... the only way I can understand it is that Ruby Hash keeps a separate list for the "read" or "getter", while somehow the "internal" assigned value are different.
If one of Ruby's design principles is "to have the least amount of surprise to the programmers", then the foo is empty but foo[123] is something, is somewhat in this case, a surprise to me.
(I haven't seen that in other languages actually... if there is a case where another language has similar behavior, maybe it is easier to make a connection.)
Suppose `
h = Hash.new(:cat)
h[:a] = 1
h[:b] = 2
h #=> {:a=>1, :b=>2}
Now
h[:a] #=> 1
h[:b] #=> 2
h[:c] #=> :cat
h[:d] #=> :cat
h #=> {:a=>1, :b=>2}
h = Hash.new(:cat) defines an empty hash h with a default value of :cat. This means that if h does not have a key k, h[k] will return :cat, nothing more, nothing less. As you can see above, executing h[k] does not change the hash when k is :c or :d.
On the other hand,
h[:c] = h[:c]
#=> :c
h #=> {:a=>1, :b=>2, :c=>:cat}
Confused? Let me write this without the syntactic sugar:
h.[]=(:d, h.[](:d))
#=> :cat
h #=> {:a=>1, :b=>2, :d=>:cat}
The default value is returned by h.[](:d) (i.e., h[:d]) whereas Hash#[]= is an assignment method (that takes two arguments, a key and a value) to which the default does not apply.
A common use of this default is to create a counting hash:
a = [1,3,1,4,2,5,4,4]
h = Hash.new(0)
a.each { |x| h[x] = h[x] + 1 }
h #=> {1=>2, 3=>1, 4=>3, 2=>1, 5=>1}
Initially, when h is empty and x #=> 1, h[1] = h[1] + 1 will evaluate to h[1] = 0 + 1, because (since h has no key 1) h[1] on the right side of the equality is set equal to the default value of zero. The next time 1 is passed to the block (x #=> 1), x[1] = x[1] + 1, which equals x[1] = 1 + 1. This time the default value is not used because h now has a key 1.
This would normally be written (incidentally):
a.each_with_object(Hash.new(0)) { |x,h| h[x] += 1 }
#=> {1=>2, 3=>1, 4=>3, 2=>1, 5=>1}
One generally does not want the default value to be a collection, such as an array or hash. Consider the following:
h = Hash.new([])
[1,2,3].map { |n| h[n] = h[n] }
h #=> {1=>[], 2=>[], 3=>[]}
Now observe:
h[1] << 2
h #=> {1=>[2], 2=>[2], 3=>[2]}
This is normally not the desired behaviour. It has happened because
h.map { |k,v| v.object_id }
#=> [25886508, 25886508, 25886508]
That is, all the values are the same object, so if the value of one key is changed the values of all other keys are changed as well.
The way around this is to use a block when defining the hash:
h = Hash.new { |h,k| h[k]=[] }
[1,2,3].each { |n| h[n] = h[n] }
h #=> {1=>[], 2=>[], 3=>[]}
h[1] << 2
h #=> {1=>[2], 2=>[], 3=>[]}
h.map { |k,v| v.object_id }
#=> [24172884, 24172872, 24172848]
When the hash h does not have a key k the block { |h,k| h[k]=[] } is executed and returns an empty array specific to that key.
The statement:
foo = Hash.new([])
creates a new Hash that has an empty array ([] as default value). The default value is the value returned by Hash::[] when its argument is not a key present in the hash.
The statement:
foo[123]
invokes Hash::[] and, because the hash is empty (the key 123 is not present in the hash), it returns a reference to the default value which is an object of type Array.
The statement above doesn't create the 123 key in the hash.
Ruby objects are always passed and returned by reference. This means that the statement above doesn't return a copy of the default value of the hash but a reference to it.
The statement:
foo[123].push("hi")
modifies the above mentioned array. Now, the default value of the foo hash is not an empty array any more; it is the array ["hi"]. But the has is still empty; none of the above statements added some (key, value) pair to it.
How is it that foo[123] has a value
foo[123] doesn't have any value, the key 123 is not present in the hash (the hash is empty). A subsequent call to foo[123] returns a reference to the default value again and the default value now it's ["hi"]. And a call to foo[456] or foo['abc'] also returns a reference to the same default value.
You didn't actually change the value of key 123, you're just accessing the default value [] you provided during initialization. You can confirm this if you inspect a different value like foo[0].
If you would do this:
foo[123] = ["hi"]
you could see the new entry, because you've created a new array under the key 123.
Edit
When you call foo[123].push("hi"), you're mutating the (default) value instead of adding a new entry.
Calling foo[123] += ["hi"] creates a new array under the given key, replacing the previous one if it existed, which will show the behavior you desire.
Printing out the hash with:
p foo
only prints the values stored in the hash. It does not display the default value (or anything added to the default array).
When you execute:
p foo[123]
Because 123 does not exist, it access the default value.
If you added two values to the default value:
foo[123].push("hi")
foo[456].push("hello")
your output would be:
p foo # => {}
p foo[123] # => ["hi","hello"]
p foo # => {}
Here, poo[123] does again still not exist, so it prints out the contents of the default value.

What is meant: "Hash.new takes a default value for the hash, which is the value of the hash for a nonexistent key"

I'm currently going through the Ruby on Rails tutorial by Michael Hartl
Not understanding the meaning of this statement found in section 4.4.1:
Hashes, in contrast, are different. While the array constructor
Array.new takes an initial value for the array, Hash.new takes a
default value for the hash, which is the value of the hash for a
nonexistent key:
Could someone help explain what is meant by this? I don't understand what the author is trying to get at regarding how hashes differ from arrays in the context of this section of the book
You can always try out the code in irb or rails console to find out what they mean.
Array.new
# => []
Array.new(7)
# => [nil, nil, nil, nil, nil, nil, nil]
h1 = Hash.new
h1['abc']
# => nil
h2 = Hash.new(7)
h2['abc']
# => 7
Arrays and hashes both have a constructor method that takes a value. What this value is used for is different between the two.
For arrays, the value is used to initialize the array (example taken from mentioned tutorial):
a = Array.new([1, 3, 2])
# `a` is equal to [1, 3, 2]
Unlike arrays, the new constructor for hashes doesn't use its passed arguments to initialize the hash. So, for example, typing h = Hash.new('a', 1) does not initialize the hash with a (key, value) pair of a and 1:
h = Hash.new('a', 1) # NO. Does not give you { 'a' => 1 }!
Instead, passing a value to Hash.new causes the hash to use that value as a default when a non-existent key is passed. Normally, hashes return nil for non-existent keys, but by passing a default value, you can have hashes return the default in those cases:
nilHash = { 'x' => 5 }
nilHash['x'] # Return 5, because the key 'x' exists in nilHash
nilHash['foo'] # Returns nil, because there is no key 'foo' in nilHash
defaultHash = Hash.new(100)
defaultHash['x'] = 5
defaultHash['x'] # Return 5, because the key 'x' exists in defaultHash
defaultHash['foo']
# Returns 100 instead of nil, because you passed 100
# as the default value for non-existent keys for this hash
Begin by reading the docs for the class method Hash#new. You will see there are three forms:
new → new_hash
new(obj) → new_hash
new {|hash, key| block } → new_hash
Creating an Empty Hash
The first form is used to create an empty hash:
h = Hash.new #=> {}
which is more commonly written:
h = {} #=> {}
The other two ways of creating a hash with Hash#new establish a default value for a key/value pair when the hash does not already contain the key.
Hash.new with an argument
You can create a hash with a default value in one of two ways:
Hash.new(<default value>)
or
h = Hash.new # or h = {}
h.default = <default value>
Suppose the default value for the hash were 4; that is:
h = Hash.new(4) #=> {}
h[:pop] = 7 #=> 7
h[:pop] += 1 #=> 8
h[:pop] #=> 8
h #=> {:pop=>8}
h[:chips] #=> 4
h #=> {:pop=>8}
h[:chips] += 1 #=> 5
h #=> {:pop=>8, :chips=>5}
h[:chips] #=> 5
Notice that the default value does not affect the value of :pop. That's because it was created with an assignment:
h[:pop] = 7
h[:chips] by itself merely returns the default value (4); it does not add the key/value pair :chips=>4 to the hash! I repeat: it does not add the key/value pair to the hash. That's important!
h[:chips] += 1
is shorthand for:
h[:chips] = h[:chips] + 1
Since the hash h does not have a key :chips when h[:chips] on the right side of the equals sign is evaluated, it returns the default value of 4, then 1 is added to make it 5 and that value is assigned to h[:chips], which adds the key value pair :chips=>5 to the hash, as seen in following line. The last line merely reports the value for the existing key :chips.
So why would you want to establish a default value? I would venture that the main reason is to be able to initialize it with zero, so you can use:
h[k] += 1
instead of
k[k] = (h.key?(k)) ? h[k] + 1 : 1
or the trick:
h[k] = (h[k] ||= 0) + 1
(which only works when hash values are intended to be non-nil). Incidentally, key? is aka has_key?.
Can we make the default a string instead? Of course:
h = Hash.new('magpie')
h[:bluebird] #=> "magpie"
h #=> {}
h[:bluebird] = h[:bluebird] #=> "magpie"
h #=> {:bluebird=>"magpie"}
h[:redbird] = h[:redbird] #=> "magpie"
h #=> {:bluebird=>"magpie", :redbird=>"magpie"}
h[:bluebird] << "jay" #=> "magpiejay"
h #=> {:bluebird=>"magpiejay", :redbird=>"magpiejay"}
You may be scratching your head over the last line: why did h[:bluebird] << "jay" cause h[:redbird] to change?? Perhaps this will explain what's going on here:
h[:robin] #=> "magpiejay"
h[:robin].object_id #=> 2156227520
h[:bluebird].object_id #=> 2156227520
h[:redbird].object_id #=> 2156227520
h[:robin] merely returns the default value, which we see has been changed from "magpie" to "magpiejay". Now look at the object_id's for the default value and for the values associated with the keys :bluebird and :redbird. As you see, all values are the same object, so if we change one, we change all the the others, including the default value. It is now evident why h[:bluebird] << "jay" changed the default value.
We can clarify this further by adding a stately eagle:
h[:eagle] #=> "magpiejay"
h[:eagle] += "starling" #=> "magpiejaystarling"
h[:eagle].object_id #=> 2157098780
h #=> {:bluebird=>"magpiejay", :redbird=>"magpiejay", :eagle=>"magpiejaystarling"}
Because
h[:eagle] += "starling" #=> "magpiejaystarling"
is equivalent to:
h[:eagle] = h[:eagle] + "starling"
we have created a new object on the right side of the equals sign and assigned it to h[:eagle]. That's why the values for the keys :bluebird and :redbird are unaffected and h[:eagle] has a different object_id.
We have the similar problems if we write: Hash.new([]) or Hash.new({}). If there are ever reasons to use those defaults, I'm not aware of them. It certainly can be very useful for the default value to be an empty string, array or hash, but for that you need the third form of Hash.new, which takes a block.
Hash.new with a block
We now consider the third and final version of Hash#new, which takes a block, like so:
Hash.new { |h,k| ??? }
You may be expecting this to be devilishly complex and subtle, certainly much harder to grasp than the other two forms of the method. If so, you'd be wrong. It's actually quite simple, if you think of it as looking like this:
Hash.new { |h,k| h[k] = ??? }
In other words, Ruby is saying to you, "The hash h doesn't have the key k. What would you like it's value to be? Now consider the following:
h7 = Hash.new { |h,k| h[k]=7 }
hs = Hash.new { |h,k| h[k]='cat' }
ha = Hash.new { |h,k| h[k]=[] }
hh = Hash.new { |h,k| h[k]={} }
h7[:a] += 3 #=> 10
hs[:b] << 'nip' #=> "catnip"
ha[:c] << 4 << 6 #=> [4, 6]
ha[:d] << 7 #=> [7]
ha #=> {:c=>[4, 6], :d=>[7]}
hh[:k].merge({b: 4}) #=> {:b=>4}
hh #=> {}
hh[:k].merge!({b: 4} ) #=> {:b=>4}
hh #=> {:k=>{:b=>4}}
Notice that you cannot write ha = Hash.new { |h,k| [] } (or equivalently, ha = Hash.new { [] }) and expect h[k] => [] to be added to the hash. You can do whatever you like within the block; you are neither required nor limited to specifying a value for the key. In effect, within the block Ruby is actually saying, "A key that is not in the hash has been referenced without a value. I'm giving you that reference and also a reference to the hash. That will allow you to add that key with a value to the hash, if that's what you want to do, but what you do in this block is entirely your business."
The default values for the hashes h7, hs, ha and hh are respectively the number 7 (though it would be easier to simply enter 7 as An argument), an empty string, an empty array or an empty hash. Probably the last two are the most common use of Hash#new with a block, as in:
array = [[:a, 1], [:b, 3], [:a, 4], [:b, 6]]
array.each_with_object(Hash.new {|h,k| h[k] = []}) { |(k,v),h| h[k] << v }
#=> {:a=>[1, 4], :b=>[3, 6]}
That's really about all there is to the last form of Hash#new.

Ruby Hash.new weirdness [duplicate]

This question already has answers here:
Closed 10 years ago.
Possible Duplicate:
Strange ruby behavior when using Hash.new([])
This is a simple one, as I'm lost for words.
Why is this happening:
1.9.3-p194 :001 > h = Hash.new([])
=> {}
1.9.3-p194 :002 > h[:key1] << "Ruby"
=> ["Ruby"]
1.9.3-p194 :003 > h
=> {}
1.9.3-p194 :004 > h.keys
=> []
1.9.3-p194 :005 > h[:key1]
=> ["Ruby"]
When you create a hash like this:
h = Hash.new([])
it means, whenever the hash is accessed with a key that has not been defined yet, its going to return:
[]
Now when you do :
h[:key1] << "Ruby"
h[:key1] has returned [] , to which "Ruby" got pushed, resulting in ["Ruby"], as output, as that is the last object returned. That has also got set as the default value to return when 'h' is accessed with an undefined key.
Hence, when you do :
h[:key1] or h[:key2] or h[:whatever]
You will get
"Ruby"
as output.
Hope this helps.
Look at the documentation of Hash.new
new → new_hash
new(obj) → new_hash
new {|hash, key| block } → new_hash
If this hash is subsequently accessed by a key that doesn’t correspond to a hash entry, the value returned depends on the style of new used to create the hash.
In the first form, the access returns nil.
If obj is specified, this single object will be used for all default values.
If a block is specified, it will be called with the hash object and the key, and should return the default value. It is the block’s responsibility to store the value in the hash if required.
irb(main):015:0> h[:abc] # ["Ruby"]
So ["Ruby"] is returned as default value instead of nil if key is not found.
This construction Hash.new([]) returns default value but this value is not initialized value of hash. You're trying to work with hash assuming that the default value is a part of hash.
What you need is construction which will initialize the hash at some key:
hash = Hash.new { |h,k| h[k] = [] }
hash[:key1] << "Ruby"
hash #=> {:key1=>["Ruby"]}
You actually did not set the value with h[:keys] << "Ruby". You just add a value for the returned default array of a not found key. So no key is created.
If you write this, it will be okay:
h = Hash.new([])
h[:keys1] = []
h[:keys1] << "Ruby"
I have to admit this tripped me out too when I read your question. I had a look at the docs and it became clear though.
If obj is specified, this single object will be used for all default values.
So what you actually doing is modifying this one single array object that is used for the default values, without ever assigning to the key!
Check it out:
h = Hash.new([])
h[:x] << 'x'
# => ['x']
h
# => {}
h[:y]
# => ['x'] # CRAZY TIMES
So you need to do assignment somehow - h[:x] += ['x'] might be the way to go.

Odd behavior from ruby Hash [duplicate]

This question already has answers here:
Strange, unexpected behavior (disappearing/changing values) when using Hash default value, e.g. Hash.new([])
(4 answers)
Closed 3 years ago.
I'm getting some rather unexpected behavior from hashes in ruby
here's a simplified demonstration of the problem
estdata = ["a","b","c"]
outputHash = Hash.new({:IDs => [], :count => 0})
estdata.each do |x|
outputHash[x][:IDs] << x
outputHash[x][:count] +=1
end
p outputHash # => {}
p outputHash["a"] # => {:count=>3, :IDs=>["a", "b", "c"]}
So firstly, why does the first p output an empty hash when clearly outputHash isn't empty?
And secondly and much more to my frustration and confusion, why does is seem that every key in the has points to a single value (the hash containing the :count and :IDs keys) and how would I get around this?
With Hash.new and a parameter everything will point to the same object.
>> h = Hash.new('hello') #=> {}
>> h[:a] #=> "hello"
>> h[:a].object_id #=> 2152871580
>> h[:b].object_id #=> 2152871580
>> h[:c].object_id #=> 2152871580
What you want is the block form:
>> h = Hash.new { |h,k| h[k] = {} } #=> {}
>> h[:a].object_id #=> 2152698160
>> h[:b].object_id #=> 2152627480

Best way to convert strings to symbols in hash

What's the (fastest/cleanest/straightforward) way to convert all keys in a hash from strings to symbols in Ruby?
This would be handy when parsing YAML.
my_hash = YAML.load_file('yml')
I'd like to be able to use:
my_hash[:key]
Rather than:
my_hash['key']
In Ruby >= 2.5 (docs) you can use:
my_hash.transform_keys(&:to_sym)
Using older Ruby version? Here is a one-liner that will copy the hash into a new one with the keys symbolized:
my_hash = my_hash.inject({}){|memo,(k,v)| memo[k.to_sym] = v; memo}
With Rails you can use:
my_hash.symbolize_keys
my_hash.deep_symbolize_keys
Here's a better method, if you're using Rails:
params.symbolize_keys
The end.
If you're not, just rip off their code (it's also in the link):
myhash.keys.each do |key|
myhash[(key.to_sym rescue key) || key] = myhash.delete(key)
end
For the specific case of YAML in Ruby, if the keys begin with ':', they will be automatically interned as symbols.
require 'yaml'
require 'pp'
yaml_str = "
connections:
- host: host1.example.com
port: 10000
- host: host2.example.com
port: 20000
"
yaml_sym = "
:connections:
- :host: host1.example.com
:port: 10000
- :host: host2.example.com
:port: 20000
"
pp yaml_str = YAML.load(yaml_str)
puts yaml_str.keys.first.class
pp yaml_sym = YAML.load(yaml_sym)
puts yaml_sym.keys.first.class
Output:
# /opt/ruby-1.8.6-p287/bin/ruby ~/test.rb
{"connections"=>
[{"port"=>10000, "host"=>"host1.example.com"},
{"port"=>20000, "host"=>"host2.example.com"}]}
String
{:connections=>
[{:port=>10000, :host=>"host1.example.com"},
{:port=>20000, :host=>"host2.example.com"}]}
Symbol
if you're using Rails, it is much simpler - you can use a HashWithIndifferentAccess and access the keys both as String and as Symbols:
my_hash.with_indifferent_access
see also:
http://api.rubyonrails.org/classes/ActiveSupport/HashWithIndifferentAccess.html
Or you can use the awesome "Facets of Ruby" Gem, which contains a lot of extensions to Ruby Core and Standard Library classes.
require 'facets'
> {'some' => 'thing', 'foo' => 'bar'}.symbolize_keys
=> {:some=>"thing", :foo=>"bar}
see also:
http://rubyworks.github.io/rubyfaux/?doc=http://rubyworks.github.io/facets/docs/facets-2.9.3/core.json#api-class-Hash
Even more terse:
Hash[my_hash.map{|(k,v)| [k.to_sym,v]}]
Since Ruby 2.5.0 you can use Hash#transform_keys or Hash#transform_keys!.
{'a' => 1, 'b' => 2}.transform_keys(&:to_sym) #=> {:a => 1, :b => 2}
http://api.rubyonrails.org/classes/Hash.html#method-i-symbolize_keys
hash = { 'name' => 'Rob', 'age' => '28' }
hash.symbolize_keys
# => { name: "Rob", age: "28" }
If you are using json, and want to use it as a hash, in core Ruby you can do it:
json_obj = JSON.parse(json_str, symbolize_names: true)
symbolize_names: If set to true, returns symbols for the names (keys) in a JSON object. Otherwise strings are returned. Strings are the default.
Doc: Json#parse symbolize_names
Here's a way to deep symbolize an object
def symbolize(obj)
return obj.inject({}){|memo,(k,v)| memo[k.to_sym] = symbolize(v); memo} if obj.is_a? Hash
return obj.inject([]){|memo,v | memo << symbolize(v); memo} if obj.is_a? Array
return obj
end
I really like the Mash gem.
you can do mash['key'], or mash[:key], or mash.key
A modification to #igorsales answer
class Object
def deep_symbolize_keys
return self.inject({}){|memo,(k,v)| memo[k.to_sym] = v.deep_symbolize_keys; memo} if self.is_a? Hash
return self.inject([]){|memo,v | memo << v.deep_symbolize_keys; memo} if self.is_a? Array
return self
end
end
params.symbolize_keys will also work. This method turns hash keys into symbols and returns a new hash.
In Rails you can use:
{'g'=> 'a', 2 => {'v' => 'b', 'x' => { 'z' => 'c'}}}.deep_symbolize_keys!
Converts to:
{:g=>"a", 2=>{:v=>"b", :x=>{:z=>"c"}}}
So many answers here, but the one method rails function is hash.symbolize_keys
This is my one liner for nested hashes
def symbolize_keys(hash)
hash.each_with_object({}) { |(k, v), h| h[k.to_sym] = v.is_a?(Hash) ? symbolize_keys(v) : v }
end
In case the reason you need to do this is because your data originally came from JSON, you could skip any of this parsing by just passing in the :symbolize_names option upon ingesting JSON.
No Rails required and works with Ruby >1.9
JSON.parse(my_json, :symbolize_names => true)
You could be lazy, and wrap it in a lambda:
my_hash = YAML.load_file('yml')
my_lamb = lambda { |key| my_hash[key.to_s] }
my_lamb[:a] == my_hash['a'] #=> true
But this would only work for reading from the hash - not writing.
To do that, you could use Hash#merge
my_hash = Hash.new { |h,k| h[k] = h[k.to_s] }.merge(YAML.load_file('yml'))
The init block will convert the keys one time on demand, though if you update the value for the string version of the key after accessing the symbol version, the symbol version won't be updated.
irb> x = { 'a' => 1, 'b' => 2 }
#=> {"a"=>1, "b"=>2}
irb> y = Hash.new { |h,k| h[k] = h[k.to_s] }.merge(x)
#=> {"a"=>1, "b"=>2}
irb> y[:a] # the key :a doesn't exist for y, so the init block is called
#=> 1
irb> y
#=> {"a"=>1, :a=>1, "b"=>2}
irb> y[:a] # the key :a now exists for y, so the init block is isn't called
#=> 1
irb> y['a'] = 3
#=> 3
irb> y
#=> {"a"=>3, :a=>1, "b"=>2}
You could also have the init block not update the hash, which would protect you from that kind of error, but you'd still be vulnerable to the opposite - updating the symbol version wouldn't update the string version:
irb> q = { 'c' => 4, 'd' => 5 }
#=> {"c"=>4, "d"=>5}
irb> r = Hash.new { |h,k| h[k.to_s] }.merge(q)
#=> {"c"=>4, "d"=>5}
irb> r[:c] # init block is called
#=> 4
irb> r
#=> {"c"=>4, "d"=>5}
irb> r[:c] # init block is called again, since this key still isn't in r
#=> 4
irb> r[:c] = 7
#=> 7
irb> r
#=> {:c=>7, "c"=>4, "d"=>5}
So the thing to be careful of with these is switching between the two key forms. Stick with one.
Would something like the following work?
new_hash = Hash.new
my_hash.each { |k, v| new_hash[k.to_sym] = v }
It'll copy the hash, but you won't care about that most of the time. There's probably a way to do it without copying all the data.
a shorter one-liner fwiw:
my_hash.inject({}){|h,(k,v)| h.merge({ k.to_sym => v}) }
How about this:
my_hash = HashWithIndifferentAccess.new(YAML.load_file('yml'))
# my_hash['key'] => "val"
# my_hash[:key] => "val"
This is for people who uses mruby and do not have any symbolize_keys method defined:
class Hash
def symbolize_keys!
self.keys.each do |k|
if self[k].is_a? Hash
self[k].symbolize_keys!
end
if k.is_a? String
raise RuntimeError, "Symbolizing key '#{k}' means overwrite some data (key :#{k} exists)" if self[k.to_sym]
self[k.to_sym] = self[k]
self.delete(k)
end
end
return self
end
end
The method:
symbolizes only keys that are String
if symbolize a string means to lose some informations (overwrite part of hash) raise a RuntimeError
symbolize also recursively contained hashes
return the symbolized hash
works in place!
The array we want to change.
strings = ["HTML", "CSS", "JavaScript", "Python", "Ruby"]
Make a new variable as an empty array so we can ".push" the symbols in.
symbols = [ ]
Here's where we define a method with a block.
strings.each {|x| symbols.push(x.intern)}
End of code.
So this is probably the most straightforward way to convert strings to symbols in your array(s) in Ruby. Make an array of strings then make a new variable and set the variable to an empty array. Then select each element in the first array you created with the ".each" method. Then use a block code to ".push" all of the elements in your new array and use ".intern or .to_sym" to convert all the elements to symbols.
Symbols are faster because they save more memory within your code and you can only use them once. Symbols are most commonly used for keys in hash which is great. I'm the not the best ruby programmer but this form of code helped me a lot.If anyone knows a better way please share and you can use this method for hash too!
If you would like vanilla ruby solution and as me do not have access to ActiveSupport here is deep symbolize solution (very similar to previous ones)
def deep_convert(element)
return element.collect { |e| deep_convert(e) } if element.is_a?(Array)
return element.inject({}) { |sh,(k,v)| sh[k.to_sym] = deep_convert(v); sh } if element.is_a?(Hash)
element
end
Starting on Psych 3.0 you can add the symbolize_names: option
Psych.load("---\n foo: bar")
# => {"foo"=>"bar"}
Psych.load("---\n foo: bar", symbolize_names: true)
# => {:foo=>"bar"}
Note: if you have a lower Psych version than 3.0 symbolize_names: will be silently ignored.
My Ubuntu 18.04 includes it out of the box with ruby 2.5.1p57
ruby-1.9.2-p180 :001 > h = {'aaa' => 1, 'bbb' => 2}
=> {"aaa"=>1, "bbb"=>2}
ruby-1.9.2-p180 :002 > Hash[h.map{|a| [a.first.to_sym, a.last]}]
=> {:aaa=>1, :bbb=>2}
This is not exactly a one-liner, but it turns all string keys into symbols, also the nested ones:
def recursive_symbolize_keys(my_hash)
case my_hash
when Hash
Hash[
my_hash.map do |key, value|
[ key.respond_to?(:to_sym) ? key.to_sym : key, recursive_symbolize_keys(value) ]
end
]
when Enumerable
my_hash.map { |value| recursive_symbolize_keys(value) }
else
my_hash
end
end
I like this one-liner, when I'm not using Rails, because then I don't have to make a second hash and hold two sets of data while I'm processing it:
my_hash = { "a" => 1, "b" => "string", "c" => true }
my_hash.keys.each { |key| my_hash[key.to_sym] = my_hash.delete(key) }
my_hash
=> {:a=>1, :b=>"string", :c=>true}
Hash#delete returns the value of the deleted key
Facets' Hash#deep_rekey is also a good option, especially:
if you find use for other sugar from facets in your project,
if you prefer code readability over cryptical one-liners.
Sample:
require 'facets/hash/deep_rekey'
my_hash = YAML.load_file('yml').deep_rekey
In ruby I find this to be the most simple and easy to understand way to turn string keys in hashes to symbols :
my_hash.keys.each { |key| my_hash[key.to_sym] = my_hash.delete(key)}
For each key in the hash we call delete on it which removes it from the hash (also delete returns the value associated with the key that was deleted) and we immediately set this equal to the symbolized key.
Similar to previous solutions but written a bit differently.
This allows for a hash that is nested and/or has arrays.
Get conversion of keys to a string as a bonus.
Code does not mutate the hash been passed in.
module HashUtils
def symbolize_keys(hash)
transformer_function = ->(key) { key.to_sym }
transform_keys(hash, transformer_function)
end
def stringify_keys(hash)
transformer_function = ->(key) { key.to_s }
transform_keys(hash, transformer_function)
end
def transform_keys(obj, transformer_function)
case obj
when Array
obj.map{|value| transform_keys(value, transformer_function)}
when Hash
obj.each_with_object({}) do |(key, value), hash|
hash[transformer_function.call(key)] = transform_keys(value, transformer_function)
end
else
obj
end
end
end

Resources