It's early and I feel like I'm forgetting something obvious but is there a method to subtract a array from another and return an array where the exact number of matches is removed.
# subtract operator
%i[a b a b a c] - %i[a c] # => [:b, :b]
# what I'm looking for
%i[a b a b a c] - %i[a c] # => [:b, :a, :b, :a]
Lacking a built in method, here's my implementation. Is there a more efficient way?
def remove lhs, rhs
ret = []
rem = rhs
lhs.each do |val|
idx = rem.index(val)
if idx
rem = rem[0...idx].concat(rem[idx+1..])
else
ret << val
end
end
ret
end
remove(%i[a b a b a c], %i[a c]) # => [:b, :a, :b, :a]
Here are a litany of possible options I could come up with. All options assume there can be no mutation of ary_1
and ary_2
, if mutation of ary_1
or ary_2
is acceptable some of these methods can be simplified further.
Order Independent
Take your original Array
and group the elements. Then we can just remove the elements in the second array from the values in the resulting Hash
ary_1 = %i[a b a b a c]
ary_2 = %i[a c]
ary_1.group_by(&:itself).then do |groups|
groups.tap {|h| ary_2.each {|e| h[e]&.shift } }.values.flatten
end
#=> [:a, :a, :b, :b]
# or for an ary_2 with duplicates we can reduce iteration using
ary_2_counts = ary_2.tally
ary_1.group_by(&:itself).then do |groups|
groups.tap {|h| ary_2_counts.each {|e,c| h[e]&.shift(c) } }.values.flatten
end
#=> [:a, :a, :b, :b]
Tally the counts in each Array
reduce the original tally by the secondary tally and then expand back out into an Array
(inspired by @dawg's post)
t1,t2 = [ary_1,ary_2].map(&:tally)
t2.slice(*t1.keys).merge(t1) {|_,o,n| n - o}.sum([]) {|k,v| [k] * v}
#=> [:a, :a, :b, :b]
#Or
t2.slice(*t1.keys).merge(t1) {|_,o,n| n - o}.flat_map {|k,v| [k] * v}
#=> [:a, :a, :b, :b]
Order Dependent
Duplicate the first Array
and remove the element at a found index for each element contained in the second Array
ary_2.each_with_object(ary_1.dup) {|e, obj| i = obj.index(e) and obj.delete_at(i) }
#=> [:b,:a,:b,:a]
Continually slice the first Array
by selecting the values at indexes other than the index for each element in the second Array
ary_2.reduce(ary_1) {|memo, e| memo.values_at(*(0...memo.length).to_a - [memo.index(e)])}
#=> [:b,:a,:b,:a]
Partition
ary_2_dup = ary_2.dup
removed, remaining = ary_1.partition do |e|
ary_2_dup.delete_at(ary_2_dup.index(e)) rescue false
end
remaining
#=> [:b, :a, :b, :a]
removed
#=> [:a, :c]
We could also create our own version of partitioning by capturing each element removed from the first Array
in another Array
removed, remaining = ary_2.each_with_object([[],ary_1.dup]) do |e,(removed,remainder)|
i = remainder.index(e) and removed << remainder.delete_at(i)
end
remaining
#=> [:b, :a, :b, :a]
removed
#=> [:a, :c]