Ruby - Arrays, bounds, and raising exceptions - ruby

Below is the code for my script.
As you can see, i have an array, and an index. I pass that to the block called 'raise_clean_exception'. The integer part of it does actually raise a Standard Error exception which is great. I have an issue when I use an index that is out of bounds. So if my array only has 4 elements (0-3) and I use an index of 9, it will not raise the exception, and instead it prints out a blank line because nothing is there. Why would it do this?
#!/usr/bin/ruby
puts "I will create a list for you. Enter q to stop creating the list."
array = Array.new
i = 0
input = ''
print "Input a value: " #get the value from the user
input = STDIN.gets.chomp
while input != 'q' do #keep going until user inputs 'q'
array[i] = input #store the value in an array of strings
i += 1 #increment out index where the inputs are stored
print "Input a value: " #get the value from the user
input = STDIN.gets.chomp
end #'q' has been entered, exit the loop or go back through if not == 'q'
def raise_clean_exception(arr, index)
begin
Integer(index)
puts "#{arr[index.to_i]}"
# raise "That is an invalid index!"
rescue StandardError => e # to know why I used this you can google Daniel Fone's article "Why you should never rescue exception in Ruby"
puts "That is an invalid index!"
end
# puts "This is after the rescue block"
end
# now we need to access the array and print out results / error messages based upon the array index value given by the user
# index value of -1 is to quit, so we use this in our while loop
index = 0
arrBound = array.length.to_i - 1
while index != '-1' do
print "Enter an index number between 0 and #{arrBound} or -1 to quit: "
index = STDIN.gets.chomp
if index == '-1'
exit "Have a nice day!"
end
raise_clean_exception(array, index)
end

Consider using a subclass of StandardError, IndexError, which is specific to the problem you are experiencing. Also, using else prevents a blank space from being printed if the index is out of bounds and when raising exceptions within a method, a begin...end block is implied.
def raise_clean_exception(arr, index)
Integer(index)
raise IndexError if index.to_i >= arr.length
rescue StandardError
puts "That is an invalid index!"
else
puts "#{arr[index.to_i]}"
end

Accessing an array element that's outside the range of existing elements returns nil. That's just the way Ruby works.
You could add the following line before the "puts" to trap that condition...
raise StandardError if index.to_i >= arr.size

Related

Why does defined input variable return nil after exception occurs?

I've noticed this strange behavior with the begin/rescue block in Ruby, when I define a variable, and an exception occurs and I try to call that variable that the exception occurred on it returns nil.
For example:
begin
print "Enter a number: "
input = Integer(gets.chomp)
sum = input + 5
puts "This is your number plus five: #{sum}"
rescue ArgumentError
puts "#{input}" #This outputs nil
end
Why does the begin/rescue block work like this, and is there a way to print the variable without it returning nil?
I'm not sure this is what you want but I try
input = gets.chomp
begin
number = Integer(input)
puts "your number plus five: #{number + 5}"
rescue ArgumentError
puts "#{input} is not a valid number"
end

Calling method isn't returning string

I created a method to count a substring 'e' in a string passed as an argument. If there isn't a substring 'e' in the string, it should return "There is no \"e\"." I am trying to achieve this:
How many times 'e' is in a string.
If given string doesn't contain any "e", return "There is no "e"."
if given string is empty, return empty string.
if given string is nil, return nil.
This is my code:
def find_e(s)
if !s.include?("e")
"There is no \"e\"."
elsif s.empty?
""
else s.nil?
nil
end
s.count("e").to_s
end
find_e("Bnjamin")
It skips the if statement and it still uses the method count. Why is this?
To achieve what you want you could move your string.count to the else statement in your if, because actually you're making your method return the quantity of e passed in the count method over your string, but what happens inside the if isn't being used:
def find_e(s)
if s.nil?
nil
elsif s.empty?
''
elsif !s.include?("e")
"There is no \"e\"."
else
s.count("e").to_s
end
end
p find_e("Bnjamin") # => "There is no \"e\"."
p find_e("Benjamin") # => "1"
p find_e(nil) # => nil
p find_e('') # => ""
And also your validations must be in order, first check nil values, then empty values, and then the rest, if you don't then you'll get some undefined method ___ for nil:NilClass errors.
You might have a hard time using the method you wrote. In the next method, you'll need a new case statement to test if find_e returned nil, an empty string, a string with a number or "no e".
This method would be a bit more consistent:
def count_e(string_or_nil)
count = string_or_nil.to_s.count("e")
if count == 0
"There is no \"e\"."
else
count
end
end
puts count_e("Covfefe")
# 2
puts count_e("Bnjamin")
# There is no "e".
puts count_e("")
# There is no "e".
puts count_e(nil)
# There is no "e".
But really, if there's no e in the input, just returning 0 would be the most logical behaviour.
You need to put your count method in a branch of the if/else statement, or else it will be evaluated last every time. Without an explicit return statement Ruby will return the last statement, so putting the method outside the if/else branch on the last line guarantees it will always be hit. Also, nil can be converted to an empty string by calling #to_s, so you can remove one of your branches by converting s.to_s, calling empty? and returning s
def find_e(s)
if s.to_s.empty?
s
elsif !s.include?("e")
"There is no \"e\"."
else
s.count("e").to_s
end
end
If you just return 0 whether you get nil, an empty string, or a string without e, you can make it one line
def find_e(s)
s.to_s.count("e").to_s
end
If it were me I'd probably return an Integer, which can always be converted to a String later. puts and "#{}" will implicitly call to_s for you anway. Then you can use that integer return in your presentation logic.
def count_e(input)
input.to_s.count("e")
end
def check_for_e(input)
count = count_e(input)
count > 0 ? count.to_s : "There's no \"e\"."
end
check_for_e("Covfefe") # => "2"
check_for_e("Bnjamin") # => "There's no \"e\"."
check_for_e(nil) # => "There's no \"e\"."
check_for_e("") # => "There's no \"e\"."
In Ruby, methods return the last statement in their body. Your method's last statement is always s.count("e").to_s, since that lies outside of the if statements.

Ruby: if boolean true return first_name from same row (in csv file)

I'm able to work this statement to a point but I'd like to add another outcome. How to get the statement to return a value from the same row (headed 'first_name') in the CSV if the boolean statement returns true?
def customer_check(user_pin)
x = false
CSV.read('customers.csv', headers: true).any? do |row|
x = true if row['pin'] == user_pin and row['work_here'] == "YES"
yellow = row['first_name']
if x == true then
puts "Welcome back #{yellow}."
sleep(1.5)
else
puts "login failed. Please try again in 3 seconds..."
sleep(3.0)
login_start
end
navigation_menu
end
If you are 100% sure the pins in your csv are unique you could try this
def customer_check(user_pin)
user = CSV.read('customers.csv', headers: true).detect { |row|
row['pin'] == user_pin && row['work_here'] == "YES"
}
if user
puts "Welcome back #{user['first_name']}."
sleep(1.5)
else
puts "login failed. Please try again in 3 seconds..."
sleep(3.0)
login_start
end
navigation_menu
end
Variable user will be initialized as the corresponding row of the .csv for which the conditional is true. Otherwise it's value will be nil:
detect method according to documentation:
Passes each entry in enum to block. Returns the first for which block is not false. If no object matches, calls ifnone and returns its result when it is specified, or returns nil otherwise.
If no block is given, an enumerator is returned instead.
Bear in mind that you might have to ensure that row['pin'] matches your user_id data type as it may come in as a String in which case you'd have to row['pin'].to_i == user_pin
How to get the statement to return a value from the same row (headed 'first_name') in the CSV if the boolean statement returns true?
Running your code as is, Ruby expects a final "end" and raises this error:
syntax error, unexpected end-of-input, expecting keyword_end
Adding another ending 'end' to your code will result in the if x == true block getting evaluated, but, note the indentation in the following code is arguably clearer. Using this example .csv file, this code will both print Welcome back... and return the first name if x == true:
require 'csv'
def customer_check(user_pin)
x = false
CSV.read('customers.csv', headers: true).any? do |row|
x = true if row['customerNo'] == user_pin && row['lastName'] == "Dunbar"
yellow = row['firstName']
# The if/else clauses are indented within the CSV.read
if x # <-- "== true" is redundant
# puts "Welcome back #{yellow}." # you can lose "yellow" if you want to
puts "Welcome back #{row['firstName']}."
sleep(1.5)
# return yellow # <-- RETURN VALUE
return row['firstName'] # <-- RETURN VALUE
else
puts "login failed. Please try again in 3 seconds..."
sleep(3.0)
# login_start
end
# navigation_menu
end
end
return_name = customer_check('1') # <-- prints "Welcome back John"
puts return_name # <-- "John" is the RETURN VALUE
I'm not sure how you are using login_start or navigation_menu but i hope this helps to answer your question. If it helps, here's the CSV doc

How to determine whether input is empty or enter is pressed

I have a task to puts an infinite number of word, each in one line to array, and when enter is pressed on an empty line, puts these words in reverse order. How can I define when enter is pressed or empty line is input?
Code is here:
word = []
puts "Enter word"
add = 0
until add == ????
word.push gets.chomp
add = word.last
end
puts word.reverse
Here's a possible solution, with comments. I didn't see any useful role being played by your add variable, so I ignored it. I also believe in prompting the user regularly so they know the program is still engaged with them, so I moved the user-prompt inside the loop.
word = [] # Start with an empty array
# Use loop when the terminating condition isn't known at the beginning
# or end of the repetition, but rather it's determined in the middle
loop do
print 'Enter word: ' # I like to prompt the user each time.
response = gets.chomp # Read the response and clean it up.
break if response.empty? # No response? Time to bail out of the loop!
word << response # Still in the loop? Append the response to the array.
end
puts word.reverse # Now that we're out of the loop, reverse and print
You may or may not prefer to use strip rather than chomp. Strip would halt if the user input a line of whitespace.
Here, this is a modified version of your code and it works as requested.
word = []
puts "Enter word"
add = 0
while add != -1
ans = gets.chomp
word.push ans
if ans == ""
puts word.reverse
exit
end
add += 1
end
puts word.reverse
This is another version, using (as you did originally) the until loop.
word = []
puts "Enter word"
add = 0
until add == Float::INFINITY
ans = gets.chomp
word.push ans
if ans == ""
puts word.reverse
exit
end
add += 1
end
puts word.reverse

"no implicit conversion from nil to integer"

I'm doing my first program, a simple to-do list. I want it to let me type a number, and delete the corresponding item from the list.
Every time though, I get "no implicit conversion from nil to integer". I can't seem to work it out. Any ideas?
$list = Array.new
def mainmethod
puts "Enter new item, or type 'view' to view the list, 'delete' to delete items"
input = gets.chomp
if input.downcase == "view"
puts "Your to do list is as follows:"
puts $list
elsif input.downcase == "delete"
puts "Which item would you like to delete? (Enter a number)"
deletenumber = gets.chomp.to_i
deletenumber-=1
delete_list = [deletenumber]
delete_list.each do |del|
$list.delete_at($list.index(del))
end
else
$list << input
puts "Added to list!"
end
end
loop { mainmethod }
See http://www.ruby-doc.org/core-1.9.3/Array.html#method-i-delete_at for the proper way of using Array#delete_at.
What you do is calling #index with a number that your array does not contain. Therefore $list.index(del) will return nil and the call to #delete_at will fail.
What you need to do is $list.delete_at(del).
The error TypeError: no implicit conversion from nil to integer happens when you try to access an element of an array using nil as the index. In your case it looks like delete_at is being passed nil:
[ 1, 2, 3 ].delete_at(nil)
# => TypeError: no implicit conversion from nil to integer

Resources