Chapter 5. Strings

Étude 5-1: Validating Input

The Elixir philosophy is "let it crash"; this makes a great deal of sense for a telecommunications system (which is what Elixir was first designed for). Hardware is going to fail. When it does, you just replace it or restart it. The person using the phone system is unaware of this; her phone just continues to work.

This philosophy, however, is not the one you want to employ when you have (atypical for Elixir) programs that ask for user input. You want to those to crash infrequently and catch as many input errors as possible.

In this étude, you will write a module named ask_area, which prompts you for a shape and its dimensions, and then returns the area by calling Geom.area/3, which you wrote in Étude 4-1.

Your module will ask for the first letter of the shape (in either upper or lower case), then the appropriate dimensions. It should catch invalid letters, non-numeric input, and negative numbers as input. Here is some sample output.

iex(1)> c("ask_area.ex")
[AskArea]
iex(2)> c("geom.ex")
[Geom]
iex(3)> AskArea.area()
R)ectangle, T)riangle, or E)llipse: r
Enter width > 4
Enter height > 3
12
iex(4)> AskArea.area()
R)ectangle, T)riangle, or E)llipse: T
Enter base  > 3
Enter height > 7
10.5
iex(5)> AskArea.area()
R)ectangle, T)riangle, or E)llipse: w
Unknown shape w
:ok
iex(6)> AskArea.area()
R)ectangle, T)riangle, or E)llipse: r
Enter width > -3
Enter height > 4
Both numbers must be greater than or equal to zero.
:ok
iex(7)> AskArea.area()
R)ectangle, T)riangle, or E)llipse: e
Enter major radius > 3
Enter minor radius > 2
18.84955592153876

Here are the functions that I needed to write in order to make this program work.

char_to_shape/1
Given a character parameter (R, T, or E in either upper or lower case), return an atom representing the specified shape (:rectangle, :triangle, :ellipse, or :unknown if some other character is entered). Hint: use String.first/1 to get the first character of the user input, and use String.upcase/1 to make it upper case.
get_number/1

Given a string as a prompt, displays the string "Enter prompt > " and returns the number that was input. This involves the following steps:

  • Use String.strip/1 to get rid of the trailing newline character
  • Use binary_to_integer/1 to convert the string to a number.

When you write the -spec for this function (you have been writing documentation for your functions, haven’t you?), the type you will use for the parameter is String.t(). You cannot use string(), because that is a built-in type for Erlang, and there would be a conflict. You can see a complete list of the built-in types at http://www.erlang.org/doc/reference_manual/typespec.html

get_dimensions/2
Takes two prompts as its parameters (one for each dimension), and calls get_number/1 twice. Returns a tuple {n1, n2} with the dimensions.
calculate/3
Takes a shape (as an atom) and two dimensions as its parameters. If the shape is :unknown, or the first or second dimension isn’t numeric, or either number is negative, the function displays an appropriate error message. Otherwise, the function calls Geom.area/3 to calculate the area of the shape.

See a suggested solution in Appendix A.

Étude 5-2: Better Validation with Regular Expressions

The code you wrote in the previous étude is still sensitive to bad input; if you enter a floating point number or a word instead of a number, you will get an error message.

iex(1)> AskArea.area()
R)ectangle, T)riangle, or E)llipse: r
Enter width > 3.5
** (ArgumentError) argument error
    :erlang.binary_to_integer("3.5")
    /Users/elixir/code/ch05-01/ask_area.ex:50: AskArea.get_number/1
    /Users/elixir/code/ch05-01/ask_area.ex:60: AskArea.get_dimensions/2
    /Users/elixir/code/ch05-01/ask_area.ex:18: AskArea.area/0
    erl_eval.erl:569: :erl_eval.do_apply/6
    src/elixir.erl:133: :elixir.eval_forms/3
    /bin/elixir/lib/iex/lib/iex/server.ex:19: IEx.Server.do_loop/1

In this étude, you will use regular expressions to make sure that input is numeric and to distinguish integers from floating point numbers. You need to do this because binary_to_float/1 will not accept a string like "1812" as an argument. If you aren’t familiar with regular expressions, there is a short summary in Appendix B.

The function you will use is the Regex.match?/2. It takes a regular expression pattern as its first argument and a string as its second argument. The function returns true if the pattern matches the string, false otherwise. Here are some examples in IEx.

iex(1)> Regex.match?(~r/e/, "hello")
true
iex(2)> Regex.match?(~r/[0-9]/, "h3llo")
true
iex(3)> Regex.match?(~r/b[aeiou]g/, "beagle")
false

You will need to change get_number/1 to test input against patterns that match integers and floating point numbers. Presume (as Elixir does) that floating point numbers must have at least one digit before and after the decimal points. Extra credit for handling exponential notation. If neither pattern matches, have the function return :error. You will then need to change calculate/3 to handle the errors. (I did this by adding clauses.)

See a suggested solution in Appendix A.

Étude 5-3: Using String.split

Write a module named Dates that contains a function date_parts/1, which takes a string in ISO date format ("yyyy-mm-dd") and returns a list of integers in the form [yyyy, mm, dd]. This function does not need to do any error checking.

iex(1)> c("dates.ex")
[Dates]
iex(2)> Dates.date_parts("2013-06-15")
[2013,6,15]

Use the String.split/3 function to accomplish this task. How, you may ask, does that function work? Ask Elixir! In IEx, type h String.split and you will see the online documentation for that function.

Yes, I know this étude seems pointless, but trust me: I’m going somewhere with this. Stay tuned.

See a suggested solution in Appendix A.