UPDATE: A more comprehensive method of comparing arrays is covered in this updated article.

When you don’t care what order items in an array are in, don’t pretend you do.

Say you’ve got a list of expected results, and some rows returned by the database:

expected_words = [ 'some', 'known', 'items' ]
found_words = Words.find( :all, :conditions => [ 'name IN (?)', expected_words ] ).map { |w| w.name }

Instead of asserting:

assert_equal expected_words, found_words

Consider:

assert_same_elements expected_words, found_words

To achieve this, I follow the pattern presented by Jay Fields in Convention Is Important, extending a core object and placing the extension in central location. In my case, this location is lib/core_extensions/array/same_elements.rb

class Array
  # Ask an Array whether it shares the same elements with another Array, irrespective of order
  # Options
  # :allow_duplicates
  #   If set to true arrays with the same elements, but differing numbers of those elements
  #   are treated as the same.
  #   Examples
  #     [ :a ].same_elements?( [ :a, :a ] ) => false
  #     [ :a ].same_elements?( [ :a, :a ], :allow_duplicates => true) => true
  def same_elements? another_array, options = {}
    raise ArgumentError, "#{another_array.inspect} was expected to be an Array" unless another_array.kind_of?(Array)
    s = self
    a = another_array
    if options[:allow_duplicates]
      s = s.uniq
      a = a.uniq
    end
 
    return element_counts(s) == element_counts(a)
  end
 
  private
    def element_counts obj
      result = []
      obj.uniq.map { |e|
        [ e, obj.inject(0) { |i, e2| i + (e == e2 ? 1 : 0 ) } ]
      }.each { |p| result << p.first; result << p.last }
      Hash[ *result ]
    end
end

This extensions allows us to ask an array about its similarity to another array.

The most interesting part of this method is the element_counts, which returns a hash whose keys are the unique elements of the array and whose values are the number of instances of each unique value in the original array.

In order to be able to use this in your tests, simply add this small helper method to test/test_helper.rb

# Assert two enumerables have the same elements, irrespective of order
def assert_same_elements enum1, enum2, *args
  message = args.last.kind_of?(String) ? args.pop : "Expected Arrays to have same elements"
  options = args.last.kind_of?(Hash) ? args.pop : {}
  assert_block(build_message(message, "<?> expected to have the same elements as \n<?>.\n", enum2, enum1)) { enum1.same_elements?(enum2, options) }
end

Bonus: Tests

require File.dirname(__FILE__) + '/../unit_test_helper'
 
class ArraySameElementsTest < Test::Unit::TestCase
  def test_same_elements_with_identical_arrays
    assert_equal true, [ :a ].same_elements?( [ :a ] )
  end
 
  def test_same_elements_with_no_shared_items
    assert_equal false, [ :a ].same_elements?( [ :b ] )
  end
 
  def test_same_elements_with_different_ordering
    assert_equal true, [ :a, :b ].same_elements?( [ :b, :a ] )
  end
 
  def test_same_elements_should_not_allow_duplicates
    assert_equal false, [ :a ].same_elements?( [ :a, :a ] ), 'Should not allow duplicates in other array.'
    assert_equal false, [ :a, :a ].same_elements?( [ :a ] ), 'Should not allow duplicates in self.'
    assert_equal false, [ :a, :b, :b ].same_elements?( [ :a, :a, :b ] ), 'Should not allow different duplicates in self and other.'
    assert_equal true, [ :a, :b, :b ].same_elements?( [ :a, :b, :b ] ), 'Should allow duplicates present in both self and other.'
  end
 
  def test_same_elements_should_allow_duplicates_if_allow_duplicates
    assert_equal true, [ :a ].same_elements?( [ :a, :a ], :allow_duplicates => true )
    assert_equal true, [ :a, :a ].same_elements?( [ :a ], :allow_duplicates => true )
    assert_equal true, [ :a, :b, :b ].same_elements?( [ :a, :a, :b ], :allow_duplicates => true )
  end
 
  def test_same_elements_should_not_allow_different_elements_if_allow_duplicates
    assert_equal false, [ :a, :b ].same_elements?( [ :a, :a, :b, :c ], :allow_duplicates => true )
  end
 
  def test_same_elements_should_raise_argument_error_if_provided_argument_is_not_an_array
    assert_raise_with_error_message ArgumentError, :error_message => / was expected to be an Array$/ do
      [ ].same_elements? nil
    end
  end
end

Leave a Comment

Enclose code in <code lang="ruby"></code> if you care.
Preview your comment using the button below.