You can make bad choices with data while you’re programming, but it’s even more likely that users will put incorrect data into your programs when asked for input. Creating resilient code means taking special care with user input. In the following section, your program reads input, and you’ll learn to make it withstand runtime errors.
To gain a better understanding of how Crystal works with types, let’s build a program to read in currencies and their relative exchange rates. Let’s start with reading in integer numbers into an array. You can find the complete source code in getting_input.cr.
The console lets you collect input, but the Playground doesn’t. To make this work, you have to open up a terminal and type: crystal getting_input.cr.
The user provides the numbers, and you expect that they will be smaller than 256, so they’re of type Int8.
| puts "Enter the numbers one by one, and end with an empty line:" |
| arr = [] of Int8 |
You’ll start with an empty array so you have to indicate the type [] of Int8. There are other ways to indicate the type of the contents of an array, like providing data when you initialize the array:
| arr1 = [75, 42, 126] |
| typeof(arr1) # => Array(Int32) |
Why did the type come up as Int32 when all of those numbers are below 127? Int32 is the default integer type. If you want to force Int8, perhaps for performance reasons, you have to write that explicitly, like so:
| arr1 = [75_i8, 42_i8, 126_i8] |
| typeof(arr1) # => Array(Int8) |
(Crystal offers i8, i16, i32, and i64 suffixes for signed integers, and u8, u16, u32, and u64 for unsigned integers.)
You read from the console with gets, which return everything it reads in as a String. You can display the input with string interpolation.
| puts "Enter a number:" |
| num = gets |
| p "You entered #{num}" # => "You entered 42" |
Let’s add the number to our array:
| arr << num |
Crystal won’t let you do this:
| Error: no overload matches 'Array(Int8)#<<' with type (String | Nil). |
By now, this should be getting familiar. The best way to find out what went wrong is to examine the type of num (first comment out the previous faulty line). You can do this in two ways:
| p typeof(num) # => (String | Nil) |
| p num.class # => String |
You see that Crystal distinguishes between:
nil can’t be appended to an array of integers. But why does the compiler think the input could be nil? Well, instead of entering a number, enter CTRL+D and see what happens: The class of num is Nil! The method << can’t be applied to nil. You have to guard against that input. There are several ways to do this. Because nil is falsy, the simplest is to test with if:
| if num |
| arr << num |
| end |
We aren’t finished yet because now we get another error:
| no overload matches 'Array(Int8)#<<' with type String. |
The compiler now knows that num is a String, but the array can only contain Int8. Time to convert the input with to_i8:
| if num |
| arr << num.to_i8 |
| p arr # => for example: [42] |
| end |
Now, Crystal will convert the string to an 8-bit signed integer.
Even if the string is a number, a few things can happen along the way. Including a decimal point will cause an error, as will integers outside of the range of -127 to 128. Non-numeric characters will break this conversion, which you’ll address later in the chapter.
Also, the if works for one input. We don’t know how many numbers will be provided, so we need a loop. while fits perfectly: gets itself returns a value that can be tested as the while condition. If this value isn’t nil or false, while is okay with it and adds it to the array.
| while num = gets |
| arr << num.to_i8 |
| p arr # => for example: [2, 3, 3, 5] |
| end |
Remember: Only nil, false, or null pointers are considered false by Crystal in any logical value or if, unless, while, and until expression. Any other value—including the string "false" and the number 0—works as true while testing expressions.
Assign Shortcut | |||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|
Because of the rules on falsiness, a ||= b, which is a shortcut for a || (a = b), is used to assign b to a only when a is nil:
This is commonly used in memoization: return the value of a when a is not nil, but otherwise return b. |
Returning to our input, let’s remove any whitespace characters with strip, just in case. To end the loop, test whether the user enters “stop” or just ENTER, and then break from the loop:
| while num = gets |
| num = num.strip # removes whitespace |
| if num == "" || num == "stop" |
| break |
| end |
| arr << num.to_i8 |
| end |
| p arr # => for example: [78_i8, 56_i8, 12_i8] |
read_line | |
---|---|
If testing after gets that the input isn’t nil doesn’t seem elegant, you can use the read_line method instead. Try it out—the exercise read_line.cr shows you how to do this. |