leol
leol

Reputation: 47

Can someone explain why this Ruby next if works in an until loop?

def sorting(arr)
  sorted = false
  until sorted 
    sorted = true
    arr.each_index do |i|
      next if i == arr.length - 1 
      if arr[i] > arr[i + 1]
        arr[i], arr[i+1] = arr[i+1], arr[i]
        sorted = false
      end
    end
  end
  arr
end

p sorting([7, 4, 5, 2, 1, 3]) => [1, 2, 3, 4, 5, 7]

My question is: Why would the above Ruby code work?

First: When we called sorted = true right inside the until loop, that fulfills the condition of the sorted and it should end the loop.

Second: Can someone explain why the process of next if inside the iteration method won't continue to iterate the whole array, when it should have ended after the first iteration, and then call on sorted = true.

I was asked to explain, and I was wondering if anyone can provide a better explanation.

Thank you Ruby Masters!

Upvotes: 2

Views: 633

Answers (1)

Tom
Tom

Reputation: 432

First, until is a top-testing loop. The entire block must loop before the until condition is tested again; it is not tested after every statement within the loop. You have to reach end or next before the until will evaluate again.

Second, <statement> if <condition> is a ruby shorthand for if <condition> <statement> end. It allows you to write a simple conditional on one line without sacrificing readability. The next will only execute at the last iteration of the arr.each_index loop, going up the stack to the until condition, by which time sorted will have been set to false.

To see how this works, try running the following modification:

#!/usr/bin/ruby

def sorting(arr)
  puts "starting with #{arr}"
  sorted = false
  until sorted 
    sorted = true
    arr.each_index do |i|
      if i == arr.length - 1 
        puts "'next' when i == #{i}, arr = #{arr}"
        next
      end
      if arr[i] > arr[i + 1]
        puts "swapping at #{i}: #{arr[i]} <=> #{arr[i+1]}"
        arr[i], arr[i+1] = arr[i+1], arr[i]
        sorted = false
      end
     end
   end
   arr
end

p sorting([7,4,5,1,2,3])

The output of this program is:

starting with [7, 4, 5, 1, 2, 3]
swapping at 0: 7 <=> 4
swapping at 1: 7 <=> 5
swapping at 2: 7 <=> 1
swapping at 3: 7 <=> 2
swapping at 4: 7 <=> 3
'next' when i == 5, arr = [4, 5, 1, 2, 3, 7]
swapping at 1: 5 <=> 1
swapping at 2: 5 <=> 2
swapping at 3: 5 <=> 3
'next' when i == 5, arr = [4, 1, 2, 3, 5, 7]
swapping at 0: 4 <=> 1
swapping at 1: 4 <=> 2
swapping at 2: 4 <=> 3
'next' when i == 5, arr = [1, 2, 3, 4, 5, 7]
'next' when i == 5, arr = [1, 2, 3, 4, 5, 7]
[1, 2, 3, 4, 5, 7]

This is of course academic: the proper way to sort the array would be with Array#sort.

Note: the next is only necessary at all because on the last iteration of the each_index loop, i + 1 will be out of range for the array, which would cause the next line which accesses arr[i + 1] to fail (it will evaluate to nil, and you can't compare an Integer to nil). Alternate ways of doing this would be to modify the conditional around the swap testing the index, or change the outside of the loop enumerating the array to be a smaller range.

Modifying the conditional, eliminating the next, which works because logical-and conditionals are evaluated left to right and the interpreter stops as soon as the first one is false:

def sorting(arr)
  sorted = false
  until sorted 
    sorted = true
    arr.each_index do |i|
      if (i < arr.size - 1) && (arr[i] > arr[i + 1])
        arr[i], arr[i+1] = arr[i+1], arr[i]
        sorted = false
      end
     end
   end
   arr
end

p sorting([7,4,5,1,2,3])

Changing the range of the loop, which is better in that the loop executes fewer times:

def sorting(arr)
  sorted = false
  until sorted 
    sorted = true
    (0..arr.size - 2).each do |i|
      if (arr[i] > arr[i + 1])
        arr[i], arr[i+1] = arr[i+1], arr[i]
        sorted = false
      end
     end
   end
   arr
end

p sorting([7,4,5,1,2,3])

next is akin to goto in other languages and should be avoided if possible.

Upvotes: 3

Related Questions