Why is This Case Switch Not Working?

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

Where am i going wrong?

Your case expression is equivalent to the following if construct:

if choice.to_i < n === choice
elsif "Q" === choice || "q" === choice
else
end
  1. 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.
  2. 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.

Thanks.

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.

By the way, choice.to_i? < n does not seem to be good.

First, choice is String | Nil and nil does not respond to to_i?. Second, you cannot compare nil with an integer like that.

D’oh! –now I get it.

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.

Recommended readings:

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.

You should pay attention to the error message. It points you directly to the line which causes the error.

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 choice can 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.

Memo to self: Cover all bases.