From: Josh Cheek Date: 2009-11-03T21:47:19+09:00 Subject: Re: Merging hashes using both symbols and strings as keys --000e0cd29a66e91d3a047776e413 Content-Type: text/plain; charset=ISO-8859-1 On Fri, Oct 30, 2009 at 11:10 AM, shenry wrote: > 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) > >> { :name => "Joe", :age => 50, "name" => "Bill } > > 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/7db25d4873042c02eca7232ad18e2432f0db7cb8/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' => :params1 , } @params2 = { 'name' => 'Sam' , 'alias' => 'Sammy' , 12 => 'favourite number' , :'from params2' => :params2 , } @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'] , :params1 assert_equal merged[:'from params2'] , :params2 end must "merge! with multiple hashes" do @default.merge!(@params1,@params2) assert_equal @default[:'from default'] , :default assert_equal @default[:'from params1'] , :params1 assert_equal @default[:'from params2'] , :params2 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 --000e0cd29a66e91d3a047776e413--