1

I am trying to remove odd numbers from an array.

arr = [1, 2, 3, 4, 5, 6, 7, 8, 10]

def remove_odd_nums(arr)
  for x in arr
    if x % 2 == 0
      arr.delete(x)
    end
  end
end

print remove_odd_nums(arr)
# [1, 3, 5, 7, 10]

I can't seem to make this program work. The method works on the numbers except for the last one. What am I doing wrong?

Practical1
  • 789
  • 1
  • 8
  • 26
  • 3
    Title should be `Need help filtering an array` not sorting – bunbun Sep 13 '18 at 01:10
  • I think you should see @bunbun's answer because it is the most "Ruby-like" syntax. But I'd also like to point out that `x % 2 == 0` checks for *even* numbers. Simply changing it to check `x % 2 != 0` should give you the result you're looking for. – rparr Sep 13 '18 at 01:24
  • 1
    Sorry, the x % 2 == 0 snippet was an mistake on my part :) – Practical1 Sep 13 '18 at 01:44

4 Answers4

5

You want to delete odd numbers but your program is deleting even numbers (x % 2 == 0 checks if x is an even number)

METHOD 1:

arr = [1, 2, 3, 4, 5, 6, 7, 8, 10]
arr.delete_if &:odd?

print arr

delete_if iterates by incrementing the index for arr, and deletes an element immediately after evaluating the block &:odd? with respect to the element. In other words, it is going through each element in array, and deleting the element if &:odd? is true.

&:odd?: a lambda function passing in an object to the odd? method, which returns true if the object is an odd number. Further explanations can be found what is the functionality of "&: " operator in ruby?

Note that method 1 actually MODIFIES the original array. For a way to create a new array of non-odd numbers, there is...

METHOD 2:

non_odds = arr.select{|i| not i.odd?}
bunbun
  • 2,595
  • 3
  • 34
  • 52
  • 1
    Adding on to this because @practical1 mentioned that they're a newcomer to Ruby; it might be a good idea to explain the `delete_if &:odd?` syntax or provide a resource they can read up on it. – rparr Sep 13 '18 at 01:20
  • 1
    added more explanation – bunbun Sep 13 '18 at 01:30
  • 1
    thank you for the update. I made a quick edit to update `odd` to `odd?` to avoid any syntax errors since `odd` isn't a valid Ruby method. – rparr Sep 13 '18 at 01:39
  • 1
    `delete_if` _mutates_ an array and while here it might be applicable, the general rule of thumb would be to avoid mutating anything unless the intent is clean. It’s always better to reassign the value returned by non-mutating `Enumerable#reject`. – Aleksei Matiushkin Sep 13 '18 at 07:21
  • 1
    "METHOD 2" can be written more succinctly as `arr.select(&:even)` or `arr.reject(&:odd)`. – Cary Swoveland Oct 26 '18 at 21:20
3

TL;DR: don't modify an array while iterating it.


Let's see what's happening by printing the current value of x and arr inside the loop:

def remove_odd_nums(arr)
  for x in arr
    p x: x, arr: arr # <- debug output
    if x % 2 == 0
      arr.delete(x)
    end
  end
end

remove_odd_nums([1, 2, 3, 4, 5, 6, 7, 8, 10])

Output:

{:x=>1, :arr=>[1, 2, 3, 4, 5, 6, 7, 8, 10]}
{:x=>2, :arr=>[1, 2, 3, 4, 5, 6, 7, 8, 10]}
{:x=>4, :arr=>[1, 3, 4, 5, 6, 7, 8, 10]}
{:x=>6, :arr=>[1, 3, 5, 6, 7, 8, 10]}
{:x=>8, :arr=>[1, 3, 5, 7, 8, 10]}

The first two x values are as expected: 1 and 2. But then it moves on to 4, skipping 3. It also skips 5, 7, and 10. But why?

It's because you are modifying the array while iterating it. Think of the for loop as someone pointing to an element at a specific position. Initially it looks like this:

1 2 3 4 5 6 7 8 10  <- array
^                   <- element

for then moves on to the next element:

1 2 3 4 5 6 7 8 10
  ^

at this point x % 2 == 0 becomes true and 2 is deleted from the array:

1 3 4 5 6 7 8 10
  ^

for isn't aware of this change and simply moves on to the next element:

1 3 4 5 6 7 8 10
    ^

which is why we have unintentionally skipped 3. The same happens for 5 and 7.

When for finally reaches 8:

1 3 5 7 8 10
        ^

it is being deleted:

1 3 5 7 10
        ^

and for stops looping because it seems to have reached the array's end.

Stefan
  • 109,145
  • 14
  • 143
  • 218
2

Hello Practical1 just to clarify why do you want to destroy objects and array?

In case you on want to filter array and only select even numbers , you can try a combination of Array#select and Integer#even? method helpers

    arr = arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    # select all even numbers in an array
    arr.select(&:even?) # shorthand for arr.select {|number| number.even? }

will return even numbers

[0] 2,
[1] 4,
[2] 6,
[3] 8,
[4] 10

source:

Array#select https://apidock.com/ruby/Array/select

Integer#even? https://ruby-doc.org/core-1.8.7/Integer.html

Exwire
  • 81
  • 4
2

Ruby has fabulous methods to modify arrays in place based on the logic in a block.

To arrive at an array with only odd numbers, you can either remove the elements that don't meet a test or keep the number that do meet a test. You can either return a new array or use one of the in place modification methods.

To remove undesired values, use either .reject for a new array or .reject! to modify an existing array in place.

Since we are removing, we would use {|e| e%2!=0} inside the block for odd numbers:

> [1,2,3,4,5,6,7,8,9,10].reject {|e| e%2!=0}
=> [2, 4, 6, 8, 10]                            # new array
> arr = [1, 2, 3, 4, 5, 6, 7, 8, 10]
> arr.reject! {|e| e%2!=0}
=> [2, 4, 6, 8, 10]                            # arr modified in place

Rather than a block, you can also use the odd? logical test for the same result:

> [1,2,3,4,5,6,7,8,9,10].reject &:odd?
=> [2, 4, 6, 8, 10]

Or, you can keep the values desired and other values will not be kept. You would use {|e| e%2==0} inside the block for even values. Or you can use &:even? instead of the block.

You can use .keep_if to return a new array:

> arr
=> [1, 2, 3, 4, 5, 6, 7, 8, 10]
> [1,2,3,4,5,6,7,8,9,10].keep_if {|e| e%2==0}
=> [2, 4, 6, 8, 10]                               # new array. 

Or use .select! to modify in place:

> arr = [1, 2, 3, 4, 5, 6, 7, 8, 10]
=> [1, 2, 3, 4, 5, 6, 7, 8, 10]
> arr.select! {|e| e%2==0}
=> [2, 4, 6, 8, 10]
> arr
=> [2, 4, 6, 8, 10]                                  # arr modified in place
dawg
  • 98,345
  • 23
  • 131
  • 206
  • Why not use the native Integer#even method? – moveson Sep 13 '18 at 19:45
  • For a Ruby noobie, understanding blocks with methods is important IMHO. I was trying to show that a block could be used with the OP's same logic (with the right method) and I showed `&:odd?` in the `.reject` method and I mentioned `&:even?` could replace the block with the `keep_if` method. – dawg Sep 13 '18 at 20:53