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