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

Viewed 58

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.

3 Answers

You have only four allowed inputs. Check for those directly. Even if as inelegantly as if !(answer == "a" || answer == "b" ...). Anything else will accept some invalid input. In particular, if answer > "d" allows "ab" as a valid answer.

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

Other answers have answered the question given in the title of your question.

I would like to suggest that you consider adopting some variation of the following code.

def get_answer(question, answers)
  choices_to_answers = construct_choices_to_answers(answers)
  loop do 
    puts "#{question}"
    display_choices(choices_to_answers)
    print "answer: "
    choice = gets.chomp
    puts choice
    break choices_to_answers[choice] if choices_to_answers.key?(choice)
    puts "\nThat answer is invalid."
  puts "Enter a letter between 'a' and '#{choices_to_answers.keys.last}'\n"
  end
end
def construct_choices_to_answers(answers)
  ('a'..('a'.ord + answers.size - 1).chr).to_a.zip(answers).to_h
end
def display_choices(choices_to_answers)
  choices_to_answers.each { |k,v| puts "(#{k}): #{v}" }
end

Suppose the possible answers were as follows.

answers = %w| Y R Z Q |
  #=> ["Y", "R", "Z", "Q"]

Then

choices_to_answers = construct_choices_to_answers(answers)
  #=> {"a"=>"Y", "b"=>"R", "c"=>"Z", "d"=>"Q"}

and

display_choices(choices_to_answers)

prints

(a): Y
(b): R
(c): Z
(d): Q

Here I've assumed that the choices are always consecutive letters beginning with "a". If that assumption is correct there is no need to manually associate those letters with the possible answers.


Let me now show a possible conversation with the method (with answers as defined above).

question = "What is the last letter in the alphabet?"
get_answer(question, answers)
  #=> "R"

The following is displayed.

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

That answer is invalid.
Enter a letter between 'a' and 'd'

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

You might then construct an array of hashes that provide the question, possible answers, correct answer, possibly the weight of the question and a boolean indicating whether a correct answer was given.

questions = [
  ...
  { question: "What is the last letter in the alphabet?",
    answers: ["Y", "R", "Z", "Q"],
    correct_answer: "Z",
    weight: 1,
    correct_answer_given: nil
  },
  ...
]

The value of :correct_answer_given for each question is updated to true or false when the question is answered. Once all questions have been answered questions can be used to determine a mark for the exam.


If you wish to assign a number to each question you could write

question_number = 0
questions.each do |h|
  question_number += 1
  g = h.merge(question: "#{question_number}. #{h[:question]}")
  answer = get_answer(question, g[:answers])      
  h[:correct_answer_given] = h[:correct_answer] == answer
end

If question_number #=> 12 and

h[:question]
  #=> "What is the last letter in the alphabet?",

then

g[:question]
  #=> "12. What is the last letter in the alphabet?"
Related