Loop through multiple conditions until all of them are met - ruby

I'm using Ruby to get input from a user to provide new names for a list of files. I'm storing the names in an array, but before I store it, I have a series of conditions that I'm looping through to make sure that the user's input is valid. It essentially boils down to this (I've removed parts of the code that aren't relevant to the question):
puts "Rename file to:"
new_name = gets.chomp
new_name = check_input(new_name,#all_names)
#all_names << new_name
def check_input(new_name,all_names)
while new_name.match(/\s/)
puts "Names must not contain spaces:"
new_name = gets.chomp
end
while new_name.empty?
puts "Please enter a name:"
new_name = gets.chomp
end
while all_names.include? new_name
puts "That name already exists. Please enter a different name:"
new_name = gets.chomp
end
return new_name
end
Overall this works pretty well, but I want to make sure to loop through each "while" condition again and again, until all of the conditions are met. If, for instance, a name "abc" already exists, the user follows this order:
enters "abc" => "That name already exists. Please enter a different
name"
enters "a b c" => "Names must not contain spaces"
enters "abc" again =>
The last entry works successfully, but I don't want it to, since it's skipping over the condition that checks for duplicates. Is there a better way to loop through these conditions simultaneously, with each new entry?
Thank you for any help!

Right idea with the loop, just the wrong place for it. You need to check each gets from the user against all possible invalid cases. What you were doing was checking until a single invalid case was passed and then going on to a different one, which didn't check if the previous case(s) still passed:
# outputs an error message and returns nil if the input is not valid.
# Otherwise returns the input
def check_input(input, all_names)
if input.match(/\s/)
puts "Name must not contain spaces:"
elsif input.empty?
puts "Please enter a name:"
elsif all_names.include?(input)
puts "That name already exists. Please enter a different name:"
else
input
end
end
#all_names = ['abc']
puts "Rename file to:"
# keep gets-ing input from the user until the input is valid
name = check_input(gets.chomp, #all_names) until name
#all_names << name
puts #all_names.inspect
Since puts returns nil, check_input will return nil if the input is not valid. Otherwise, in the final else, we'll return the valid input and assign it to the variable name and stop executing the until loop.
Example run:
Rename file to:
abc
That name already exists. Please enter a different name:
a b c
Name must not contain spaces:
abc
That name already exists. Please enter a different name:
abc23
["abc", "abc23"]

Code
def rename_files(fnames)
fnames.each_with_object({}) do |fn,h|
loop do
puts "Rename file '#{fn}' to:"
new_name = gets.chomp
bad_name = bad_name?(new_name, h)
if bad_name
print bad_name
else
h.update(new_name=>fn)
break
end
end
end.invert
end
def bad_name?(new_name, h)
if new_name.include?(' ')
"Names must not contain spaces. "
elsif new_name.empty?
"Names cannot be empty. "
elsif h.key?(new_name)
"That name already exists. Duplicates are not permitted. "
else
nil
end
end
Example
rename_files(["cat", "dog", "pig"])
Rename file 'cat' to:
# <enter "three blind mice">
Names must not contain spaces. Rename file 'cat' to:
# <enter ENTER only>
Names cannot be empty. Rename file 'cat' to:
# <enter "three_blind_mice">
Rename file 'dog' to:
# <enter "four_blind_mice">
Rename file 'pig' to:
# <enter "three_blind_mice?>
That name already exists. Duplicates are not permitted. Rename file 'pig' to:
# <enter "five_blind_mice"
#=> {"cat"=>"three_blind_mice", "dog"=>"four_blind_mice", "pig"=>"five_blind_mice"}
Notes
bad_name? returns a (truthy) message string if the proposed file name is invalid for one of the three specified tests; else nil is returned.
if bad_name? returns a truthy value, it is printed using print, rather than puts, as it will be followed by puts "Rename file '#{fn}' to:" on the same line. The latter message is in part to remind the user which file is being renamed.
Hash.key? is used to determine if a proposed filename is a duplicate of one already entered, in part because hash key lookups are much faster than linear searches used to find array elements.
the new names may include the original names. Care therefore must be taken in renaming the files. (Consider, for example, "f1" changed to "f2" and "f2" changed to "f1".) If none of the original names are to be used as new filenames an additional test must be added to bad_name? (and fnames must be passed as a third argument to that method).
the hash being constructed is inverted (using Hash#invert) as a final step, so that the keys are the original filenames.

This could be a good use for recursion (just showing one of the conditions here, the others are the same structure):
def check_input(new_name,all_names)
# using if instead of while ... recursion provides the 'loop' here
if new_name.match(/\s/)
puts "Names must not contain spaces:"
new_name = check_input(gets.chomp, all_names)
end
# etc, other conditionals
new_name
end
Essentially, none of these new_name assignments will resolve until the input passes all the checks. The program moves deeper into the stack, but everything will resolve as soon as some input passes all the checks.

Yep, recursion is the right way to do something like this I think. Just throw this in a test.rb file and run ruby test.rb:
#all_names = []
def check_name(name = nil)
# Find out if it's invalid and why
invalid_reason = if name.empty?
"Please enter a name:"
elsif name.match(/\s/)
"Names must not contain spaces:"
elsif #all_names.include?(name)
"That name already exists. Please enter a different name:"
end
# Either return the name or ask for it again
if invalid_reason
puts invalid_reason
name = check_name(gets.chomp)
end
# Once we have it return the name!
name
end
puts "Rename file to:"
new_name = check_name(gets.chomp)
puts "Successfully storing name '#{new_name}'..."
#all_names << new_name
Let me know if that's doing what you were looking for!

Related

Can I recall the "case" in case?

I want to recall the case until user writes a or b. I do not want to use "case"
particularly.
I just want to get input from user but not geting something else. If he writes something else, he should need to write until he writes a or b.
str = gets.chomp.to_s
case str
when "a"
print "nice a"
when "b"
puts "nice b"
else
puts "please do it again"
end
class person
attr_accessor :name , :surname #and other attributes
end
#There will be a method here and it will run when the program is opened.
#The method will create the first object as soon as the program is opened.
#The new object that the user will enter will actually be the 2nd object.
puts "What do you want to do?
add
list
out"
process = gets.chomp.to_s
case process
when "add"
#in here user will add new objects of my class
when "list"
#in here user will show my objects
when "out"
puts "Have a nice day"
else
puts "please do it again"
end
In fact, if you look at it, many actions will be taken as a result of the user entering the correct input. what I want to tell is more detailed in this example. According to the input of the user, there will be actions such as calling methods, adding objects, etc.
I wrote most of the code on my computer. But still I couldn't solve my first problem.
Use Kernel#loop
There are a lot of ways to solve this problem, but let's start with a simple Kernel#loop wrapper around your existing code, as that's probably the easiest path forward for you.
loop do
str = gets.chomp.to_s
case str
when "a"
print "nice a"
when "b"
puts "nice b"
else
puts "please do it again"
# restart your loop when not "a" or "b"
next
end
# exit the loop if else clause wasn't triggered
break
end
Use until Control Expression
The loop construct above is pretty straightforward, but it requires you to think about where you need next and break statements for flow control. My own instinct would be to simply call a block until it's truthy. For example, the core logic could be shortened to:
str = nil; until str =~ /a|b/i do str = gets.chomp end; p str
This is a lot shorter, but it's not particularly user-friendly. To leverage this approach while making the solution more communicative and error-resistant, I'd refactor the original code this way:
# enable single-character input from console
require 'io/console'
# make sure you don't already have a value,
# especially in a REPL like irb
str = nil
until str =~ /a|b/ do
printf "\nLetter (a, b): "
str = STDIN.getch.downcase
end
puts "\nYou entered: #{str}"
While not much shorter than your original code, it handles more edge cases and avoids branching. It also seems less cluttered to me, but that's more a question of style. This approach and its semantic intent also seem more readable to me, but your mileage may legitimately vary.
See Also
IO::Console
Control Expressions
"I just want to do something until something else happens" is when you use some sort of while loop.
You can do this:
while true
str = gets.chomp
break unless str == 'a' || str == 'b'
puts "please do it again"
end
You can also use loop do:
loop do
str = gets.chomp
break unless ['a', 'b'].include?(str)
puts "please do it again"
end
puts "Nice #{str}."
Rubyists tend to prefer loop do over while true. They do pretty much the same thing.
One more thing. There's a simpler way to write out arrays of strings:
loop do
str = gets.chomp
break unless %w(a b).include?(str)
puts "please do it again"
end
puts "Nice #{str}."
It doesn't look a whole lot simpler, but if you have, say, 10 strings, it's definitely quicker to type in when you don't have to use all those quotation marks.
As your intuition was telling you, you don't need to use the case statement at all. Like trying to kill a flea with a sledgehammer. The most concise way to do your check is to check whether the input character is included in an array of the desired characters.

How to create a user defined exception in Ruby from user input

I am trying to create a user defined error where, if the user enters their first name only, it raises an error and tells the user to try again or, if they enter a numeral, it will raise an error.
This is my code but when I run it it outputs "Enter your first and last name" then it rescues it no matter if I enter a full name. It still says "Sorry I didn't quite catch that." then concatenates it:
class MyNewException < Exception
attr_reader :first, :last
def initialize(first)
#first = first
end
end
print "Enter your first and last name: "
begin
first = gets.chomp
last = gets.chomp
#prompt the user to enter first and last name
rescue MyNewException
puts "Sorry I didn't quite catch that. Try again."
gets
else
puts "Hello, " + first + last + "!"
end
gets will grab everything you type before pressing Enter. So first actually includes the first and last name that you are entering, and then last is empty. It looks like they are being concatenated, but all the text is in first. If you enter your first name, then press Enter, then enter your second name, then it should output both names.
The error is not being raised. You need to add something like this if you want to raise the error when last is blank:
last = gets.chomp
if last == ""
raise MyNewException, first
end

Ruby validation of name, email, and phone number

After creating the loop to check that the phone number is 10 characters I believe the phone issue is now resolved. Now I'm working with checking the email address, name, and making sure it outputs correctly, and making sure 2 names are entered by the user. Having issues getting the email address to validate and output in the correct format.
NAME_PATTERN = /([\w\-\']{2,})([\s]+)([\w\-\']{2,})/
EMAIL_PATTERN = /\A([^#\s]+)#((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i
PHONE_PATTERN = /^(?:\+?1\s*(?:[.-]\s*)?)?(?:\(\s*([2-9]1[02-9]|[2-9][02-8]1|[2-9][02-8][02-9])\s*\)|([2-9]1[02-9]|[2-9][02-8]1|[2-9][02-8][02-9]))\s*(?:[.-]\s*)?([2-9]1[02-9]|[2-9][02-9]1|[2-9][02-9]{2})\s*(?:[.-]\s*)?([0-9]{4})$/
def valid_name?(name)
!!name.match(NAME_PATTERN)
end
puts "Enter your first and last name (John Doe): "
while (name=gets)
names = name.split(" ", 2)
if valid_name?(name)
puts "Great, that looks like it could work."
break
else
puts "Please enter first and last name."
end
end
def valid_email?(email)
!!email.match(EMAIL_PATTERN)
end
puts "Enter your email address (joe#info.com): "
while (email = gets)
if valid_email?(email)
puts "Great, that looks like it could work."
break
else
puts "Invalid email address entered. Please try again. "
end
end
def valid_phone?(number)
!!number.match(PHONE_PATTERN)
end
puts "Enter your phone number including area code (numbers only): "
while (number=gets)
if valid_phone?(number)
puts "Great, that looks like it could work."
break
else
puts "Invalid phone number entered. Please try again."
end
end
puts names
puts email
puts number
I suspect you didn't intend to use the assignment operator here:
if (email = /\A([^#\s]+)#((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i)
Try this:
if email =~ /\A([^#\s]+)#((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i
You have a similar error in the phone_number method:
if number = 10
I'm not sure what you intended here. Perhaps this?
if number.size == 10
You have more problems, however. Take a look at this loop:
loop do
if number.size == 10
break
else
puts "Invalid phone number entered. Please try again."
end
end
How will the user ever exit this loop if the number is invalid? The value of number never changes.
Here's a Ruby flavoured approach to what you're trying to do:
# Define constants for things that are special and get re-used.
EMAIL_PATTERN = /\A\S+#\S+\z/
# Methods that test something and return true or false often end with
# a question mark (?) to indicate this.
def valid_email?(email)
!!email.match(EMAIL_PATTERN)
end
# Try and keep your support methods separate from the main body of
# your program.
puts "Enter your email address (joe#info.com): "
# This sets up a loop that waits until you get a valid response.
while (email = gets)
email = gets
if valid_email?(email)
puts "Great, that looks like it could work"
break
else
# Note that the message is rendered here, not in the validation
# method, so there's no assumptions about how this method is used.
puts "Invalid email address entered. Please try again. "
end
end
If you try and structure your code this way you'll find it's a lot easier to keep things organized. This is one of the big challenges when learning programming so as not to get overwhelmed.
Regular expressions are great for validating things that conform to a very specific pattern, but try not to get overly confident in the pattern of everyday things. Even the humble IPv4 address comes in a multitude of forms.

.include? doesn't raise when I add identical strings

I want to be able to downcase name before include is ran. Calling it on push has caused some weird behavior: It will add the second Jack to the list if it starts with a capitol, but will not add it if the second Jack is lowercase. How can I downcase name before the include check?
$list = []
def hand_out_gift(name)
if $list.include?name
raise "u can't get more than one present!"
else
$list.push(name.downcase)
puts "Here you go #{name} :)"
end
end
hand_out_gift("Jack")
hand_out_gift("Mary")
hand_out_gift("Jill")
hand_out_gift("Jack")
Downcase it early in your function:
def hand_out_gift(name)
key = name.downcase
if $list.include? key
raise "u can't get more than one present!"
else
$list.push key
puts "Here you go #{name} :)"
end
end
Aside: avoid using global variables like that.

how to re-ask a user for input if none was given the first time?

My current code is this:
print "Feed me input."
def get_input
input_value=gets.chomp
if !input_value
print "you didn't type anything"
else
input_value.downcase!
if input_value.include? "s"
input_value.gsub!(/s/,"th")
else
print "You entered a string but it had no 's' letters."
end
end
return input_value
end
get_input()
if !get_input
get_input
else
puts "#{get_input}"
end
I'm not sure why it isn't working. When I run it I get prompted for input then when I press enter after entering none I get the "You entered a string but it had no 's' letters", not the "you didn't type anything" that I wanted.
Every object except false and nil is treated as false if they are used as predicates. Even empty string is treated as true:
s = ""
puts true if s # => true
Use String#empty? to check if it is empty string.
As you said When I run it I get prompted for input then when I press enter after entering none - It means what happened acctually is
input_value="\n".chomp #( you gets methods take only `\n` as input)
"\n".chomp # => ""
so your input_value variable holds and empty string object. Now in Ruby every object has true value, except nil and false. Said that "" is also true,but you did !input_value,which means you are making it false explicitly. That's the reason in the below if-else block, else part has been executed and you didn't see the expected output "you didn't type anything".
if !input_value
print "you didn't type anything"
else
input_value.downcase!
if input_value.include? "s"
#.. rest code.
So I would suggest you in such a context replace the line if !input_value to if input_value.empty?, Which will make your code to behave as you are expecting. I didn't take your logic as a whole,but tries to show you how to code to meet your needs:
print "Feed me input."
def get_input
input_value=gets.chomp
if input_value.empty?
puts "you didn't type anything"
false
else
puts "found value"
input_value.downcase!
end
end
until input = get_input
# code
end
puts input
output
kirti#kirti-Aspire-5733Z:~/Ruby$ ruby test.rb
Feed me input.
you didn't type anything
you didn't type anything
you didn't type anything
HH
found value
hh
kirti#kirti-Aspire-5733Z:~/Ruby$

Resources