Where developers come to connect, share, build and be inspired.

0

Array#extract_if

1116 views


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(" - ") %>

Comments

  • User-avatar

    Thanks for this, Charles. To achieve the same thing as your example, I would typically have used two #selects - one for wikipedia links, the other for non wp links. So #extract_if certainly provides both legibility and performances.

    Maybe you should name it #extract_if!, as this is a destructive operation ?

  • C28a3b5af4b4431542240f1036ef1e89

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

  • 3fbe81f6942a6edb382054b2491837f8

    @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... )

  • 684e59a0e53725d2a8291318a42e7ee4

    "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/ }

  • 3fbe81f6942a6edb382054b2491837f8

    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 !

  • Fd1a879445f337144bc1f2a5ebd8402b

    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

  • 3fbe81f6942a6edb382054b2491837f8

    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...

  • Img_0739_2

    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?

  • Edd43268d5a70a980473bd2fa8fa52a1

    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!"

  • Edd43268d5a70a980473bd2fa8fa52a1

    @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.

  • 3fbe81f6942a6edb382054b2491837f8

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

Add a comment