Extra Step for Fun: Force user Choice


#1



For this exercise, how can we force the user to enter a choice? For example, if the prompt comes up asking them to either Add, Delete, Update, Or Display and the user simply presses Enter without choosing a response, how can we make the sequence loop until the user enters valid input?


Replace this line with your code.


4. Prompting: Redux!
#2

Write a utility function that can be called repeatedly:

def choice(prompt)
    do
        puts prompt
        x = gets.chomp
    end until x.length > 0
    return x
end

command = choice("add, delete, update, display")

#3

Thanks for the explanation @mtf but I think this is advanced and way over my head. Will file this away to revisit for when I understand Ruby better.


#4

puts "What would you like to do?"
puts "-- Type 'add' to add a movie."
puts "-- Type 'update' to update a movie."
puts "-- Type 'display' to display all movies."
puts "-- Type 'delete' to delete a movie."

command = choice("add, delete, update, display").downcase
case command
# ...

#5

I think I'm even more confused now. Would you be able to use it in the lesson?


#6

All the above is theoretical, more-or-less pseudo-code. I'll try it out in the lessons (and fix any errors) to see what we can and cannot save and submit.


#7

It took some reading and a little tweaking, but this passes Lesson 3.

def choice(prompt)
    x = ""
    loop do
        puts prompt
        x = gets.chomp
        break if not x.to_s.empty?
    end
    return x
end

movies = {
    Babel: 4,
    Crash: 4
}

puts "What would you like to do?"
puts "-- Type 'add' to add a movie."
puts "-- Type 'update' to update a movie."
puts "-- Type 'display' to display all movies."
puts "-- Type 'delete' to delete a movie."

command = choice("add, delete, update, display").downcase
case command
when "add"
  puts "Added!"
when "update"
  puts "Updated!"
when "display"
  puts "Movies!"
when "delete"
  puts "Deleted!"
else
  puts "Error!"
end

#8

This is so helpful. Thanks so much!


#9

Just for fun, I tried this to get rid of all the puts lines...

def prompt
<<-eos
What would you like to do?
-- Type 'add' to add a movie.
-- Type 'update' to update a movie.
-- Type 'display' to display all movies.
-- Type 'delete' to delete a movie.
eos
end

puts prompt
command = choice("add, delete, update, display").downcase
# ...

#10

For even more fun, refactor all the code to incorporate the new function. I've renamed it getstr since it is more practical and makes sense anywhere it is called up.

def getstr(prompt, x="")
    loop do
        puts prompt
        x = gets.chomp
        break if not x.to_s.empty?
    end
    x
end

def prompt
<<-eos
What would you like to do?
-- Type 'add' to add a movie.
-- Type 'update' to update a movie.
-- Type 'display' to display all movies.
-- Type 'delete' to delete a movie.
eos
end

movies = {
    Babel: 4,
    Crash: 4
}

puts prompt
command = getstr("add, update, display, delete").downcase

Previous to refactoring, a typical input would look like this:

  puts "Enter a movie title"
  title = gets.chomp

After refactoring...

  title = getstr("Enter a movie title")

So now we've shaved some volume off the case statement.

case command
when "add"
  title = getstr("Enter a movie title")
  if movies[title.to_sym].nil?
    rating = getstr("Rate this movie (0-4)")
    movies[title.to_sym] = rating.to_i
    puts "Added!"
  else
    puts "That movie already exists! Its rating is #{movies[title.to_sym]}."
  end
when "update"
  title = getstr("What movie do you want to update?")
  if movies[title.to_sym].nil?
    puts "Movie not found!"
  else
    rating = getstr("What's the new rating? (Type a number 0 to 4.)")
    movies[title.to_sym] = rating.to_i
    puts "#{title} has been updated with new rating of #{rating}."
  end
when "display"
  movies.each do |title,rating|
    puts "#{title}: #{rating}"
  end
when "delete"
  title = getstr("Enter a movie title")
  if movies[title.to_sym].nil?
    puts "That movie was not found."
  else
    movies.delete(title)
    puts "#{title} deleted!"
  end
else
  puts "Command not found."
end

All in all, the fun is just beginning. What about validation of inputs? The switch takes care of the main cycle, but what about inside each task? How can we automate validation of numbers and their range? And what about turning this into a program that stays alive so we can issue new commands at the completion of each cycle?


#11

Continuing on, some refactoring and a new counterpart to getstr(), getnum(). This one depends upon getstr so will need to be further examined. For now, it does the job.

def getstr(prompt, x="")
    loop do
        puts prompt
        x = gets.chomp
        break if not x.to_s.empty?
    end
    x
end
def getnum(prompt, a=0, b=0, x=0)
    loop do
        x = getstr(prompt)
        break if (a..b).to_a.include? x.to_i
    end
    x
end

And we call it with three parameters, prompt text, minimum, maximum. The min and max are integers that are combined into a range and then converted to a lookup table (an array).

    rating = getnum("Rate this movie (0-4)", 0, 4)

That takes care of the number validation problem.


#12

This <<-eos "end of statement" is awesome! Thanks! CC hasn't covered this yet. Going to google more this to learn more about it. Thanks again!


#13

def getstr(prompt, x="")
    loop do
        print prompt
        print "?"
        x = gets.chomp
        break if not x.to_s.empty?
    end
    x
end
def getnum(prompt, a=0, b=0, x=0)
    loop do
        x = getstr(prompt)
        break if (a..b).to_a.include? x.to_i
    end
    x
end

def not_found_error
    "<! Movie not found !>"
end
def exists_error
    "<! Movie already exists !>"
end
def prompt
  "< add | update | display | delete | quit >\n"
end

There is a slight change to getstr where I print at the very least, ? in case there is no prompt message supplied. For this reason, a question will be sent without a ? since it is already supplied by the program.

The defined strings are self-explaining and cannot be altered or removed. It makes things in the body code a little more readable, as well.

The program now has a continuous loop cycle through the main prompt menu:

loop do
  command = getstr(prompt).downcase
  break if command == 'quit'
  case command
  when "add"
    title = getstr("Enter a movie title")
    if movies[title.to_sym].nil?
      rating = getnum("Rate this movie (0..4)", 0, 4)
      movies[title.to_sym] = rating.to_i
      puts "#{title}: #{rating}"
    else
      puts exists_error
    end
  when "update"
    title = getstr("Enter a movie title")
    if movies[title.to_sym].nil?
      puts not_found_error
    else
      puts "<% Found %>\n#{title}: #{rating}"
      rating = getnum("Rate this movie (0..4)", 0, 4)
      movies[title.to_sym] = rating.to_i
      puts "#{title}: #{rating}"
    end
  when "display"
    movies.each do |title,rating|
      puts "#{title}: #{rating}"
    end
  when "delete"
    title = getstr("Enter a movie title")
    if movies[title.to_sym].nil?
      puts not_found_error
    else
      puts "<% Found %>\n#{title}: #{rating}"
      movies.delete(title)
      puts "#{title} deleted!"
    end
  else
    puts "<! Command not found !>"
  end
end
puts "Bye!"

#14

This is where we put this puppy to bed... Added to the definition stack:

def found(t, r)
    "<% Found %>\n#{t}: #{r}"
end
def key_value(k,v)
    "#{k}: #{v}"
end
def title_prompt
    "Enter a movie title"
end
def rate_prompt_04
    "Rate this movie (0..4)"
end

And we mustn't leave out the hash...

movies = {
    Babel: 4,
    Crash: 4
}

Now the control loop is pared down to minimum string literal content.

loop do
  command = getstr(prompt).downcase
  break if command == 'quit'
  case command
  when "add"
    title = getstr(title_prompt)
    if movies[title.to_sym].nil?
      rating = getnum(rate_prompt_04, 0, 4)
      movies[title.to_sym] = rating.to_i
      puts key_value(title, rating)
    else
      puts exists_error
    end
  when "update"
    title = getstr(title_prompt)
    if movies[title.to_sym].nil?
      puts not_found_error
    else
      puts found(title, rating)
      rating = getnum(rate_prompt_04, 0, 4)
      movies[title.to_sym] = rating.to_i
      puts key_value(title, rating)
    end
  when "display"
    movies.each do |title,rating|
      puts key_value(title, rating)
    end
  when "delete"
    title = getstr(title_prompt)
    if movies[title.to_sym].nil?
      puts not_found_error
    else
      puts found(title, rating)
      movies.delete(title)
      puts "#{title} deleted!"
    end
  else
    puts "<! Command not found !>"
  end
end
puts "Bye!"

At this point all that needs doing is separating the procedural code from the switch which could get messy. Much better if it were a class and things could be structured to a parent object. That is another story for another day when you or I get more advanced at Ruby.


#15

for getnum(prompt, a=0, b=0, x=0)

I'm assuming ...

prompt is the statement, ie.) Rate this movie.
a is for the minimum number
b is for the maximum number
x is for ??? what is x for?


#16

The return value. It's actually a simple way to define a local variable without using a separate line. If it is not defined before the do..end, but only within that block, it does not exist when we get to the return. By defining it outside of the loop, it is in the right scope come return time.

In this case, it doesn't stay a number for long. getstr returns a string, and that's what it remains when it goes out of getnum. It gets evaluated as .to_i in the function, ... .include? x.to_i and on its return.

  rating = getnum(rate_prompt_04, 0, 4)    # returns a string
  movies[title.to_sym] = rating.to_i       # sets as an integer
  puts key_value(title, rating)            # prints as a string