CrackedRuby logo

CrackedRuby

while and until Loops

Overview

Ruby implements two primary conditional loop constructs: while loops that continue execution as long as a condition remains true, and until loops that continue until a condition becomes true. Both loops evaluate their conditions before each iteration and support modifier syntax for single-line expressions.

The while loop executes its block repeatedly while the given condition evaluates to a truthy value. The until loop provides the logical inverse, executing while the condition evaluates to a falsy value. Ruby treats all values as truthy except false and nil.

counter = 0
while counter < 3
  puts "Iteration #{counter}"
  counter += 1
end
# => "Iteration 0"
# => "Iteration 1" 
# => "Iteration 2"
counter = 0
until counter == 3
  puts "Until iteration #{counter}"
  counter += 1
end
# => "Until iteration 0"
# => "Until iteration 1"
# => "Until iteration 2"

Both constructs return nil and support the modifier syntax for concise single-line operations:

counter = 0
counter += 1 while counter < 5
puts counter  # => 5

numbers = [1, 2, 3, 4, 5]
numbers.pop until numbers.length == 2
puts numbers  # => [1, 2]

Basic Usage

The standard while loop syntax requires a condition expression followed by a block terminated with end. The condition is evaluated before each iteration, making it a pre-test loop structure.

input = ""
while input != "quit"
  print "Enter command (or 'quit'): "
  input = gets.chomp.downcase
  
  case input
  when "hello"
    puts "Hello there!"
  when "time"
    puts Time.now
  when "quit"
    puts "Goodbye!"
  else
    puts "Unknown command: #{input}"
  end
end

The until loop provides more readable code when the terminating condition is more natural to express as a positive assertion:

password_attempts = 0
valid_password = false

until valid_password || password_attempts >= 3
  print "Enter password: "
  input_password = gets.chomp
  
  if input_password == "secret123"
    valid_password = true
    puts "Access granted!"
  else
    password_attempts += 1
    puts "Invalid password. #{3 - password_attempts} attempts remaining."
  end
end

puts "Access denied!" unless valid_password

Modifier syntax transforms simple operations into concise one-liners. The expression executes once before the condition is evaluated, making modifier loops post-test constructs:

# Reading file lines until empty
file = File.open("data.txt")
begin
  line = file.gets
  process_line(line) if line
end while line

file.close
# Building arrays with conditions
result = []
num = 1
result << num and num += 1 until result.length == 10
puts result  # => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

Nested loops handle multi-dimensional iteration scenarios:

row = 0
while row < 3
  col = 0
  while col < 3
    print "[#{row},#{col}] "
    col += 1
  end
  puts
  row += 1
end
# => [0,0] [0,1] [0,2] 
# => [1,0] [1,1] [1,2] 
# => [2,0] [2,1] [2,2]

Advanced Usage

Complex loop control requires understanding Ruby's break, next, and redo statements. The break statement terminates the loop immediately, while next skips to the next iteration. The redo statement repeats the current iteration without re-evaluating the condition.

numbers = []
counter = 0

while counter < 100
  counter += 1
  
  # Skip even numbers
  next if counter.even?
  
  # Stop at first number divisible by 13
  break if counter % 13 == 0
  
  # Redo if we hit a multiple of 7 (creates infinite loop protection needed)
  if counter % 7 == 0 && counter < 50
    puts "Skipping #{counter} (multiple of 7)"
    next
  end
  
  numbers << counter
end

puts "Collected numbers: #{numbers}"
puts "Stopped at: #{counter}"

Loop variables maintain scope outside the loop construct, unlike block iterators. This behavior enables accumulator patterns and state preservation:

fibonacci_sequence = [1, 1]
current_sum = 2

while fibonacci_sequence.length < 20
  next_number = fibonacci_sequence[-1] + fibonacci_sequence[-2]
  fibonacci_sequence << next_number
  current_sum += next_number
  
  # Early termination if sum exceeds threshold
  break if current_sum > 10000
end

puts "Generated #{fibonacci_sequence.length} Fibonacci numbers"
puts "Sum: #{current_sum}"
puts "Sequence: #{fibonacci_sequence.join(', ')}"

Exception handling within loops requires careful consideration of control flow. Exceptions can propagate out of loops or be handled internally:

retry_count = 0
max_retries = 3
success = false

until success || retry_count >= max_retries
  begin
    # Simulate unreliable operation
    result = perform_network_request()
    
    if result.status == 200
      success = true
      puts "Operation succeeded on attempt #{retry_count + 1}"
    else
      raise StandardError, "HTTP #{result.status}"
    end
    
  rescue StandardError => e
    retry_count += 1
    puts "Attempt #{retry_count} failed: #{e.message}"
    
    # Exponential backoff
    sleep(2 ** retry_count) if retry_count < max_retries
  end
end

puts success ? "Final success!" : "Failed after #{max_retries} attempts"

Proc and lambda objects can encapsulate complex loop termination logic:

# Complex condition as a proc
termination_condition = proc do |data, iterations|
  data.length > 100 || 
  iterations > 50 || 
  data.last&.negative?
end

data_points = [1]
iteration_count = 0

until termination_condition.call(data_points, iteration_count)
  new_value = data_points.last * rand(0.8..1.2) + rand(-5..5)
  data_points << new_value.round(2)
  iteration_count += 1
  
  puts "Iteration #{iteration_count}: #{new_value}" if iteration_count % 10 == 0
end

puts "Terminated after #{iteration_count} iterations"
puts "Final data size: #{data_points.length}"

Common Pitfalls

Infinite loops represent the most frequent pitfall in conditional loops. The condition must eventually change within the loop body, or external intervention must terminate execution:

# DANGEROUS: Infinite loop
counter = 0
while counter < 10
  puts "This will run forever"
  # counter never increments!
end

# SAFE: Proper counter management
counter = 0
while counter < 10
  puts "Iteration #{counter}"
  counter += 1  # Essential increment
end

Variable scoping creates subtle bugs when loop variables shadow outer scope variables:

counter = 100  # Outer scope variable

# This shadows the outer counter
counter = 0
while counter < 5
  puts "Inner counter: #{counter}"
  counter += 1
end

puts "Outer counter: #{counter}"  # => 5, not 100!
# Better approach: Use different variable names
outer_counter = 100
loop_index = 0

while loop_index < 5
  puts "Loop index: #{loop_index}, Outer: #{outer_counter}"
  loop_index += 1
end

puts "Preserved outer counter: #{outer_counter}"  # => 100

Modifier syntax creates post-test loops, executing the statement at least once regardless of the condition:

# Dangerous with modifier syntax
balance = 0
balance -= 100 while balance > 0
puts balance  # => -100 (executed once even though condition was false)

# Safer with block syntax
balance = 0
while balance > 0
  balance -= 100
end
puts balance  # => 0 (never executed)

Floating-point arithmetic introduces precision errors that can break termination conditions:

# Problematic floating-point loop
value = 0.1
while value != 1.0
  puts value
  value += 0.1
  break if value > 2.0  # Safety break to prevent infinite loop
end
puts "Final value: #{value}"  # Never equals exactly 1.0
# Better approach with tolerance
value = 0.1
target = 1.0
tolerance = 0.0001

while (value - target).abs > tolerance
  puts value.round(2)
  value += 0.1
  break if value > 2.0  # Safety break
end
puts "Final value: #{value.round(2)}"

Ruby's truthiness can create unexpected behavior when working with return values:

# Unexpected behavior with truthiness
lines = []
file = File.open("data.txt")

# This stops when gets returns an empty string, not just nil
while line = file.gets
  lines << line.chomp unless line.empty?  # Empty lines are truthy!
end

file.close
# Explicit nil check is clearer
lines = []
file = File.open("data.txt")

while (line = file.gets) != nil
  lines << line.chomp
end

file.close

Exception handling can disrupt expected loop flow:

# Problematic exception handling
counter = 0
while counter < 10
  begin
    risky_operation(counter)
  rescue StandardError => e
    puts "Error at #{counter}: #{e.message}"
    next  # This skips the counter increment!
  end
  
  counter += 1  # Never reached after exceptions
end
# Proper exception handling
counter = 0
while counter < 10
  begin
    risky_operation(counter)
  rescue StandardError => e
    puts "Error at #{counter}: #{e.message}"
  ensure
    counter += 1  # Always increments
  end
end

Reference

While Loop Syntax

Syntax Description Evaluation
while condition; statements; end Block form with semicolon Pre-test
while condition\n statements\n end Block form with newlines Pre-test
statement while condition Modifier form Post-test
begin; statements; end while condition Begin-while form Post-test

Until Loop Syntax

Syntax Description Evaluation
until condition; statements; end Block form with semicolon Pre-test
until condition\n statements\n end Block form with newlines Pre-test
statement until condition Modifier form Post-test
begin; statements; end until condition Begin-until form Post-test

Control Flow Statements

Statement Effect Scope
break Terminate loop immediately Current loop only
break value Terminate loop and return value Current loop only
next Skip to next iteration Current loop only
next value Skip iteration with value Current loop only
redo Repeat current iteration Current loop only

Truthiness Rules

Value Truthiness Loop Behavior
true Truthy while continues, until stops
false Falsy while stops, until continues
nil Falsy while stops, until continues
0 Truthy while continues, until stops
"" (empty string) Truthy while continues, until stops
[] (empty array) Truthy while continues, until stops
{} (empty hash) Truthy while continues, until stops

Return Values

Construct Return Value Notes
while condition; end nil Always returns nil
until condition; end nil Always returns nil
break nil Default break value
break value value Returns specified value
Modifier loops Result of last expression Post-test evaluation

Common Patterns

# Counter loop
i = 0
while i < limit
  # operations
  i += 1
end

# Accumulator pattern  
sum = 0
i = 1
while i <= n
  sum += i
  i += 1
end

# Input processing
input = nil
while input != "quit"
  input = gets.chomp
  # process input
end

# Conditional accumulation
results = []
current = start_value
while condition(current)
  results << current if meets_criteria(current)
  current = next_value(current)
end

# File processing
File.open(filename) do |file|
  while line = file.gets
    process_line(line)
  end
end

# Retry with exponential backoff
attempts = 0
max_attempts = 5
begin
  # risky operation
rescue SomeError
  attempts += 1
  retry if attempts < max_attempts
  sleep(2 ** attempts)
end