How to match hashes that contain arrays ignoring order of array elements?
Asked Answered
A

4

7

I have two hashes containing arrays. In my case, the order of array elements is not important. Is there a simple way to match such hashes in RSpec2?

{ a: [1, 2] }.should == { a: [2, 1] } # how to make it pass?

P.S.

There is a matcher for arrays, that ignores the order.

[1, 2].should =~ [2, 1] # Is there a similar matcher for hashes?

SOLUTION

The solution works for me. Originally suggested by tokland, with fixes.

RSpec::Matchers.define :match_hash do |expected|
  match do |actual|
    matches_hash?(expected, actual) 
  end
end

def matches_hash?(expected, actual) 
  matches_array?(expected.keys, actual.keys) &&
    actual.all? { |k, xs| matches_array?(expected[k], xs) }
end   

def matches_array?(expected, actual)
  return expected == actual unless expected.is_a?(Array) && actual.is_a?(Array)
  RSpec::Matchers::BuiltIn::MatchArray.new(expected).matches? actual
end

To use the matcher:

{a: [1, 2]}.should match_hash({a: [2, 1]})
Aragon answered 6/7, 2012 at 16:50 Comment(0)
L
2

I'd write a custom matcher:

RSpec::Matchers.define :have_equal_sets_as_values do |expected|
  match do |actual|
    same_elements?(actual.keys, expected.keys) && 
      actual.all? { |k, xs| same_elements?(xs, expected[k]) }
  end

  def same_elements?(xs, ys)
    RSpec::Matchers::BuiltIn::MatchArray.new(xs).matches?(ys)
  end
end

describe "some test" do
  it { {a: [1, 2]}.should have_equal_sets_as_values({a: [2, 1]}) }  
end

# 1 example, 0 failures
Lanilaniard answered 6/7, 2012 at 19:31 Comment(2)
Thanks for the idea. The =~ does not work here, need to call RSpec::Matchers::BuiltIn::MatchArray.new(expected).matches? actual. I have added the fix to my question above.Aragon
Got the error uninitialized constant RSpec::Matchers::BuiltIn::MatchArray. Looks like that matcher has been deprecated in recent versions? I'm using rspec-core 3.6.0 with Rails 5. To get around it, I just simplified the same_elements? method to xs.sort == xy.sortHibernal
T
2

[Rspec 3]
I ended up sorting the hashed values (arrays), like so:

hash1.map! {|key, value| [key, value.sort]}.to_h
hash2.map! {|key, value| [key, value.sort]}.to_h
expect(hash1).to match a_hash_including(hash2)

I'm sure it wouldn't perform great with a considerably large array though...

Theophany answered 1/2, 2017 at 8:41 Comment(0)
T
1

== on Hashes does not care about order, {1 => 2, 3=>4} == {3=>4, 1=>2}. However, it is going to check for equality on the values, and of course [2,1] does not equal [1,2]. I don't think that ~= is recursive: [[1,2],[3,4]] may not match with [[4,3],[2,1]]. If it does, you can just write two checks, one for keys and one for values. That would look like this:

hash1.keys.should =~ hash2.keys
hash1.values.should =~ hash2.values

But as I said, that may not work. So possibly, you might want to extend the Hash class to include a custom method, something like:

class Hash
  def match_with_array_values?(other)
    return false unless self.length == other.length
    return false unless self.keys - other.keys == []
    return false unless self.values.flatten-other.values.flatten == []
    return true
  end
end
Twinscrew answered 6/7, 2012 at 19:9 Comment(2)
I know some people like this pattern, but let me note that return false unless a; return false unless b; return true can be written as a simple expression a && b.Lanilaniard
@Lanilaniard In the case of a == b && (c - d) == e && (e - f == g), you are creating a compound complex expression. In this case, I went for readability, not concision.Twinscrew
H
1

you can use sets instead of arrays if the order is not important:

require 'set'
Set.new([1,2]) == Set.new([2,1])
=> true
Horrified answered 6/7, 2012 at 19:33 Comment(1)
Beware that sets will also remove duplicatesTops

© 2022 - 2024 — McMap. All rights reserved.