rubyvalidationpromptmultiple-choice

How to validate command line input from multiple choice prompt in Ruby?


I'm writing a multiple choice quiz where the user has to choose an answer by entering a letter from a to d. Each question prompt looks like this:

What is the last letter in alphabet?
(a) Y
(b) R
(c) Z
(d) Q

If the user enters anything else, I want to show a message and print the whole question again until the input is a, b, c or d.

Here's what I've tried: (simplified)

question = "What is the last letter in alphabet?\n(a) Y\n(b) R\n(c) Z\n(d) Q"

puts question
answer = gets.chomp.to_s
while answer > "d"
  puts "Please enter a, b, c, or d"
  puts
  puts question
  answer = gets.chomp.to_s
end

It works fine when entering e, f, g etc. but it doesn't catch input like ab, 1, Z, or when the user just presses enter.


Solution

  • Your approach doesn't work because answer > "d" compares both strings character-wise using their (Unicode) code points. To understand what this means, take a look at the Basic Latin chart: (it's equivalent to an ASCII table)

    Basic Latin chart

    "d" has a codepoint of U+0064. Any smaller codepoint – i.e every character in the chart before "d" – is regarded smaller. This includes all (regular) digits, all (basic latin) uppercase letters and several symbols:

    "0" > "d"  #=> false
    "Z" > "d"  #=> false
    

    You could add a lower bound like answer < "a" || answer > "d" but this would still allow all strings starting with one of the allowed characters, e.g.:

    "apple" < "a" || "apple" > "d" #=> false
    

    To actually limit the answer to the four allowed values, you have to compare the string to each of them. You could combine these comparisons:

    answer == 'a' || answer == 'b' || answer == 'c' || answer == 'd'
    

    use a loop over an array of allowed values:

    ['a', 'b', 'c', 'd'].any? { |letter| answer == letter }
    

    check whether the array includes the answer:

    ['a', 'b', 'c', 'd'].include?(answer)
    
    # or
    
    %w[a b c d].include?(answer)
    

    or use a regular expression to match a to d:

    answer.match?(/\A[a-d]\z/)
    

    Note that the examples above become true if answer is between a and d. You could either negate the condition via !(...):

    while !(%w[a b c d].include?(answer))
      # ...
    end
    

    or use until instead of while:

    until %w[a b c d].include?(answer)
      # ...
    end