New to Crystal, so i’m probably doing something dumb here, but I cant get this to work at all.
Scenario: I’m reading in a config file which contains a list of available names. I then ask the user to pick one of those names [by entering the corresponding number and I then do ‘stuff’ with their choice. It was all going swimmingly til I tried writing a case switch to handle the user input. This is what i’ve got:
[The value n refers to the number of lines read in from the config file. So, if the user enters a choice greater than n-1, it’s an invalid choice]
puts "Which profile do you want to use. [Enter number]:"
print "> "
# loop waiting for user input
while choice = gets # loop while getting user input
# case choice
case choice
when choice.to_i < n
puts "DEBUG: Valid choice --> #{choice.to_i}"
break
when "Q", "q"
puts "DEBUG: Quitting. Byeee!!!...."
exit
else
puts "DEBUG: invalid choice"
print "> "
end
# end case choice
end
# loop waiting for user input
The problem is that, whatever number I enter at the prompt, I get the DEBUG: invalid choice error and, if I enter Q or q I get an unhandled exception:
Which profile do you want to use. [Enter number]:
> 0
DEBUG: invalid choice
1
DEBUG: invalid choice
2
DEBUG: invalid choice
3
DEBUG: invalid choice
4
DEBUG: invalid choice
q
Unhandled exception: Invalid Int32: q (ArgumentError)
from /usr/local/Cellar/crystal/0.32.1/src/string.cr:411:83 in 'to_i32'
from /usr/local/Cellar/crystal/0.32.1/src/string.cr:322:5 in 'to_i'
from scutty.cr:70:10 in 'loadconfig'
from scutty.cr:36:3 in '__crystal_main'
from /usr/local/Cellar/crystal/0.32.1/src/crystal/main.cr:97:5 in 'main_user_code'
from /usr/local/Cellar/crystal/0.32.1/src/crystal/main.cr:86:7 in 'main'
from /usr/local/Cellar/crystal/0.32.1/src/crystal/main.cr:106:3 in 'main'
I can’t get my head round why:
1: the else condition is running when I enter a number in the valid range
2: seems entering Q or q it’s being treated as an Int32 instead of a string
Your case expression is equivalent to the following if construct:
if choice.to_i < n === choice
elsif "Q" === choice || "q" === choice
else
end
choice.to_i < n === choice can never be true. You should probably use appropriate if conditionals instead of case. See reference on case for details.
Parsing "q".to_i expectedly results in an ArgumentError because "q" can’t be parsed as Int32. Try using String#to_i? instead, which returns nil if the value is not a number.
I can’t quite understand the inner workings of case enough to follow why it’s wrong. But re-purposing it as an if...else...end seems to have done the trick…
puts "Which profile do you want to use. [Enter number]:"
print "> "
# loop waiting for user input
while choice = gets # loop while getting user input
if choice.to_i < n
puts "DEBUG: Valid choice --> #{choice.to_i}"
elsif choice == "Q" || choice == "q"
puts "DEBUG: Quitting. Byeee!!!...."
exit
else
puts "DEBUG: invalid choice"
# print "> "
end
end
# loop waiting for user input
with one remaining puzzle…
Which profile do you want to use. [Enter number]:
> 0
DEBUG: Valid choice --> 0
1
DEBUG: Valid choice --> 1
2
DEBUG: Valid choice --> 2
3
DEBUG: invalid choice
4
DEBUG: invalid choice
5
DEBUG: invalid choice
q
Unhandled exception: Invalid Int32: q (ArgumentError)
from /usr/local/Cellar/crystal/0.32.1/src/string.cr:411:83 in 'to_i32'
from /usr/local/Cellar/crystal/0.32.1/src/string.cr:322:5 in 'to_i'
from scutty.cr:68:8 in 'loadconfig'
from scutty.cr:36:3 in '__crystal_main'
from /usr/local/Cellar/crystal/0.32.1/src/crystal/main.cr:97:5 in 'main_user_code'
from /usr/local/Cellar/crystal/0.32.1/src/crystal/main.cr:86:7 in 'main'
from /usr/local/Cellar/crystal/0.32.1/src/crystal/main.cr:106:3 in 'main'
Why does entering a Q or q still generate a Invalid Int32 error? Maybe I’m misunderstanding the error message., but it reads like Crystal is objecting to the fact that Q / q isn’t an integer. But why is it expecting an Int32 when reading keyboard input?
It is allready explained:
Parsing "q".to_i expectedly results in an ArgumentError because "q" can’t be parsed as Int32 . Try using String#to_i? instead, which returns nil if the value is not a number.
puts "Which profile do you want to use. [Enter number]:"
print "> "
# loop waiting for user input
while choice = gets # loop while getting user input
if choice.to_i? < n
puts "DEBUG: Valid choice --> #{choice.to_i}"
elsif choice == "Q" || choice == "q"
puts "DEBUG: Quitting. Byeee!!!...."
exit
else
puts "DEBUG: invalid choice"
# print "> "
end
end
The main point is that the conditions are tried top to bottom. Once one condition is met, the rest are ignored.
So, if you enter a number, in both versions you hit the first conditional branch and everything works well. If you enter “q”, the first check is still done, and that first check is the one raising, the actual comparison with “q” does not get a chance.
I was thinking the problem was with the elsif choice == "Q" || choice == "q" line and wondering why Crystal thought the user had entered an integer when s/he entered Q or q. But, thinking about it a bit more, I see that the problem is actually with the if choice.to_i < n line, as user entering a Q or q causes that line to fail to compile, as it’s expecting an integer.
[As an aside; I’d have thought [mistakenly] trying to cast an alpha character .to_s would have returned the ASCII code for that character, not generated an error. Isn’t that the usual practice?]
EDIT: @madex beat me to it, with his explanation above.
It’s OK. Might not be ideal code-wise, but Owing to previous File.exists sanity checks, choice will always be a number [even if it’s zero], as it’s based on number of lines read in from a file.
Might not be ideal code-wise, but Owing to previous File.exists sanity checks, choice will always be a number [even if it’s zero], as it’s based on number of lines read in from a file.
If that’s the case then the values “Q” and “q” for choice can never happen.
I should have said; the value for choice as read in from the config file will always be a number, as previous code checks whether the config file exists and can be read. User can select one of those [read in] numbers or Q/q to exit the programme.
Either way, it’s not going to be Nil –was all I meant.
The variable choice is read from the user, not from a file. It could be the case that n is computed from the file, but choicecan be “Q”, or any arbitrary string for that matter, so not necessarily a number.
Either way, it’s not going to be Nil –was all I meant.
It could be the case that you have a series of options for sure, 1 … n, but the variable choice can be nil because the user could for example press ^D (Ctrl-D) instead of typing an answer to the prompt. That is why the type of the variable is String | Nil instead of just String. And that is why the compiler refuses to compile choice.to_i? without a guard, because nil does not respond to to_i?.
Righto. Thanks for taking the time to explain. Having mostly tinkered with Python before, I think I need to get get out of my ‘dynamic language mindset’ where any unhandled value would just fall through to the else branch.
maybe a little late, but no one give a working solution?
following code is working
n = 50
puts "Which profile do you want to use. [Enter number]:"
print "> "
# loop waiting for user input
while choice = gets # loop while getting user input
if choice.to_i? && choice.to_i < n
puts "DEBUG: Valid choice --> #{choice.to_i}"
elsif choice == "Q" || choice == "q"
puts "DEBUG: Quitting. Byeee!!!...."
exit
else
puts "DEBUG: invalid choice"
# print "> "
end
end
And, in fact, i still prefer to use case
n = 50
puts "Which profile do you want to use. [Enter number]:"
print "> "
# loop waiting for user input
while choice = gets # loop while getting user input
case
when choice.to_i? && choice.to_i < n
puts "DEBUG: Valid choice --> #{choice.to_i}"
when ["Q", "q"].includes? choice
puts "DEBUG: Quitting. Byeee!!!...."
exit
else
puts "DEBUG: invalid choice"
# print "> "
end
end