I am trying to compare an array of externally defined Objects. I was hoping I would be able to do a simple .difference
, a function that was introduced in Ruby 2.6.0 but after looking at it: https://ruby-doc.org/core-2.6/Array.html#method-i-difference I'm not sure I can specify a custom comparison.
Okay assuming we have a simple Object Num
# Pretend we don't have access to this, just for reference
class Num
def initialize(val)
@val = val
end
def val
@val
end
end
And I have two arrays, one is a subset of the other. I want to find what the subset is missing. In this following example I want the difference to be the Object with value 3, since it doesn't exist in the subset.
all = [Num.new(1), Num.new(2), Num.new(3)]
subset = [Num.new(1), Num.new(2)]
The default .difference
function compares using .eql?
between the two objects so the difference does not give the expected result:
all.difference(subset)
=> [#<Num:0x00007fcae19e9540 @val=1>, #<Num:0x00007fcae19e9518 @val=2>, #<Num:0x00007fcae19e94f0 @val=3>]
I was able to create my own custom hacky solution to properly give me the values I want:
def custom_difference(all, subset)
diff = all.reject { |all_curr|
subset.find{ |subset_curr|
subset_curr.val == all_curr.val
} != nil
}
end
custom_difference(all, subset)
=> [#<Num:0x00007fcae19e94f0 @val=3>]
But I want to know if there's anyway to utilize the existing .difference
function, I was trying to use like this as well in order to override the way the two objects are compared:
all.difference(subset) { |a, b|
a.val <=> b.val
}
=> [#<Num:0x00007fcae19e9540 @val=1>, #<Num:0x00007fcae19e9518 @val=2>, #<Num:0x00007fcae19e94f0 @val=3>]
But this doesn't do anything to adjust the way the comparison occurs (AFAIK) Am I doing something wrong? Is this just not possible? :'(
If you don't want to add eql?
to the class as described by Aleksei Matiushkin (e.g. if you want to use multiple criteria for different things), there's no way to reuse #difference
. Doing what you were doing is pretty much what you need to do, though with Array#include?
is O(N^2), so I like sticking Set
in there:
Set.new(subset.map(&:val)).then { |s| all.reject { |x| s === x.val } }
# => [#<Num:0x00007febd32330e0 @val=3>]
or, as a new method:
module ArrayWithDifferenceBy
refine Array do
def difference_by(other)
other_set = Set.new(other.map { |x| yield x })
self.reject { |x| other_set.include?(yield x) }
end
end
end
module TestThis
using ArrayWithDifferenceBy
all = [Num.new(1), Num.new(2), Num.new(3)]
subset = [Num.new(1), Num.new(2)]
all.difference_by(subset, &:val)
end
# => [#<Num:0x00007febd32330e0 @val=3>]