From: Phrogz Date: 2007-12-19T09:05:11+09:00 Subject: Hash#collate I wanted a method like Hash#update, but that preserved the values from both the original and argument Hash. A little searching failed to find it. (I did find that someone somewhere wrote a Hash#collate that's in my ri docs, but who knows where it came from. Its description appears not to do at all what I wanted, anyhow.) So, I wrote my own. Comments welcome. Efficiency patches particularly welcome. Under a different name, perhaps Trans might consider it for inclusion in Facets. class Hash # Merge the values of this hash with those from another, setting all values # to be arrays representing the values from both hashes. # { :a=>1, :b=>2 }.collate :a=>3, :b=>4, :c=>5 # #=> { :a=>[1,3], :b=>[2,4], :c=>[5] } # # The 'uniq' option allows you to ensure all values are unique: # { :a=>1, :b=>2 }.collate( { :a=>1, :b=>3 }, :uniq=>true ) # #=> { :a=>[1], :b=>[2,3] } # # By default, array values in either side are merged: # foo = { :a=>[1,2], :b=>[3] } # bar = { :a=>[4,5], :c=>[6,7] } # foo.collate( bar ) # #=> { :a=>[1,2,4,5], :b=>[3], :c=>[6,7] } # # Use the 'preserve_arrays' option to prevent them from being merged: # foo = { :a=>[1,2], :b=>[3] } # bar = { :a=>[4,5], :c=>[6,7] } # foo.collate( bar, :preserve_arrays=>true ) # #=> { :a=>[[1,2],[4,5]], :b=>[[3]], :c=>[[6,7]] } # # Note that, as shown above, preserving arrays will cause array values # to be wrapped up in another array. def collate( other_hash, options={} ) dup.collate!( other_hash, options ) end # The same as #collate, but modifies the receiver in place. def collate!( other_hash, options={} ) # Prepare, ensuring every existing key is already an Array each{ |key, value| if value.is_a?( Array ) && !options[ :preserve_arrays ] self[key] = value else self[key] = [ value ] end } # Collate with values from other_hash other_hash.each{ |key, value| if self[ key ] if value.is_a?( Array ) && !options[ :preserve_arrays ] self[ key ].concat( value ) else self[ key ] << value end elsif value.is_a?( Array ) && !options[ :preserve_arrays ] self[ key ] = value else self[ key ] = [ value ] end } each{ |key, value| value.uniq! } if options[ :uniq ] self end end if __FILE__ == $0 require 'test/unit' class TestHashCollation < Test::Unit::TestCase def setup $a = { :a=>1, :b=>2, :z=>26, :all=>%w|a b z|, :stuff1=>%w|foo bar|, :whee=>%w|a b| } $b = { :a=>1, :b=>4, :c=>9, :all=>%w|a b c|, :stuff2=>%w|jim jam|, :whee=>%w|a b| } $c = { :a=>1, :b=>8, :c=>27 } end def test1_defaults collated = $a.collate( $b ) assert_equal( 8, collated.keys.length, "There are 7 unique keys" ) assert_equal( [1,1], collated[ :a ] ) assert_equal( [2,4], collated[ :b ] ) assert_equal( [9], collated[ :c ] ) assert_equal( [26], collated[ :z ] ) assert_equal( %w|a b z a b c|, collated[ :all ], "Arrays are merged by default." ) assert_equal( %w|foo bar|, collated[ :stuff1 ] ) assert_equal( %w|jim jam|, collated[ :stuff2 ] ) assert_equal( %w|a b a b|, collated[ :whee ] ) end def test2_uniq collated = $a.collate( $b, :uniq=>true ) assert_equal( 8, collated.keys.length, "There are 7 unique keys" ) assert_equal( [1], collated[ :a ] ) assert_equal( [2,4], collated[ :b ] ) assert_equal( [9], collated[ :c ] ) assert_equal( [26], collated[ :z ] ) assert_equal( %w|a b z c|, collated[ :all ], "Arrays are merged by default." ) assert_equal( %w|foo bar|, collated[ :stuff1 ] ) assert_equal( %w|jim jam|, collated[ :stuff2 ] ) assert_equal( %w|a b|, collated[ :whee ] ) end def test3_preserve_arrays collated = $a.collate( $b, :preserve_arrays=>true ) assert_equal( 8, collated.keys.length, "There are 7 unique keys" ) assert_equal( [1,1], collated[ :a ] ) assert_equal( [2,4], collated[ :b ] ) assert_equal( [9], collated[ :c ] ) assert_equal( [26], collated[ :z ] ) assert_equal( [ %w|a b z|, %w|a b c|], collated[ :all ], "Two arrays are not merged." ) assert_equal( [%w|foo bar|], collated[ :stuff1 ], "Arrays unique to one side are wrapped" ) assert_equal( [%w|jim jam|], collated[ :stuff2 ], "Arrays unique to one side are wrapped" ) assert_equal( [%w|a b|, %w|a b|], collated[ :whee ] ) end def test4_preserve_and_uniq collated = $a.collate( $b, :preserve_arrays=>true, :uniq=>true ) assert_equal( 8, collated.keys.length, "There are 7 unique keys" ) assert_equal( [1], collated[ :a ] ) assert_equal( [2,4], collated[ :b ] ) assert_equal( [9], collated[ :c ] ) assert_equal( [26], collated[ :z ] ) assert_equal( [ %w|a b z|, %w|a b c|], collated[ :all ], "Two arrays are not merged." ) assert_equal( [%w|foo bar|], collated[ :stuff1 ], "Arrays unique to one side are wrapped" ) assert_equal( [%w|jim jam|], collated[ :stuff2 ], "Arrays unique to one side are wrapped" ) assert_equal( [%w|a b|], collated[ :whee ], "Preserve arrays + uniq == duplicate arrays are removed" ) end def test5_multi_collate collated = $a.collate( $b ).collate( $c ) assert_equal( [1,1,1], collated[ :a ] ) assert_equal( [2,4,8], collated[ :b ] ) assert_equal( [9,27], collated[ :c ] ) end def test6_multi_collate_with_preserve collated = $a.collate( $b, :preserve_arrays=>1 ).collate( $c ) assert_equal( [1,1,1], collated[ :a ] ) assert_equal( [2,4,8], collated[ :b ] ) assert_equal( [9,27], collated[ :c ] ) collated = $a.collate( $b ).collate( $c, :preserve_arrays=>1 ) assert_equal( [[1,1],1], collated[ :a ] ) assert_equal( [[2,4],8], collated[ :b ] ) assert_equal( [[9],27], collated[ :c ] ) collated = $a.collate( $b, :preserve_arrays=>1 ).collate( $c, :preserve_arrays=>1 ) assert_equal( [[1,1],1], collated[ :a ] ) assert_equal( [[2,4],8], collated[ :b ] ) assert_equal( [[9],27], collated[ :c ] ) end end end