Recently I’ve to use the case equality method more often during my daily routines - that is writing a Rails application. I use it in conjunction with case statements. I find it more readable than using plain ifs. For example when I have a pair of two dates and I expect that one or both can be a nil:

def from_to(from, to)
  from, to = parse_date(from), parse_date(to)

  case [from.nil?, to.nil?]
  when [false, false] then [from, to]
  when [true, true]   then [Date.current, Date.current]
  when [false, true]  then [from, from + 1.day]
  when [true, false]  then [to - 1.day, to]
  end
end

def parse_date(value)
  case value
  when Date, DateTime
    value
  when '', :nil?.to_proc
    nil
  when 'today'
    Date.current
  when 'yesterday'
    Date.yesterday
  when 'tomorrow'
    Date.tomorrow
  when /^\d{4}-\d{2}-\d{2}$/
    Date.strptime(value, "%Y-%m-%d")
  when String
    raise ArgumentError, "YYYY-MM-DD is the expected format "
  else
    raise ArgumentError, "#{value} is not supported"
  end
end

from, to = from_to(params[:from], params[:to])
Policy.create!(from: from, to: to)

As you can see in the example the method Object#=== is overwritten in various classes.

  • Regexp#=== is comparing against the value.
  • Module#=== is checking if the value is of that type.
  • Proc#=== calls it with the object being compared as the argument.
  • Range#=== does what you would expect - it checks if the value is within that range.

In case of Class#=== and Array#=== it is not overwritten so effectively it’s the same as Object#=== (it calls Object#==).

def sanitize_age(value)
  case value
  when nil, ''
    raise ArgumentError, "age is blank"
  when :negative?.to_proc
    raise ArgumentError, "age is less than 0"
  when 0
    raise ArgumentError, "age is 0"
  when 1...18
    raise ArgumentError, "age is under 18"
  when -> v { !v.integer? }
    value.to_i
  else
    value
  end
end

User.create!(age: sanitize_age(params[:age]))

Don’t forget that you can use the splat operator to compare against any of the elements of an Array:

UNSOPPORTED_STATES = ["CA", "NY"]

def ensure_valid_state(value)
  case value
  when :blank?.to_proc
    raise ArgumentError, "state is blank"
  when *UNSUPPORTED_STATES
    raise ArgumentError, "#{state} is not supported"
  else
    value
  end
end

Carrier.create!(state: ensure_valid_state(params[:state]))

If you want to experiment just use the console:

irb(main):001:0> (1...18) === 17
=> true

Despite being a well known feature the case quality method is not being used so much in the wild. I think that developers could reach for it more often as it makes your code more readable and prettier at the same time.