Search code examples
ruby

is there a built in method to remove the elements in a ruby array from another array


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]


Solution

  • 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]