Lesson 6: Flow Control - Decision Making
In the previous lesson, I taught you about boolean logic. Hopefully, you have a good basis to the concept. This lesson will actually put that logic to good, practical use.
This lesson is the first part in the series on Flow Control.
Now, for the obligatory "What is flow control...?"
At this point, all your programs have had a very basic flow, just execute each line, one by one. However, when you start making real applications/programs, you'll find that this one directional program flow is too simple for your application. This is where flow control comes in, we want to be able to control the flow of the program.
The most basic form of flow control is decision making.
In programming, decision making is possible through the use of `if` statements. An if statement is what we use to make our boolean expressions do something. "if this, then do that."
So, in ruby...
if <condition> # What we want to do end
where <condition> is an expression.
note that an if statement must be closed with the "end" keyword.
What happens when the Ruby interpreter reaches an if statement? The interpreter will first evaluate <condition>, if <condition> evaluates to true, the code inside the statement will be executed. If the <condition> is false, the code inside the statement will be ignored, and the interpreter will continue from the line with "end"
What about this?
if 1 puts "1 is true" end
If you were to run this code, the output would be:
1 is true
But why? 1 is not a boolean!
Well, this is because of how ruby handles boolean expressions. When the interpreter gets to the "if ..." it assumes the expression to be a boolean expression, and with ruby, anything except nil and false is interpreted as true, and nil and false interpreted as false.
So, when the interpreter gets to "if 1", it evaluates the expression, in this case 1 returns 1, and since the value is not nil and not false (1), the code within the if statement is executed.
So, let's try an example. I want you to create a script this time, call it "age.rb"
So let's do some decision handling. We are going to make a program that takes the user's age, and tells them if they can buy alcohol. In Canada, where I live, you need to be 19 or older. You can incorporate your local age limit for drinking into the program if you'd like.
# Prompt the user to enter their age # we use print as that does not append a new line character print ("Enter your age: ") # Now we should get the users age # and convert to an integer: age = gets.to_i # Lets see if the user is old enough if (age >= 19) # They are of age! puts ("You are old enough to buy alcohol.") end
So, why not try it out!
ruby age.rb
Upon running your script, you will notice, unless you enter 19 or greater, the script will just end. Only if you enter age greater than or equal to 19, you will get "You are old enough to buy alcohol."
Well, we should let the user know if they aren't old enough to buy alcohol as well...and hey, why not let them know how many more years until they can?
The next part of the "if" statement, is the "else" clause.
if <condition> # Code if condition true else # Code if condition not true end
The block of code in the "else" clause will only be executed if the original block or any other block within the if statement is NOT executed. In this case, if the <condition> is false, the first block will not be executed. Because of this, the second block under "else" will be executed instead.
if (age >= 19) # They are of age! puts ("You are old enough to buy alcohol.") else # They are not of age! years_left = 19 - age # How many years until they can buy alcohol puts ("You are not old enough. Come back in #{years_left} years!") end
If you add that to your age.rb script, you will notice if you enter an age less than 19, the program will print out "You are not old enough. Come back in X years!"
So now, you can test a value, and can act based upon whether or not that test is true.
But, what if we want to act depending on several possibilities?
This is where "elsif" come into play. The "elsif" clause allows us to create multi-sided responses, based on multiple conditions.
if <condition> # Code elsif <condition> # Code else # Code end
The ruby interpreter will first enter the if statement at "if <condition>," if that condition is true, it will execute the following block of code, then jump to the "end." If that condition is false, the interpreter will then move to the first "elsif <condition>." If that condition is true, the interpreter will execute the block of code within the elsif clause, then jump to the "end." If false, the interpreter will continuously check any following "elsif" and if none are true, finally the "else" block will be executed instead.
So, this means you could have any arbitrary number of elsifs:
if <condition> # Code elsif <condition> # Code elsif <condition> # Code elsif <condition> # Code else # Code end
So, why might you need to test multiple conditions? Well, let's try this example:
Let's pretend we want to make a guessing game. The Game will have a set number, and the user will try to guess which number is set. If they guess too low, the program will say: "Too low!" and if the number is too high, the program will say: "Too high!", and if the number is equal, the computer will say "Wow! You got it!"
So, how would we do this?
# Set a number to guess number = 10 # Prompt the user to guess what number it is: print ("I am thinking of a number. What is it? ") # Get the guess guess = gets.to_i if guess < number puts ("Too low!") elsif guess > number puts ("Too high!") else puts ("Wow! You got it!") end
Try putting this in a script called "guessinggame.rb" and running it.
if guess < number elsif guess > number elsif guess == number end
This would work exactly the same. However, I chose to catch that in an else clause, as we know that if guess is NOT less than number, and NOT greater than number, it HAS to be equal to number. So, it would be more efficient to group that code under an else clause. In order to determine whether to execute a block under an if/elsif, the interpreter has to execute and evaluate the condition. With else, that condition is not executed, saving us some CPU processing.
Let's try another example. Let's make a program that gets the current month as a number, and prints out the English name of that month.
# Get month: prints ("Please enter the current month as a number: ") month = gets.to_i if month == 1 puts ("January") elsif month == 2 puts ("February") elsif month == 3 puts ("March") elsif month == 4 puts ("April") elsif month == 5 puts ("May") elsif month == 6 puts ("June") elsif month == 7 puts ("July") elsif month == 8 puts ("August") elsif month == 9 puts ("September") elsif month == 10 puts ("October") elsif month == 11 puts ("November") elsif month == 12 puts ("December") end
Whew, that was a lot of work. It works, but that seems like an awful a lot for a simple translation. As you can see, in some cases you may want to test for a specific value, and each value could represent something specific. Well, thankfully, ruby has a much more elegant way of doing this, with "switch" or "case" statements.
case <variable> when value then #code when value then #code when value then #code else #code end
So how does this work? The case statement takes <variable> then tests that variable against each value, testing if they are equal to each other. The first value that matches, it's corresponding code is executed. You can have an arbitrary number of when clauses, and else works exactly like in if. If no other values are equal to <variable> then else will be executed. Like in the if statements, else is optional here as well.
when value # Code must be here
However, with the "then" portion, you can actually put the block on the same line:
when value then # Code could be here
Also, I would like to point out that the "else" portion SHOULD NEVER have a "then," if you want to put the else block on the same line, use a semi-colon (;) (ruby interprets this as a line terminator)
else; # Code could be here
So, lets fix our nasty if statement:
case month when 1 then puts ("January") when 2 then puts ("February") when 3 then puts ("March") when 4 then puts ("April") when 5 then puts ("May") when 6 then puts ("June") when 7 then puts ("July") when 8 then puts ("August") when 9 then puts ("September") when 10 then puts ("October") when 11 then puts ("November") when 12 then puts ("December") end
Well, that looks much neater. It's still long, but neater and a little less typing. However, I feel that the output is too "hard coded." What if later, we decide we want to put the month into a variable, for later usage? We'd have to change each "puts" to "variable =." With modern IDEs, this would be a simple task, but still unnecessary. One major rule of the ruby community (or even any programming community) is "Don't Repeat Yourself", and I feel both "puts" or "variable =" that many times, is repeating yourself.
Conveniently, we can get around this. Remember how everything in ruby is an object? And every expression returns an object? Well, case is no different.
case will "return" (or send back what the block returns) whichever block of code passes the test. In this case, it sends back "puts ("month")". Well, as you may know, the line of code "string" will return a string object, with the value "string." So what about this:
puts case month when 1 then "January" when 2 then "February" when 3 then "March" when 4 then "April" when 5 then "May" when 6 then "June" when 7 then "July" when 8 then "August" when 9 then "September" when 10 then "October" when 11 then "November" when 12 then "December" end
Now, that, is much neater. Normally, we would want to put the block of code in a when ... clause on the next line, but in the case where we are simply returning a single string, I find it actually more readable like this.
Then, if we wanted to change it to store the month in a variable:
# Get month prints ("Please enter the current month as a number: ") month_no = gets.to_i month_word = case month when 1 then "January" when 2 then "February" when 3 then "March" when 4 then "April" when 5 then "May" when 6 then "June" when 7 then "July" when 8 then "August" when 9 then "September" when 10 then "October" when 11 then "November" when 12 then "December" end puts ("The month is #{month_word}.")
Now we only had to change one line of code, rather than 12 lines of code.
You'll probably find yourself using "if" statements MUCH more than case/switch statements; however for certain things, especially menus (where you have several choices, and the current choice is stored in a single variable) case statements are much easier to employ.
case when variable == 5 when other_variable < 5 end
However, this isn't often used, as if...elsif would be preferred in this case.
One last thing I would like to teach you, is the "conditional assignment operator." As you start writing your own programs, you may find yourself in a situation where you only wish to assign a variable a certain value, depending on one condition, otherwise, set it to something else. This is called conditional assignment. For example, what if we want the absolute (non-negative) value of a number? We could do something like this:
if number < 0 absolute = -number else absolute = number end
That works, and is neat; but it's a lot of extra typing. This is where the conditional assignment operator comes in. It's a little bit trickier than if statements, but can save a lot of typing.
variable = <condition> ? <true> : <false>
<condition> is any boolean expression
<true> is the value assigned to variable if <condition> is true
<false> is the value assigned to variable if <condition> is false
The important things to watch for is, <condition> MUST be followed by a `?` and must be separated by spaces. The true / false values MUST be separated by `:`
So, let's try our example:
absolute = number < 0 ? -number : number
So, if number is less than 0, give absolute the value of -number (which would be positive, since number is < 0) otherwise, just give absolute the value of number.
Now, it looks a bit neater, and much less typing.
There are also other "forms" of conditional assignment, that are used less often. I will leave those out for now, and include them at another time.
So, that's it for now. Before I go, I would like to leave you with a few practice coding challenges:
1. Write a program that takes a student's grade as a number/percentage, and prints out their letter grade.
Letter Grade | Numeric Grade |
F | 0-60 |
D | 61-70 |
C | 71-80 |
B | 81-90 |
A | 91-100 |
(HINT: Remember how the interpreter executes an if statement)
2. Modify the previous program, to also display a + or - after the letter. The +/- modifier is based on the last digit of the numeric grade:
Last Digit | Modifier |
1-3 | - |
4-7 | <blank> |
8-0 | + |
ex. 81 = B-, 94 = A, 68 = D+. Remember, and F is only an F, you cannot get an F-
(HINT: Remember the modulus operator? `%` ?
EVEN BIGGER HINT:
irb(main):001 > 1 % 10 => 1 irb(main):002 > 11 % 10 => 1 irb(main):003 > 21 % 10 => 1
3. Given an amount of money (less than $1.00), compute the number of quarters, dimes, nickels, and pennies needed.
4. A leap year is any year divisible by 4, unless the year is divisible by 100, but not 400. Write a program to tell if a year is a leap year.
5. Write a program that, given the number of hours an employee worked and the hourly wage, computes the employee's weekly pay. Count any hours over 40 as overtime at time and a half.
6. EXTRA HOMEWORK CHALLENGE:
If the previous challenges did not satisfy you, here's the REAL challenge. For this challenge, you will need to go out and learn about Ruby's "Range" data type. Do not worry, I will teach you about this data type in the future.
http://www.ruby-doc.....9.3/Range.html
Now, the challenge will be to redo the first 2 challenges, using the Range class.
HINTS
To create a Range data type, the shorthand is:
variable = 0..5 variable = 0...5
0 is the (minimum) starting number
5 is the (maximum) ending number
.. means 0 to 5 inclusive (0, 1, 2, 3, 4, 5)
... means 0 to 5 exclusive (0, 1, 2, 3, 4)
To solve this new challenge, you will need to find out how Ranges behave within a case statement (HINT: look up the === method)
The following is a few extra points about "if" statements. These are non-essentially to learning if statements, but you may find them QUITE useful (I know I do). I left them out of the main lesson because I didn't want to overload you with too much. I would advise that you first ensure you are comfortable with using if statements, before learning these.
For almost every ruby statement, there is an opposite to that statement. For example, puts/gets. puts prints a string and adds a new-line; gets takes a string and adds a new-line.
For if, we have "unless."
unless variable == 5 # Do this end
That would be the same as writing:
if variable != 5 # Do this end
or, more specifically
if !(variable == 5) # Do this end
Unless works exactly like if, except you cannot use "elsif", and of course, it evaluates the <condition> then inverses the result. Unless works best with single sided-decisions, and statement modifiers (easiest to read), so, be careful, as unless can get confusing fast, especially when testing complex statements, like:
unless (true and false) or true puts "false" else puts "true" end
The output would be:
true
This is because (true and false) or true is true, (true and false) = false, (false or true) = true, then unless executes the else block, because the condition was true. So,
unless !(true and false or true) puts "false" else puts "true" end
would produce:
false
Confusing? It can be. Use your discretion when using the unless statement.
Sometimes, we only need do execute one line of code, based on a specific condition, ex.
if <condition> variable = 0 end
However, ruby provides us a simple, neat way to write this in one line of code:
variable = 0 if <condition>
this Works EXACTLY like the previous statement. With statement modifiers, you can ONLY have a one-sided decision, and can ONLY have one line that is executed.
This also works for unless statements:
variable = 0 unless <condition>
(puts variable; variable = 0) if variable != 0
However, this is not preferable. It makes the code more difficult to read. But, nonetheless, in some cases it might be okay to do this, just do what feels best for you.
0 Comments
Recommended Comments
There are no comments to display.
Please sign in to comment
You will be able to leave a comment after signing in
Sign In Now