59
(1..4).collect do |x|
  next if x == 3
  x + 1
end # => [2, 3, nil, 5]
    # desired => [2, 3, 5]

If the condition for next is met, collect puts nil in the array, whereas what I'm trying to do is put no element in the returned array if the condition is met. Is this possible without calling delete_if { |x| x == nil } on the returned array?

My code excerpt is heavily abstracted, so looking for a general solution to the problem.

Andrew Marshall
  • 95,083
  • 20
  • 220
  • 214
  • 1
    There's a real quick, built to purpose solution to this introduced in 2.7: `filter_map`, [more info here](https://stackoverflow.com/questions/5152098/skip-over-iteration-in-enumerablecollect/57323739#answer-57323739). – SRack Aug 02 '19 at 09:35

6 Answers6

79

There is method Enumerable#reject which serves just the purpose:

(1..4).reject{|x| x == 3}.collect{|x| x + 1}

The practice of directly using an output of one method as an input of another is called method chaining and is very common in Ruby.

BTW, map (or collect) is used for direct mapping of input enumerable to the output one. If you need to output different number of elements, chances are that you need another method of Enumerable.

Edit: If you are bothered by the fact that some of the elements are iterated twice, you can use less elegant solution based on inject (or its similar method named each_with_object):

(1..4).each_with_object([]){|x,a| a << x + 1 unless x == 3}
shivam
  • 16,048
  • 3
  • 56
  • 71
Mladen Jablanović
  • 43,461
  • 10
  • 90
  • 113
  • 4
    This is the slowest answer (not by much though), but is certainly the cleanest. I just wish there was a way to do this with `collect`, as I would've expected calling `next` to have returned _absolutely nothing_, rather than "nothing" (aka `nil`). – Andrew Marshall Mar 01 '11 at 23:12
  • 1
    If it's slower than any other approach, Ruby's optimizer could use some work don't you think? – Shannon Dec 27 '12 at 21:30
49

I would simply call .compact on the resultant array, which removes any instances of nil in an array. If you'd like it to modify the existing array (no reason not to), use .compact!:

(1..4).collect do |x|
  next if x == 3
  x
end.compact!
Benson
  • 22,457
  • 2
  • 40
  • 49
  • 15
    I did a quick benchmark, for interest, of the four solutions suggested so far: collect+compact, collect+compact!, reject+collect and building a results array as you go. In MRI 1.9.1, at least, collect+compact! is the speed winner by a narrow margin, with collect+compact and reject+collect pretty close behind. Building the results array is about twice as slow as those. – glenn mcdonald Mar 01 '11 at 14:01
  • @glenn: Would you mind including the following to your benchmark: `(1..4).inject([]){|a,x| x == 3 ? a : a.push(x + 1)}`? Thanks. – Mladen Jablanović Mar 01 '11 at 14:59
  • Sure. That's the slowest one yet, albeit only slightly slower than building the results array. I also added a version tweaked like this: `a.inject([]){|aa,x| aa << x + 1 unless x == 3; aa}`, which was faster than building the array, but still considerably slower than the three 3 fast ways. – glenn mcdonald Mar 01 '11 at 18:07
  • 6
    This would work, but you'd have to worry about cases where the original array had legitimate `nil`s in them. – Andrew Grimm Mar 01 '11 at 22:33
  • @Andrew G. That's true, though in my particular case that would never happen. Good side-effect worth noting though. – Andrew Marshall Mar 01 '11 at 22:53
  • 4
    I dunno about using `map{}.compact!`, b/c it returns `nil` if nothing was skipped. I guess if you can be guaranteed that at least one element will be skipped... – Kache Jul 26 '13 at 23:57
  • You should consider performance before using this. I think it's better just use `each` instead of these fancy magic – Steven Yue Mar 18 '15 at 17:41
  • I like this solution, but the `next` is misleading because it's not really skipping the collect, it's just stopping the execution of the block and adding nil to the array. You could write it more clearly and cleanly like this: `(1..4).collect { |x| x + 1 unless x == 3 }.compact` or even better and more clear in my mind: `(1..4).select {|n| n != 3}.map {|x| x + 1}` – Josh Sep 13 '16 at 04:44
10

In Ruby 2.7+, it’s possible to use filter_map for this exact purpose. From the docs:

Returns an array containing truthy elements returned by the block.

(0..9).filter_map {|i| i * 2 if i.even? }   #=> [0, 4, 8, 12, 16]
{foo: 0, bar: 1, baz: 2}.filter_map {|key, value| key if value.even? }  #=> [:foo, :baz]

For the example in the question: (1..4).filter_map { |x| x + 1 unless x == 3 }.

See this post for comparison with alternative methods, including benchmarks.

Andrew Marshall
  • 95,083
  • 20
  • 220
  • 214
SRack
  • 11,495
  • 5
  • 47
  • 60
  • 1
    Great find :) It also works with `next` as tried by the OP: `numbers.filter_map { |i| next if i.odd?; i * 2 }` – tanius Apr 26 '20 at 22:26
4

just a suggestion, why don't you do it this way:

result = []
(1..4).each do |x|
  next if x == 3
  result << x
end
result # => [1, 2, 4]

in that way you saved another iteration to remove nil elements from the array. hope it helps =)

Staelen
  • 7,691
  • 5
  • 34
  • 30
  • This is like a much more verbose version of Mladen's `reject` example. – Benson Mar 01 '11 at 09:22
  • actually nope, "reject" iterates through the array once, and "collect" iterates through the array once again, so there's two iterations. in the case of "each" it does so only once =) – Staelen Mar 01 '11 at 09:27
  • 4
    This is a reasonable idea, but in Ruby it's often very interesting to do the speed test and see what actually happens. In my quick benchmark (which might or might not match Andrew's real case, of course), building the array this way is actually about twice as slow as any of the other ways. I suspect the issue is that iterating through the array in C is actually a lot faster than appending items at the Ruby << level. – glenn mcdonald Mar 01 '11 at 14:04
  • This is using mutable state, rather than a functional programming style. – Andrew Grimm Mar 01 '11 at 22:35
  • 4
    There's actually no need for `next` here. Simply `result << x unless x == 3` would work and is a bit cleaner. – Andrew Marshall Mar 01 '11 at 23:15
  • 1
    @Staelen I apologize, you are correct. That said, I still think the prettier and faster solutions win this round. – Benson Mar 01 '11 at 23:24
  • @benson: no worries benson =) @andrew: yeah u r right, 1 vote up =) – Staelen Mar 02 '11 at 00:35
  • @Staelen Ideally the interpreter would optimize it down to a single loop. – Shannon Dec 27 '12 at 21:31
0

You could pull the decision-making into a helper method, and use it via Enumerable#reduce:

def potentially_keep(list, i)
  if i === 3
    list
  else
    list.push i
  end
end
# => :potentially_keep

(1..4).reduce([]) { |memo, i| potentially_keep(memo, i) }
# => [1, 2, 4]
alxndr
  • 3,851
  • 3
  • 34
  • 35
0

i would suggest to use:

(1..4).to_a.delete_if {|x| x == 3}

instead of the collect + next statement.

ALoR
  • 4,904
  • 2
  • 23
  • 25
  • The _real_ code is more complicated than the abstracted code in my question, so this wouldn't achieve what I'm looking for, unfortunately. (The collect actually manipulates the value, rather than just returning it) – Andrew Marshall Mar 01 '11 at 08:53