CrackedRuby logo

CrackedRuby

case/when Expressions

Overview

Ruby's case/when expressions provide pattern matching capabilities that go beyond simple equality comparisons. The case statement evaluates an expression and compares it against multiple when clauses using the case equality operator (===), which enables sophisticated matching patterns including ranges, regular expressions, classes, and custom objects.

The case expression returns the value of the matched when clause, making it both a control structure and an expression that can be assigned to variables or used in method chains. Ruby evaluates when clauses sequentially from top to bottom, executing the first match and ignoring subsequent clauses.

def categorize_number(num)
  case num
  when 0
    "zero"
  when 1..10
    "small positive"
  when -Float::INFINITY...-1
    "negative"
  else
    "large positive"
  end
end

categorize_number(5)    # => "small positive"
categorize_number(-3)   # => "negative"

The case equality operator behavior varies by object type. Ranges use include?, regular expressions use match semantics, classes check inheritance, and procs call themselves with the case expression as an argument. This flexibility makes case statements adaptable to different matching requirements.

case "hello@example.com"
when /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
  "valid email"
when String
  "string but not email"
else
  "not a string"
end
# => "valid email"

Ruby case expressions support multiple values per when clause, implicit array creation with comma separation, and optional else clauses for default behavior. The entire case expression returns nil if no when clause matches and no else clause exists.

Basic Usage

Case expressions match values using the case equality operator, which provides different comparison semantics than the regular equality operator (==). This distinction enables flexible pattern matching across different data types and structures.

def process_response(response)
  case response
  when 200
    "success"
  when 404, 403, 401
    "client error"
  when 500..599
    "server error"
  else
    "unknown status"
  end
end

process_response(404)  # => "client error"
process_response(503)  # => "server error"

Regular expressions in when clauses match against string representations of the case expression. Ruby converts the case expression to a string if necessary and applies the regular expression match.

def identify_file_type(filename)
  case filename
  when /\.rb$/
    "Ruby source"
  when /\.js$/, /\.ts$/
    "JavaScript/TypeScript"
  when /\.(jpg|jpeg|png|gif)$/i
    "image file"
  when /\.txt$/
    "text file"
  else
    "unknown type"
  end
end

identify_file_type("script.rb")     # => "Ruby source"
identify_file_type("photo.JPG")    # => "image file"

Class matching uses inheritance hierarchy, matching the case expression against class types and their ancestors. This enables type-based dispatch and polymorphic behavior.

def handle_exception(error)
  case error
  when StandardError
    "standard error: #{error.message}"
  when SystemExit
    "system exit with code #{error.status}"
  when Exception
    "other exception: #{error.class}"
  else
    "not an exception object"
  end
end

handle_exception(ArgumentError.new("bad arg"))  # => "standard error: bad arg"

Case expressions work without an explicit case value, functioning like if/elsif chains but with when clause syntax. This pattern suits complex conditional logic where multiple variables require evaluation.

def categorize_user(user)
  case
  when user.admin?
    "administrator"
  when user.premium? && user.active?
    "premium member"
  when user.active?
    "regular member"
  when user.suspended?
    "suspended account"
  else
    "inactive user"
  end
end

Advanced Usage

Advanced case expressions leverage Ruby's case equality operator customization through the === method. Custom classes can override this method to define specialized matching behavior for domain-specific pattern matching.

class AgeRange
  def initialize(min, max)
    @min, @max = min, max
  end
  
  def ===(age)
    (age >= @min) && (age <= @max)
  end
  
  def to_s
    "#{@min}-#{@max} years"
  end
end

child = AgeRange.new(0, 12)
teen = AgeRange.new(13, 19)
adult = AgeRange.new(20, 64)
senior = AgeRange.new(65, 120)

def life_stage(age)
  case age
  when child
    "childhood"
  when teen
    "teenage years"
  when adult
    "adulthood"
  when senior
    "senior years"
  else
    "invalid age"
  end
end

life_stage(8)   # => "childhood"
life_stage(45)  # => "adulthood"

Proc objects and lambdas in when clauses call themselves with the case expression as an argument, enabling functional pattern matching and complex conditional logic encapsulation.

positive = ->(n) { n > 0 }
negative = ->(n) { n < 0 }
even = ->(n) { n.even? }
odd = ->(n) { n.odd? }

def number_properties(num)
  properties = []
  
  case num
  when positive
    properties << "positive"
  when negative  
    properties << "negative"
  else
    properties << "zero"
  end
  
  case num
  when even
    properties << "even"
  when odd
    properties << "odd"
  end
  
  properties.join(" and ")
end

number_properties(6)   # => "positive and even"
number_properties(-3)  # => "negative and odd"

Complex pattern matching combines multiple techniques, including guard clauses simulated through case expressions without explicit case values and nested case statements for hierarchical matching.

class RequestRouter
  def route(request)
    case request[:method]
    when "GET"
      case request[:path]
      when "/"
        handle_root
      when /^\/users\/(\d+)$/
        handle_user_show($1.to_i)
      when /^\/api\//
        case
        when request[:headers]["Content-Type"] == "application/json"
          handle_api_json(request)
        when request[:headers]["Authorization"]
          handle_api_auth(request)
        else
          handle_api_basic(request)
        end
      else
        handle_not_found
      end
    when "POST"
      case request[:path]
      when "/users"
        handle_user_create
      when /^\/users\/(\d+)\/posts$/
        handle_post_create($1.to_i)
      else
        handle_method_not_allowed
      end
    else
      handle_method_not_allowed
    end
  end
  
  private
  
  def handle_root; "root page"; end
  def handle_user_show(id); "user #{id}"; end
  def handle_api_json(req); "api json"; end
  def handle_api_auth(req); "api auth"; end  
  def handle_api_basic(req); "api basic"; end
  def handle_not_found; "404"; end
  def handle_user_create; "create user"; end
  def handle_post_create(user_id); "create post for #{user_id}"; end
  def handle_method_not_allowed; "405"; end
end

Splat operators and array decomposition work within case expressions, though Ruby treats them as array literals rather than pattern matching destructuring. Complex when clauses can combine multiple matching strategies.

def analyze_data_structure(data)
  case data
  when Array
    case data.size
    when 0
      "empty array"
    when 1
      "single element: #{data.first}"
    when 2..5
      "small array with #{data.size} elements"
    else
      "large array with #{data.size} elements"
    end
  when Hash
    case
    when data.empty?
      "empty hash"
    when data.keys.all? { |k| k.is_a?(String) }
      "string-keyed hash with #{data.size} pairs"
    when data.keys.all? { |k| k.is_a?(Symbol) }  
      "symbol-keyed hash with #{data.size} pairs"
    else
      "mixed-key hash with #{data.size} pairs"
    end
  when String, Symbol
    "scalar value: #{data}"
  when NilClass
    "nil value"
  else
    "unknown type: #{data.class}"
  end
end

Common Pitfalls

Case expressions use the case equality operator (===) rather than regular equality (==), leading to unexpected behavior when developers assume standard equality comparison. This distinction affects how objects match against when clauses.

# Common mistake: expecting == behavior
string = "42"
number = 42

case string
when number
  "matches number"  # This will NOT execute
else
  "no match"       # This executes because 42 === "42" is false
end

# Correct approach: understand === behavior  
case string
when /^\d+$/
  "numeric string"  # This executes
when String
  "regular string"
else
  "something else"
end

Range matching uses include? internally, but this creates performance issues with large ranges and unexpected results with non-comparable objects. Developers often assume ranges work identically to mathematical intervals.

# Performance trap with large ranges
huge_range = (1..1_000_000)

# This creates the entire range in memory for some Ruby versions
case 999_999  
when huge_range
  "found"  # Slow and memory-intensive
end

# Better approach for large numeric ranges
def in_range?(value, min, max)
  value >= min && value <= max  
end

case 999_999
when method(:in_range?).curry[1][1_000_000]
  "found"  # More efficient but complex
end

# Unexpected behavior with non-comparable objects
case "b"
when ("a".."z")  # Works fine
  "lowercase letter"
when (Date.today..Date.today + 30)  # Type mismatch
  "this month"   # Never matches strings
end

When clauses execute sequentially, and Ruby stops at the first match, causing issues when broader patterns precede more specific ones. Order dependency creates maintenance problems and logical errors.

# Wrong order: broad pattern first
def categorize_error(error)
  case error
  when StandardError        # Too broad, catches everything first
    "standard error"
  when ArgumentError       # Never reached
    "argument error" 
  when NoMethodError       # Never reached
    "method error"
  else
    "other error"
  end
end

# Correct order: specific to general
def categorize_error(error)
  case error
  when NoMethodError       # Most specific first
    "method error"
  when ArgumentError       # More specific
    "argument error"
  when StandardError       # Most general last
    "standard error"
  else
    "other error"
  end
end

Variable assignment within when clauses creates scoping issues and uninitialized variable errors when the case expression doesn't match expected patterns.

# Problematic variable assignment
def process_data(input)
  case input
  when Array
    result = input.map(&:upcase)  # Only assigned if input is Array
  when String  
    result = input.upcase         # Only assigned if input is String
  end
  
  result.length  # NameError if input doesn't match any when clause
end

# Better approach: initialize variables or use return values
def process_data(input)
  result = case input
           when Array
             input.map(&:upcase)
           when String
             input.upcase
           else
             []  # Default value
           end
  
  result.length
end

Regular expressions in when clauses can produce confusing behavior because Ruby converts the case expression to a string before matching. This implicit conversion causes type-related bugs.

# Confusing implicit conversion
number = 42

case number
when /4/          # number.to_s => "42", then /4/ matches
  "contains four" # This executes unexpectedly
end

# Class checking fails with inheritance confusion
class CustomString < String
end

custom = CustomString.new("hello")

case custom
when String      # Matches because CustomString inherits from String
  "string"
when CustomString  # Never reached due to inheritance
  "custom string"
end

Multiple values in when clauses use array semantics, but developers sometimes expect logical OR behavior that doesn't account for array edge cases.

# Unexpected array behavior
values = [1, 2, 3]

case values
when 1, 2, 3     # This creates [1, 2, 3] and compares arrays
  "matches"      # Never executes because [1,2,3] === [1,2,3] is false
end

# Correct approach depends on intent
case values
when Array
  if [1, 2, 3].include?(values) || values == [1, 2, 3]
    "matches array"
  end
end

# Or for individual elements
values.each do |value|
  case value
  when 1, 2, 3
    puts "found #{value}"
  end
end

Reference

Case Expression Syntax

Pattern Syntax Behavior
Basic case case expr; when value; end Compares using ===
Multiple values when val1, val2, val3 Matches any value (OR logic)
No case expression case; when condition; end Functions like if/elsif
With else case expr; when val; else; end Default clause if no match
Assignment result = case expr; when val; end Returns matched clause value

Case Equality Operator (===) Behavior

Type Behavior Example
Range Uses include? (1..10) === 5 # => true
Regexp Matches against string /\d+/ === "42" # => true
Class Checks inheritance String === "hello" # => true
Proc/Lambda Calls with argument ->(x) { x > 0 } === 5 # => true
Module Checks class/ancestors Enumerable === Array # => true
Object Falls back to == 42 === 42 # => true

Pattern Matching Examples

# Range patterns
case age
when 0...13    then "child"
when 13...20   then "teenager" 
when 20...65   then "adult"
when 65..Float::INFINITY then "senior"
end

# Regular expression patterns  
case input
when /\A\d+\z/           then "integer"
when /\A\d+\.\d+\z/      then "decimal"
when /\A[a-zA-Z]+\z/     then "letters only"
when /\A\s*\z/           then "whitespace"
end

# Class patterns
case object
when String              then object.upcase
when Integer             then object * 2
when Array               then object.size
when Hash                then object.keys
when NilClass            then "nothing"
end

# Proc patterns
positive = ->(n) { n > 0 }
even = ->(n) { n.even? }

case number
when positive            then "positive"
when even                then "even"
end

Performance Characteristics

Pattern Type Performance Memory Usage Notes
Integer literals Excellent Minimal Direct comparison
String literals Very good Low Hash table lookup
Small ranges (< 100) Good Low Array creation
Large ranges (> 1000) Poor High Avoid if possible
Regular expressions Moderate Moderate Compilation cost
Class checking Very good Minimal Ancestor chain walk
Proc calls Moderate Low Method call overhead

Error Handling Patterns

# Exception type matching
begin
  risky_operation
rescue => error
  case error
  when ArgumentError
    handle_bad_arguments(error)
  when StandardError
    handle_standard_error(error)  
  when SystemExit
    handle_system_exit(error)
  else
    handle_unknown_error(error)
  end
end

# Validation patterns
def validate_input(input)
  case input
  when nil, ""
    raise ArgumentError, "input cannot be empty"
  when String
    input.strip
  when Numeric  
    input.to_s
  else
    raise TypeError, "unsupported input type: #{input.class}"
  end
end

Common Return Values

Scenario Return Value Example
Match found Value of when clause "result"
No match, has else Value of else clause "default"
No match, no else nil nil
Empty when clause nil when condition; # => nil
Multiple statements Last statement value when val; a = 1; b = 2; b returns 2

Complex Matching Strategies

# Nested case expressions
case request
when Hash
  case request[:type]
  when "GET"
    handle_get(request)
  when "POST"  
    case request[:format]
    when "json"
      handle_post_json(request)
    when "xml"
      handle_post_xml(request)
    end
  end
end

# Guard clause simulation
case
when user.nil?
  "no user"
when user.admin? && user.active?
  "active admin"
when user.admin?
  "inactive admin"
when user.active?
  "regular user"
else
  "inactive user"  
end

# Combined pattern types
case data
when /^\d{4}-\d{2}-\d{2}$/, Date
  parse_date(data)
when /^\d+$/, Integer
  parse_number(data)  
when String
  parse_string(data)
when Array, Hash
  parse_structure(data)
end