Last Updated: October 12, 2018
·
1.44K
· charly

Array#extract_if

A handy Array method that essentially finds and removes an item from an array in one go

class Array
  def extract_if( &block )
    if n = index(&block)
      slice!(n)
    end
  end
end

Use cases are numerous. Here's one : you have a list of random links but you want wikipedia to always appear first.

@links = [ 'random.com/bla', '...', 'wikipedia.com/bla', '...']

# show.html.erb
<%= @links.extract_if {|link| link.matches/wikipedia.com/ } %> - 
<%= @links.join(" - ") %>

10 Responses
Add your response

"A handy Array method that essentially finds and removes an item from an array in one go"

Ruby on its own already has this (Array#reject!) :

@links.reject!{|link| link.match /wikipedia.com/ } </code>

over 1 year ago ·

Array#reject! will return the array stripped out of the selection.

a = [1, 2, 3, 4, 5]
a.reject! {|i| i > 2} #=> [1, 2]

Array#extract_if will return the first item found and return it (like detect & find) but will also slice it out from the array (like pop or unshift)

a = [1, 2, 3, 4, 5]
a.extract_if { |i| i > 2 } #=> 3
a #=> [1, 2, 4, 5]

I looked it up closely, there's no equivalent in Ruby core nor ActiveSupport. Not even the venerable Facets !

over 1 year ago ·

You have a few typos in your example. If this was your goal - "Here's one : you have a list of random links but you want wikipedia to always appear first." , then your example needs to be rewritten like this.

@links = [ 'random.com/bla','...', 'wikipedia.com/bla',' ...', 'random2.com']
@wiki= @links.extract_if {|link| link.match(/wikipedia.com/) }
@links.unshift(@wiki)
@links.join(" - ")

This can be futher optimized of course

over 1 year ago ·

Thanks, that looks better.

However my general use case would be rather treating the extracted item separately from the rest - not putting it back in the array.

But this raises a good question : is there any Array method which puts an item in the first position ? Will investigate...

over 1 year ago ·

Will it be as efficient as #reject! method?

If you really want to delete items from existing array, it makes sense to do it with linked list. I suspect that deleting item from ruby array shifts the other elements to occupy the empty slot?

over 1 year ago ·

I'd expect extract_if to extract all element matched, not just the first.

over 1 year ago ·

In the example, I used to do:

links.sort_by { |link| link.match(/wikipedia.com/) ? 0 : 1 }.join(' - ')

Of course, if I don't care to the order of the links as long as wikipedia appears first.
And I agree with deradon and oelmekki, I would call this method "extract_first!"

over 1 year ago ·

@thehappycoder whenever I find some time to benchmark I'll share it - but reject! doesn't achieve the same thing : see prev comment.

@deradon & @otaviocardoso : agreed #extract_first! is better. But how about a simple #extract! - which I used in the first place ? Also unshift & pop don't have a bang so I wonder if it's truly necessary (naming...)

@otaviocardoso : good solution for putting an item in the first position. I wonder how it compares to extract + unshift, performance wise. Also one reason you'd extract instead of sort is to treat it differently e.g. call the extracted link a generic "wikipedia" instead of the full url - (in my use case the links are actually AR Link objects... )

over 1 year ago ·

@charly : I think #extract! is simpler, but it could make the same confusion that @deradon points, people expect that it is the same that #reject!, extracting all elements matched. And about the bang... Actually, this makes me think why #delete and his "brothers" does not have the bang.

And I agree with you. My solution is just for this "simplest" case, when you don't have to do any different treatment with the extracted link, you don't care about the ordering of the links but the first, etc.

over 1 year ago ·

my previous comment seems to have taken a walk.
otaviocardoso's answer gives you the gist of it.

over 1 year ago ·