[Note: parts of this message were removed to make it a legal post.]
I'm trying to merge to hashes, one using symbols as keys (the defined
default values for my class) and the other using strings as keys
(taken from the params hash).
default = { :name => "Joe", :age => 50 }
params = { "name" => "Bill" }
new_hash = default.merge(params)
What's the Ruby way to handle this so that it overwrites :name with
"name"? Do I need to implement a stringify_keys or symbolize_keys
method like in Rails? I'd like to avoid using strings as the keys in
my default hash.
Any help greatly appreciated.
Stu
I decided that I wasn't happy with the tests, it should be able to access
the same object through either a string or a symbol (previously it just
turned everything into a symbol, then if you tried to access that object w/
the string, it would not find it).
So I overrode [] and has_key? also, and changed some of the tests.
Here is the updated version
# file: symbolize_keys.rb
module SymbolizeKeys
# converts any current string keys to symbol keys
def self.extended(hash)
hash.each do |key,value|
if key.is_a?(String)
hash.delete key
hash[key] = value #through overridden []=
end
end
end
#considers string keys and symbol keys to be the same
def [](key)
key = convert_key(key)
super(key)
end
#considers string keys and symbol keys to be the same
def has_key?(key)
key = convert_key(key)
super(key)
end
# assigns a new key/value pair
# converts they key to a symbol if it is a string
def []=(*args)
args[0] = convert_key(args[0])
super
end
# returns new hash which is the merge of self and other hashes
# the returned hash will also be extended by SymbolizeKeys
def merge(*other_hashes , &resolution_proc )
merged = Hash.new.extend SymbolizeKeys
merged.merge! self , *other_hashes , &resolution_proc
end
# merges the other hashes into self
# if a proc is submitted , it's return will be the value for the key
def merge!( *other_hashes , &resolution_proc )
# default resolution: value of the other hash
resolution_proc ||= proc{ |key,oldval,newval| newval }
# merge each hash into self
other_hashes.each do |hash|
hash.each{ |k,v|
# assign new k/v into self, resolving conflicts with resolution_proc
self[k] = self.has_key?(k) ? resolution_proc[k.to_sym,self[k],v] : v
}
end
self
end
private
def convert_key(key)
key.is_a?(String) ? key.to_sym : key
end
end
--------------------------------------------------
# file: symbolize_keys_test.rb
require 'test/unit'
require 'symbolize_keys'
# this method was written by Gregory Brown
# and comes from
http://github.com/sandal/rbp/blob/7...e2432f0db7cb8/testing/test_unit_extensions.rb
module Test::Unit
# Used to fix a minor minitest/unit incompatibility in flexmock
AssertionFailedError = Class.new(StandardError)
class TestCase
def self.must(name, &block)
test_name = "test_#{name.gsub(/\s+/,'_')}".to_sym
defined = instance_method(test_name) rescue false
raise "#{test_name} is already defined in #{self}" if defined
if block_given?
define_method(test_name, &block)
else
define_method(test_name) do
flunk "No implementation provided for #{name}"
end
end
end
end
end
class ExtendingWithSymbolizeKeysTest < Test::Unit::TestCase
def setup
@default = {
:age => 50 ,
'initially a string' => 51 ,
/neither string nor symbol/ => 52 ,
}
@default.extend SymbolizeKeys
end
must "convert string keys to symbols when extended" do
assert_equal @default[:'initially a string'] , 51
end
must "sym/str keys can access through either, but only one key" do
assert_equal @default[ 'initially a string'] , 51
assert_equal @default[:'initially a string'] , 51
assert_equal @default[:'initially a string'] , @default['initially a
string']
assert_equal @default.size , 3
end
must "leave symbol keys as symbols" do
assert_equal @default[:age] , 50
end
must 'leave non symbols / strings as they are' do
assert_equal @default[/neither string nor symbol/] , 52
end
end
class SettingKeysWithSymbolizeKeysTest < Test::Unit::TestCase
def setup
@default = Hash.new.extend SymbolizeKeys
end
must "enable access to strings through symbols" do
@default['foo'] = 'bar'
assert_equal @default[:foo] , 'bar'
assert_same @default[:foo] , @default['foo']
end
must "enable access to symbols through strings" do
@default[:foo] = 'bar'
assert_equal @default['foo'] , 'bar'
assert_same @default[:foo] , @default['foo']
end
must 'leave non symbols / strings as they are' do
@default[/foo/] = :bar
assert_equal @default[/foo/] , :bar
end
end
class MergingWithSymbolizeKeysTest < Test::Unit::TestCase
def setup
@default = {
:name => 'Joe' ,
:age => 50 ,
'initially a string' => 51 ,
/neither string nor symbol/ => 52 ,
:'from default' => :default ,
}
@params1 = {
:name => 'Bill' ,
'alias' => 'Billy' ,
:'from params1' =>
arams1 ,
}
@params2 = {
'name' => 'Sam' ,
'alias' => 'Sammy' ,
12 => 'favourite number' ,
:'from params2' =>
arams2 ,
}
@default.extend SymbolizeKeys
end
must "retain new keys for merge" do
merged = @default.merge(@params2)
assert_equal merged[12] , 'favourite number'
end
must "retain new keys for merge!" do
@default.merge!(@params2)
assert_equal @default[12] , 'favourite number'
end
must "replace current values with new values for merge" do
merged = @default.merge(@params1)
assert_equal merged[:name] , 'Bill'
end
must "replace current values with new values for merge!" do
@default.merge!(@params1)
assert_equal @default[:name] , 'Bill'
end
must "not change original hash for merge" do
@default.merge(@params1)
assert_equal @default[:name] , 'Joe'
end
must "receive [key,oldval,newval] as params to block" do
h1 = {:no_conflict_1 => 1 , :conflict => 2 }.extend(SymbolizeKeys)
h2 = {:conflict => 3 , :no_conflict_2 => 4}
resolution_proc_params = nil
h1.merge(h2){ |*params| resolution_proc_params = params }
assert_equal resolution_proc_params , [:conflict,2,3]
end
must "only invoke the resolution proc on conflicts" do
conflict_count = 0
conflicts = { :name => false , :alias => false }
@params1.extend(SymbolizeKeys).merge(@params2) do |k,ov,nv|
conflict_count += 1
conflicts[k] = true
end
assert_equal conflict_count , 2
assert conflicts[:name]
assert conflicts[:alias]
end
must "replace resolve conflicts with block for merge" do
merged = @default.merge(@params1){ |key,oldval,newval| oldval }
assert_equal merged[:name] , 'Joe'
merged = @default.merge(@params1){ |key,oldval,newval| newval }
assert_equal merged[:name] , 'Bill'
end
must "replace resolve conflicts with block for merge!" do
@default.merge!(@params1){ |key,oldval,newval| oldval }
assert_equal @default[:name] , 'Joe'
@default.merge!(@params1){ |key,oldval,newval| newval }
assert_equal @default[:name] , 'Bill'
end
must "convert string keys to symbols for merge" do
unique_keys = @default.keys.map{|k|k.to_s.to_sym} |
@params1.keys.map{|k|k.to_s.to_sym}
merged = @default.merge(@params1)
assert_equal merged['alias'] , 'Billy'
assert_equal merged[ :alias] , 'Billy'
assert_equal merged.size , unique_keys.size
end
must "convert string keys to symbols for merge!" do
unique_keys = @default.keys.map{|k|k.to_s.to_sym} |
@params1.keys.map{|k|k.to_s.to_sym}
@default.merge!(@params1)
assert_equal @default['alias'] , 'Billy'
assert_equal @default[ :alias] , 'Billy'
assert_equal @default.size , unique_keys.size
end
must "merge with multiple hashes" do
merged = @default.merge(@params1,@params2)
assert_equal merged[:'from default'] , :default
assert_equal merged[:'from params1'] ,
arams1
assert_equal merged[:'from params2'] ,
arams2
end
must "merge! with multiple hashes" do
@default.merge!(@params1,@params2)
assert_equal @default[:'from default'] , :default
assert_equal @default[:'from params1'] ,
arams1
assert_equal @default[:'from params2'] ,
arams2
end
must "return object that is extended with SymbolizeKeys, for merge" do
merged = @default.merge(@params1)
assert_kind_of SymbolizeKeys , merged
end
must "not modify original hash, for merge" do
original = @default.dup
@default.merge(@params1,@params2)
assert_equal original , @default
end
end