23

How can I tell if if a Ruby hash is a subset of (or includes) another hash?

For example:

hash = {a: 1, b: 2, c: 3, d: 4, e: 5, f: 6, g: 7}
hash.include_hash?({})            # true
hash.include_hash?({f: 6, c: 3})  # true
hash.include_hash?({f: 6, c: 1})  # false
ma11hew28
  • 121,420
  • 116
  • 450
  • 651
  • 3
    Matt, you seem to have been bestowed with an [embarrassment of riches](http://dictionary.cambridge.org/dictionary/british/an-embarrassment-of-riches). (Link for those for whom English is a second language.) Getting four out of four quality answers on SO is pretty rare. – Cary Swoveland Apr 17 '14 at 18:50

5 Answers5

24

Since Ruby 2.3 you can also do the following to check if this is a subset

hash = {a: 1, b: 2, c: 3, d: 4, e: 5, f: 6, g: 7}
{} <= hash           # true
{f: 6, c: 3} <= hash # true
{f: 6, c: 1} <= hash # false
Pieter
  • 561
  • 6
  • 6
  • does this work with nested hashes? Otherwise it's definitely a concise answer. –  Jul 04 '16 at 20:12
  • 1
    @nus it does work with nested hashes. However, the nested hash must equal the other, so it can't be just a subset: `{a: {x:1, y:2}} <= {a: {x:1, y:2}, b:3 } # true` `{a: {x:1}} <= {a: {x:1, y:2}, b:3 } # false` – Pieter Jul 06 '16 at 08:59
  • Also works the other way around: `hash >= {f: 6, c: 3}` – Simon Perepelitsa Feb 27 '17 at 15:32
16

The solution that came into my mind use Hash#merge method:

class Hash
  def include_hash?(hash)
    merge(hash) == self
  end
end

hash = {a: 1, b: 2, c: 3, d: 4, e: 5, f: 6, g: 7}
hash.include_hash?({})
# => true
hash.include_hash?(f: 6, c:3)
# => true
hash.include_hash?(f: 6, c:1)
# => false
Marek Lipka
  • 50,622
  • 7
  • 87
  • 91
  • 2
    And for nested hash you can use `active support`'s [deep_merge](http://apidock.com/rails/Hash/deep_merge) – Musaffa Mar 04 '16 at 08:09
  • This is very elegant. I call mine `superset?` though, and I made `subset?` as well. –  Jul 04 '16 at 20:15
  • 1
    If you're looking to build assertions that test against nested hash values in RSpec, you can use Rspec's `includes` matcher. See http://stackoverflow.com/a/29248139/590767. Using `include_hash?` in tests would only print out true or false and is less informative than the `includes` approach. – fatuhoku Apr 11 '17 at 17:30
8

Array difference seems easiest:

class Hash
  def include_hash?(h)
    (h.to_a - to_a).empty?
  end
end

h = {a: 1, b: 2}
h.include_hash?({b: 2}) #=> true
h.include_hash?({b: 3}) #=> false
Cary Swoveland
  • 106,649
  • 6
  • 63
  • 100
  • I understand `(h.to_a - to_a)`.. But for better readbility `(h.to_a - self.to_a)`... IMO – Arup Rakshit Apr 17 '14 at 15:13
  • 3
    @Arup, I respectfully disagree, though I know your view is shared by many others. Imo, `self.to_a` suggests that `self` is needed here (as it would be if it were followed by an accessor or `class`). I think it is always best to avoid using `self` when it is not required. If a reader is confused by `to_a` without `self`, they will quickly figure it out, and learn something in the bargain. – Cary Swoveland Apr 17 '14 at 15:36
7

You could convert the hashes to sets and than perform the check using the methods subset? and superset? (or their respective aliases <= and >=):

require 'set'

hash.to_set.superset?({}.to_set)
# => true

hash.to_set >= {a: 1}.to_set
# => true

{a: 2}.to_set <= hash.to_set 
# => false

Update: a benchmark of the proposed solutions:

require 'fruity'
require 'set'

hash = ('aa'..'zz').zip('aa'..'zz').to_h
# {"aa"=>"aa", "ab"=>"ab", ...
find = ('aa'..'zz').zip('aa'..'zz').select { |k, _| k[0] == k[1] }.to_h 
# {"aa"=>"aa", "bb"=>"bb", ...

compare(
  toro2k:        -> { hash.to_set >= find.to_set },
  MarekLipka:    -> { hash.merge(find) == hash },
  CarySwoveland: -> { (find.to_a - hash.to_a).empty? },
  ArupRakshit:   -> { arr = hash.to_a; find.all? { |pair| arr.include?(pair) } }
)

Result:

Running each test 2 times. Test will take about 1 second.
MarekLipka is faster than toro2k by 3x ± 0.1
toro2k is faster than CarySwoveland by 39.99999999999999% ± 10.0%
CarySwoveland is faster than ArupRakshit by 1.9x ± 0.1
toro2k
  • 19,020
  • 7
  • 64
  • 71
  • Thanks! Based on `Set#superset?`, I should call the method `Hash#superset?` instead of `Hash#include_hash?`. – ma11hew28 Apr 17 '14 at 14:42
  • Probably `superset?` would be a better name, however I think it's just a matter of taste. – toro2k Apr 17 '14 at 14:43
  • 2
    Wow I'd just like to say thanks for introducing me to the [fruity gem](https://github.com/marcandre/fruity)! That's awesome. – Nick Veys Apr 17 '14 at 15:19
  • Good solutions, toro, and thanks for the fruity benchmarks. (I'd never heard of fruity, and hadn't noticed the require until I read @Nick's comment). – Cary Swoveland Apr 17 '14 at 15:54
6

You can do :

def include_hash?(hash,subset_hash)
   arr = hash.to_a
   subset_hash.all? { |pair| arr.include?(pair) }
end
Arup Rakshit
  • 116,827
  • 30
  • 260
  • 317