A pattern to destructively extract items from an array

Posted on

Problem

I want to efficiently (few intermediate objects) and neatly (few lines and easy to read) filter an array, removing and returning rejected items. So like a call to delete_if except instead of returning the remaining items, returning the deleted items.

Here’s some code that works:

ary = (0..9).to_a
odds = ary.select {|i| i%2==1 }
ary -= odds

or:

ary = (0..9).to_a
(_,ary),(_,odds) = ary.group_by {|i| i.odd? }.sort {|a,b| a[0] ? 1 : -1 }

But I can’t help but think there should be a better way…

Ideally something more like: odds = ary.delete_and_return(&:odd?)

Solution

Array.partition returns an array of two arrays – one where the elements returned true to the condition, and one that returned false.

So, a single liner for your need would be:

odds, ary = (0..9).to_a.partition(&:odd?)
=> [[1, 3, 5, 7, 9], [0, 2, 4, 6, 8]] 

(&:odd?) acts exactly like writing { |x| x.odd? } which will return true if the number is, well, odd…

it is built-in :

ary.delete_if( &:odd? )

EDIT

Sorry, misread your question. You can do this :

deleted = ary.select( &:odd? ).tap{|odd| ary -= odd }

You need #partition:

irb> a = (1..9).to_a
=> [1, 2, 3, 4, 5, 6, 7, 8, 9]
irb> a.partition(&:odd?).tap{ |y, n| a = n }.first
=> [1, 3, 5, 7, 9]
irb> a
=> [2, 4, 6, 8]

Since there is no such method yet in ruby, you can always add your own if that makes sense to your use case (if you find yourself needing that method a lot).

Here is and example implementation:

class Array
  def extract! &block
    extracted = select(&block)
    reject!(&block)
    extracted
  end
end

The code above first selects what you want to return and then destructively removes those items which is what you want to avoid doing manually to improve readability.

Note: the method is called extract! with a bang since it modifies the array itself. If the method wuould not modify the underlying object, it would be equivalent to select, so you might name it select!, or select_and_remove! if you prefer.

Usage:

a = (0..10).to_a

# extract even numbers from our array
p a.extract!(&:even?) # => [0, 2, 4, 6, 8, 10]

# odd numbers are left
p a # => [1, 3, 5, 7, 9]

Leave a Reply

Your email address will not be published. Required fields are marked *