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

9

Array#extract_if

923 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

  • Moi-medium_normal
    oelmekki

    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 ?

  • Blank-mugshot
    deradon

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

  • Blank-mugshot
    charly

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

  • Blank-mugshot
    hamin

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

  • Blank-mugshot
    charly

    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 !

  • Blank-mugshot
    kinvoki

    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

  • Blank-mugshot
    charly

    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
    thehappycoder

    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?

  • Blank-mugshot
    otaviocardoso

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

  • Blank-mugshot
    otaviocardoso

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

  • Blank-mugshot
    charly

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

Add a comment