Overview
By the end of the chapter, you will be able to utilize Boolean operators in Ruby programs; create and implement conditional expressions in Ruby; utilize ternary operators and ranges for programs; implement iterators on arrays and hashes and create programmatic loops with while, for, and until. The chapter aims to introduce us to different methods and entities involved in the Ruby program workflow.
In the previous chapter, we studied the concept of arrays and hashes in Ruby. We also looked at different methods applied to arrays and hashes. In this chapter, we will be looking at how programs in Ruby are designed and used in applications.
Useful software programs are not simply a linear set of instructions; they make decisions about what code to run at any given time based on a set of conditions or criteria. Different programming languages have different types of program flow options. Two of the most common types of program flow options are conditionals and loops, which we will cover in this chapter.
Conditionals, also known as branches, are like a fork in the road. Do you turn left, or do you turn right? In the case of programming, for instance, you can decide what to do if a variable equals a value or is less than or greater than a certain value. Software simplifies the problem and makes the decision simply about truthiness: whether a condition is satisfied or not. A common example is evaluating a user's password and logging them in if the user-supplied value matches the value in the database or showing an error message if it does not.
Loops are another way to control program flow. A section of code will be repeated in a loop until a condition is met. In programming, you often want to loop over a collection of data such as an array and process the values in that collection. In some cases, the condition may never be met as the program may be configured to run forever. A web server, for instance, loops forever, waiting for new requests to come in to serve content.
In all types of program flow decisions, the fundamental unit is the Boolean. The Boolean represents truthiness or true or false values. We use the word "truthiness" to express that some values may not be an exact true or false Boolean but will be considered by the language as behaving as "true" or "false".
Since program flow is decided by truthiness, which is represented by Booleans, we need to learn how Booleans operate. We can do this by first learning about Boolean operators; there are three: AND, OR, and NOT. This is how we will begin our chapter.
As we know, Booleans tell us whether a value or condition is true and false. We can work with Booleans using the following three operators:
In Ruby, the AND operator is represented by a double ampersand, &&. It represents what the truthiness is across two values if both are true. Consider the following sample code snippet:
var1 = true
var2 = true
var1 && var2
var1 = false
var2 = true
var1 && var2
var1 = false
var2 = false
var1 && var2
The output should look like this:
In the preceding example, the var1 and var2 variables depict true and false Boolean states in different combinations and the && operator gives results as per the combination.
In Ruby, the OR operator is represented by a double pipe, ||. It represents what the truthiness is across two values if one is true. Consider the following sample code snippet:
var1 = true
var2 = true
var1 || var2
var1 = false
var2 = true
var1 || var2
var1 = false
var2 = false
var1 || var2
The output should look like this:
As depicted in the previous example, the var1 and var2 variables depict true and false Boolean states in different combinations and the || operator gives results as per the combination.
In Ruby, the NOT or negation operator is represented by a bang or exclamation point (!). It represents the opposite of a value. If a value is true, the negation of it is false, and if a value is false, the negation of it is true:
var1 = true
!var1
var1 = false
!var1
The output should look like this:
The possibilities of these operators are best described with a truth table. Here is a sample truth table for the AND and OR operators:
As you can see from this table, there are some rules that can be inferred:
The following table is a truth table for the NOT operator:
As seen in the preceding table, the following statements can be inferred:
Truth tables are the base of Boolean algebra, which is beyond the scope of this book. However, let's cover two additional interesting properties that occur when we combine the negation operator with the AND and OR operators.
Consider the following statements:
Another way of looking at this is to say if you want to negate an operator used with two variables, then you distribute the negation to each variable and "flip" the operator. Flipping the operator means to switch between && and ||.
We can confirm this with a truth table, which is just a representation of two variables and all the possible combinations of true and false and the result of each operator:
Here is another truth table:
This is an example of one of the many laws of Boolean algebra.
Understanding Booleans and truthiness is a foundational concept that will lead us through to the use of all other aspects of program flow in Ruby.
As we mentioned, in Ruby, you can have Booleans that are direct representations of true and false. However, other values, such as strings and numbers, can be evaluated as to whether they should be true or false.
Take the number 0, for instance. Is the number 0 a true or false value? In other words, if we had an if condition statement that evaluated 0, Ruby should choose the true branch or the false branch. This is what we mean by "truthy." If a variable is "truthy," it means it doesn't contain a direct true value but will be treated as true for the purposes of program flow or Boolean evaluation. Different languages handle truthiness in different ways. Here is the truthiness table for Ruby:
As you can see, 0 evaluates to truthy In fact, in Ruby, almost everything evaluates to true except for nil and false. We can see this in an IRB console using a special trick: the bang, (!), and the double bang, (!!).
! is the negation operator in Ruby and therefore gives us the Boolean opposite of whatever value we are evaluating. When we apply the double bang operator, !!, we will get the Boolean opposite of the variable, which if you are following, gives us the Boolean value of the variable.
Let's look at the following example:
def is_truthy(var)
if var
puts "The var #{var} is truthy!"
else
puts "The var #{var} is falsey"
end
end
is_truthy(0) # "The var 0 is truthy
is_truthy(nil) # "The var is falsey"
is_truthy(false) # "The var false is falsey"
is_truthy("") # "The var is truthy!"
is_truthy(5) # "The var 5 is truthy!"
is_truthy("hello") # "The var hello is truthy!"
is_truthy(!0) # "The var false is falsey"
is_truthy(!!0) # "The var true is truthy!"
is_truthy(!nil) # "The var true is truthy!"
is_truthy(!!nil) # "The var false is falsey"
The output should look like this:
In Ruby, there are both && and and as well as the || and or syntaxes. They serve the same purpose in that they both do a logical comparison, however, there is a small and notable difference in how they are interpreted by Ruby. The difference lies in the precedence of operators. In other words, Ruby will process some operators sooner than other operators.
The following is a table on operator precedence in Ruby
We can see that the logical && and || operators have higher precedence than the AND and OR operators. In most cases, the difference in precedence is negligible. However, there are a couple of situations where it is important, particularly when doing variable assignment:
var1 = true AND 1 # var1 == true
var2 = true && 2 # var2 == 2
In the preceding example, var1 is true because the variable assignment has higher precedence than the"and operator. However, var2 is equal to 2 because the && operator is evaluated first and is then assigned to the variable. The astute reader will note that even though true && 2 is a logical comparison, the assigned value is 2. Ruby will return the actual value of logical comparison if it is true. The value it returns depends on the operator.
Consider the following example:
var1 = 5 || true # var1 == 5
var2 = true or 5 # var2 == true
var3 = false || nil # nil
In the case of var1, we see that even though we are using the || operator, which has high precedence, var1 is getting assigned 5. This is because when Ruby processes the || operator, it will stop processing once it has satisfied the || operator (that is, it has reached a true condition). In the case of var2, the assignment operation happens first (var2 is assigned to true) and then is or'd with 5 (which is also true). In the case of var3, the Ruby interpreter can't satisfy the or condition with false, so it proceeds and then hits nil, which is then returned to the assignment operation and gets assigned to var3.
The general rule of thumb is that it is safe to use and and or in a program flow (for example, if/else conditions), whereas you always use && and || when doing variable assignment. While the and and or syntax is more readable than && and ||, it avoids sinister bugs that come up due to oversights in the order of precedence. Therefore, it is recommended to always use && and ||.
In this exercise, we will demonstrate how Boolean operators work by creating a method that evaluates each of the operators. We can then pass different values in to see how the Boolean operators work. The following steps will help you with the solution:
def truthiness(x, y)
puts "AND: ", x && y
puts "OR: ", x || y
puts "!AND: ", !(x && y)
puts "!OR: ", !(x || Y)
end
truthiness(true, true)
truthiness(true, false)
truthiness(false, true
truthiness(false, false)
truthiness(0, true)
truthiness(nil, false)
Here's the expected output:
Now that we are well-versed in Booleans and truthiness, we can begin to dive into program flow. We'll begin by discussing branches, which are also known as conditionals.
There are several keywords that denote conditionals:
In Ruby, conditionals are structured into blocks of code that begin with a conditional keyword and end with an end keyword.
Let's look at each of them, one by one.
The if conditional is used to evaluate the existing condition of a variable and display the result accordingly. Consider the following example:
good_weather = true
if good_weather
puts "Go outside"
end
The output should look like this:
Here, we initialize a good_weather variable, which will represent whether the weather is good. If it's good weather, we will output a statement telling us to go outside.
Next, we will see how to branch our code to handle good and bad, which are the weather conditions.
An else condition will help provide an alternate result if the desired condition is not satisfied. Consider the following example:
good_weather = true
if good_weather
puts "Go outside"
else
puts "Stay inside"
end
The output should look like this:
In this example, if the weather is good, we will get an output statement: Go outside. If not, the statement will be Stay inside.
We can use the else keyword to represent an either/or conditional. If we have more than two choices, we can use the elsif keyword to match multiple conditions. Look at the following example:
bananas_are_fresh = true
oranges_are_fresh = true
if bananas_are_fresh
puts "Make a smoothie"
elsif oranges_are_fresh
puts "Make orange juice"
else
puts "Go to the farmers market to get more fresh fruit"
end
In the preceding example, there are two variables, bananas_are_fresh and oranges_are_fresh, which are set to true. If the first condition is met, the output statement will be Make a smoothie. If the second condition is also met, the output will be Make orange juice. If both conditions are not met, the output will be Go to the farmers market to get more fresh fruit.
The output should look like this:
If statements, like most other programming languages, are saying if the following evaluation is true, then execute the next block of code. Ruby has an additional keyword to express a conditional when something is false. That keyword is unless. Look at the following example:
its_raining = false
unless its_raining
puts "Go outside"
end
The output should look like this:
As you might guess, the unless keyword is the same as saying if not. While Ruby has elsif for additional conditions within an if/else block, there is no corresponding keyword for unless. Therefore, unless can only be paired with else, as in the following example:
its_raining = false
unless its_raining
puts "Go outside"
else
puts "Stay inside"
end
When deciding which keyword to use, one of the considerations should be readability. Which form makes it more readable to you and/or for others later on?
Let's look at an example that combines conditional keywords with Boolean operators:
its_raining = false
its_cold = true
if its_raining && its_cold
puts "Bring rain jacket"
elsif its_raining
puts "Bring umbrella"
else
puts "It's nice outside, just don't forget your wallet"
end
The preceding code clearly depicts the use of the && Boolean operator to combine two conditions and give out the result accordingly. Additionally, if one of the conditions is met, the output will be Bring umbrella. If both conditions are not met, the output would be It's nice outside, just don't forget your wallet.
The output should look like this:
Note
It's good practice to steer clear of excessively lengthy and nested if/else statements. This is called a "code smell" because it makes you wince when you try to understand it. It's a good time to consider whether there is a better way to organize the code.
We've learned how to evaluate the truthiness of a statement and how to branch code based on that truthiness. The last foundational concept in conditional program flow is how to compare two variables. We need to be able to determine whether two variables are the same, are not the same, or are less than or greater than each other.
Let's look at the operators used for comparison in Ruby.
The following are the operators most commonly used for comparison in Ruby:
Let's now write a method that combines all of the concepts in this chapter. We will be comparing two variables, x and y, and putting them through different comparison conditions:
def compare(x, y)
if x < y
puts "#{x} < #{y}"
elsif x <= y
puts "#{x} <= #{y}"
elsif x == y
puts "#{x} == #{y}"
elsif x >= y
puts "#{x} >= #{y}"
elsif x > y
puts "#{x} > #{y}"
end
end
While this code appears to represent all the cases of each operator, it's actually problematic. We won't possibly encounter the == case, because if we evaluate to true for <=, this will evaluate first and we won't encounter the == case. In fact, we won't even run into the >= case.
Hence, a better way to write a comparison method would be as follows:
def compare(x,y)
if x==y
puts "x and y are equal"
elsif x < y
puts "x is less than y"
else
puts "x is greater than y"
end
end
The choice of whether to use > and >= or < and <= depends on the situation and the problem you are trying to solve. This is called a boundary condition.
It is convenient to use comparison operators with integers, but it can be difficult with other types of variables, such as strings. Each object type (or class) in Ruby will define how to compare itself to other objects of the same type. For instance, when it comes to comparing two strings, we can think of it as follows:
"this string" == "something else" # false
That is straightforward, but the following condition is unrealistic:
"apples" > "oranges"
Let's use our compare method from the previous example to see how lots of different inputs behave:
compare("apples", "oranges") # x is less than y
compare("a", "a") # x and y are equal
compare("a", "A") # x is greater than y
compare("a", "b") # x is less than y
We are seeing some surprising things here. Why is a greater than A but less than b? The reason is because of how Ruby implements that comparison operator on strings. Most people will not memorize the internal algorithms of Ruby, so this is a good opportunity to read the docs.
When you go to the online Ruby documentation at https://packt.live/35mif9C, search for the type of object (or class) that you are looking for. In this case, string. There will be several results, usually for different versions of Ruby. Find your version of Ruby and dive into the string class. You will notice that there isn't an entry for <, >, or ==. Instead, Ruby implements all comparison operators with a single method: <=>, which returns -1, 0, and 1 to indicate less than, equal to, or greater than. All the other comparison operators (which really are just methods) are based on this central comparison operator. When you click on <=>, you will get an explanation of how Ruby decides to compare strings.
In this exercise, we will write a method to determine whether you have enough money to purchase something. We will determine the logic necessary for our balancer checker method and then implement it using conditionals and comparison operators:
def can_purchase?(amount_in_bank, cost_of_item)
if amount_in_bank >= cost_of_item
return true
else
return false
end
end
Here, we use the >= operator because as long as we have an amount equal to or greater than the cost of the item, we are okay to purchase it.
def can_purchase?(amount_in_bank, cost_of_item)
if amount_in_bank < cost_of_item
return false
else
return true
end
end
Note
It's up to you as the programmer to decide which is easier to read. You will also notice that we named our method with a question mark. This is good practice with methods that return Boolean values.
bank_balance = 100
cost_of_item = 200
if can_purchase?(bank_balance, cost_of_item)
puts "You can purchase this item"
else
puts "Sorry, you don't have enough money to buy this item"
end
You can see how when you write code with clearly named variables and method names, the resulting code is quite readable, even to people who are new to Ruby.
Here's the expected output:
Thus, we have successfully created a method to initiate/decline a purchase after checking the balance amount of the bank account by using conditionals and comparison operators.
Often, when programming, you will need to branch code based on many conditions. Using lots of if/else statements can result in spaghetti code, which is not easy to read or understand.
Spaghetti code refers to a pejorative term where the code is unstructured and normally has many goto statements, making it difficult to interpret.
An alternative to lots of if/else statements is to use the case/when keywords. Here is an example:
def get_animal_sound(animal_type)
case animal_type
when :dog
"woof"
when :cat
"meow"
when :cow
"moo"
when :bird
"tweet"
else
nil
end
end
Here, we have a method that depends on the type of animal we pass into it and returns a string with the sound it makes. This works by doing a comparison of the animal_type variable against each value in the case/when statement, going from top to bottom.
Try calling the methods with the following values:
puts get_animal_sound(:dog) # woof
puts get_animal_sound(:bird) # tweet
puts get_animal_sound("dog") # nil
The last call did not return the correct sound because strings and symbols are unequal and so the execution will continue until it hits the final else condition in the block.
An alternative format in the case/when statement is to leave out the variable to the right of the case keyword. In this format, Ruby will execute the statement following an evaluation to true.
Consider the following example:
def get_animal_sound(animal_type)
case
when animal_type == :dog
"woof"
when animal_type == :cat
"meow"
when animal_type == :cow
"moo"
when animal_type == :bird
"tweet"
else
nil
end
end
In this case, the expanded format is less ideal than the first since we are repeating the animal_type variable for each condition. However, this format can come in handy when you don't know all the cases ahead of time or need to do more complex logic such as doing a greater than or less than comparison.
You might naturally wonder about the difference between == and ===. First, let's remember that the == operator is actually a method that each Ruby object type (class) defines for itself so it knows how to compare two objects of the same type. The === operator is an additional method that Ruby gives us to do a similar but slightly different equality comparison. The === operator is often called the "case equality" operator or the "identity" operator. It allows comparison based on the type of object passed into it. Another way to put it is that the === operator determines whether the input passed to it is part of the set of what it's being compared to.
Consider the following example:
def guess_type(input)
case input
when String
puts "It's a string!"
when Integer
puts "It's a number!"
when Array
puts "It's an array"
when Hash
puts "It's a hash"
else
puts "Not sure what you passed: #{input}"
end
end
guess_type(5)
guess_type("5")
guess_type([1,2,3])
guess_type({foo: :bar})
The output should look like this:
What's happening here is the case statement is calling the === operator on the input and determining whether the input is part of the set. In this case, it is determining whether the type of object passed in is a type that belongs to the class it's being compared to.
Here are some further examples of the === operator:
String === "mystring" # true
Integer === 5 # true
5 === Integer # false?
Here, you can see in the last statement that something strange is going on. Integer === 5 holds true but 5 === Integer is shown as false. The reason is, while 5 is a type of Integer, Integer is a class and is not a type of 5. This concept will become clearer as we cover classes and ranges in upcoming chapters.
Ternary means composed of three parts. In Ruby (and also in other programming languages), there is a common programming idiom called the ternary operator, which allows a quick if/else conditional to be put on one line by breaking it into three parts:
user_input = 'password'
secret = 'password'
user_has_access = user_input == secret ? true : false
The three parts are the condition (user_input == secret), the statement if true (true), and the statement if false (false). The three parts are separated first by a ? and secondly by a : notation.
While Ruby doesn't always require syntax such as parentheses, the preceding statement may be a bit hard to read unless you are very familiar with how Ruby handles the order of operations. Here is a clearer way of writing the preceding code:
user_has_access = (user_input == secret) ? true : false
The ternary operator is great for quick one-liners. However, if lots of complex logic is starting to creep into any of the three parts, this may be considered a code smell and the code will need refactoring into simpler logic. You can either refactor it into a proper if/else statement or you can refactor the complex logic into separate variables that are then evaluated in the ternary condition.
Consider the following example:
user_input = 'secret'
password = 'secret'
def login_user
puts "logging in user"
end
def show_access_denied
puts "Password was incorrect, try again"
end
user_has_access = user_input == secret
user_has_access ? login_user : show_access_denied
By using clearly labeled method names and simplifying the ternary statement, we can clearly see that if the user has access, we will log them in; otherwise, we will ask them to re-input their password.
Write a method that determines the speed that a self-driving car should adjust to, based on environmental conditions, as well as the traffic status and distance to a traffic light. The method should return the new speed. Perform the following steps:
Green light:
Sunny, all conditions: speed_limit
Rainy, distance >= 50ft: speed_limit
Rainy, distance < 50ft: 90% speed_limit
Yellow light:
Sunny, distance >= 50ft: 80% speed_limit
Sunny, distance < 50ft: 50% speed_limit
Rainy, distance >= 50f: 80% speed_limit
Rainy, distance < 50f: 25% speed limit
Red light:
Sunny, distance >= 50ft: 50% speed limit
Sunny, distance <= 50ft: 0% speed limit
Rainy, distance >= 50ft: 25% speed limit
Rainy, distance <= 50ft: 0% speed limit
Note
We are introducing raise here, which will fatally exit the program if the code is encountered. This is a basic way to make sure that if a parameter is not passed in correctly, we get notified about it.
def drive_decision(traffic_signal, weather, distance_to_signal, speed_limit)
if traffic_signal == :green
if weather == :sunny
speed_limit
elsif distance_to_signal >= 50
speed_limit
else
speed_limit * 0.9
end
elsif traffic_signal == :yellow
if weather == :sunny && distance_to_signal >= 50
speed_limit * 0.8
elsif weather == :sunny && distance_to_signal < 50
speed_limit * 0.5
elsif weather == :rainy && distance_to_signal >= 50
speed_limit * 0.8
elsif weather == :rainy && distance_to_signal < 50
speed_limit * 0.25
else
raise "Condition not handled"
end
else # red light
if weather == :sunny && distance_to_signal >= 50
speed_limit * 0.5
elsif weather == :rainy && distance_to_signal >= 50
speed_limit * 0.25
else
0 # all other conditions should stop the car
end
end
end
Note
raise is a keyword that raises exceptions. Exceptions can be caught and handled – this topic is covered later in the book.
drive_decision(:green, :sunny, 100, 50)
drive_decision(:yellow, :rainy, 50, 25)
def drive_decision_when_green(weather, distance_to_signal, speed_limit)
case weather
when :sunny
speed_limit
when :rainy
if distance_to_signal >= 50
speed_limit
else
speed_limit * 0.9
end
else
raise "Not handled"
end
end
def drive_decision_when_yellow(weather, distance_to_signal, speed_limit)
case weather
when :sunny
if distance_to_signal >= 50
speed_limit * 0.8
else
speed_limit * 0.5
end
when :rainy
if distance_to_signal >= 50
speed_limit * 0.8
else
speed_limit * 0.25
end
else
raise "Not handled"
end
end
def drive_decision_when_red(weather, distance_to_signal, speed_limit)
if distance_to_signal >= 50
case weather
when :sunny
speed_limit * 0.5
when :rainy
speed_limit * 0.25
else
raise "Not handled"
end
else
0
end
end
def drive_decision(traffic_signal, weather, distance_to_signal, speed_limit)
case traffic_signal
when :green
drive_decision_when_green(weather, distance_to_signal, speed_limit)
when :yellow
drive_decision_when_yellow(weather, distance_to_signal, speed_limit)
else
drive_decision_when_red(weather, distance_to_signal, speed_limit)
end
end
Here's the expected output:
We can see, in the preceding code, that we are repeating code in the case statement. We can refactor this code by creating a new method that checks for valid traffic, weather states and writing this as a one-liner at the top of a method definition like this:
def drive_decision(traffic_signal, weather, distance_to_signal, speed_limit)
raise "Unhandled weather condition" unless valid_weather_condition?(weather)
#...rest of the method implementation…
end
def valid_weather_condition?(weather)
[:sunny, :rainy].include?(weather.to_sym)
end
This line of code, which starts off a method implementation, is called a guard clause because it makes a quick decision about whether we should proceed with the method implementation. In other words, it guards the method implementation by making sure only valid parameters are passed in. Guard clauses are a great way to refactor complicated if/else conditionals by checking for these conditions at the top of a method implementation with a guard clause. Then, you can keep the rest of your method implementation clean of additional checks on the data.
Loops are a type of program flow that repeat blocks of code. The ability to repeat a block of code allows us to do things such as processing collections of data. Typically, loops will run until a condition is satisfied. For instance, a loop may be run until it has run a certain number of times, or a loop may be run until it has processed all the items in a collection of data. Let's look at the following types of loops in Ruby:
Another foundational concept for program flow is being able to repeat sections of code until a condition is met. These are called loops and in Ruby there are many ways to create loops. The most basic structure of a loop contains two things:
Here is a simple block using the while keyword:
while true do
puts Time.now
end
It should be fairly clear what this loop does. We can write this loop in English by saying, while true, output the current time:
In this case, true is obviously always evaluated to true and therefore this will loop forever. In other words, this is an infinite loop. Infinite loops are important concepts because they should either be intentional in the design of the program or otherwise avoided. An intentional infinite loop might be a program that runs forever, such as a web server.
Let's try another while loop that gets user input each time, which will determine whether we keep looping again or not.
In this exercise, we will write a method that picks a random number and asks us, the user, to try to guess it. The random number will be picked from a range of numbers supplied as a parameter to the method.
The following steps will help you to complete the exercise:
def mind_reader
end
def mind_reader(range)
magic_number = rand(range)
puts "The magic number is #{magic_number}"
end
def mind_reader(range)
magic_number = rand(range)
puts "What's your guess?"
guess = gets.chomp.to_i
if guess == magic_number
puts "That's right!"
else
puts "Sorry, that's not correct. The correct number is: #{magic_number}"
end
end
The output should look as follows:
def mind_reader
magic_number = 7
guess = nil
while guess != magic_number do
print "Nope! Try again! " unless guess.nil?
puts "What's your guess?"
guess = gets.chomp.to_i
end
puts "That's right!"
end
The output should look as follows:
def mind_reader
magic_number = 7
max_guesses = 3
attempts = 0
guess = nil
while guess != magic_number do
print "Nope! Try again! " unless guess.nil?
puts "What's your guess?"
guess = gets.chomp.to_i
break if attempts >= max_guesses
end
puts guess == magic_number ? "That's right!" : "You ran out of guesses, try again later!"
end
In this iteration, we establish the maximum number of guesses and keep track of how many attempts the user has made. However, this code could result in an infinite loop if the user never guesses the right number. We need to increment the number of guesses we are on. Let's add that in.
def mind_reader
magic_number = 7
max_guesses = 3
attempts = 0
guess = nil
while guess != magic_number do
print "Nope! Try again! " unless guess.nil?
puts "What's your guess?"
guess = gets.chomp.to_i
break if attempts >= max_guesses
attempts += 1
end
winner = "You've guessed it!"
loser = "You ran out of guesses, try again later!"
puts guess == magic_number ? winner : loser
end
Here's the output:
Thus, we have created a method where we cover all the different conditions of guessing a number.
while loops run while a condition is true. Conversely, Ruby has included the until/do construct, which runs code while a condition is not true or rather until the code becomes true. It is understandable that this converse logic can get confusing. It is similar to if and unless statements though. if statement blocks are run when a condition is true, whereas unless statement blocks are only run when a condition is false.
Consider the following example:
bank_balance = 0
cost_of_vacation = 1000
until bank_balance >= cost_of_vacation do
bank_balance += 50
end
The loop clearly indicates that until bank_balance is greater than the defined cost_of_vacation variable, it will keep incrementing bank_balance.
The output should look like this:
The previous loops first evaluate the condition before running the block of code. Sometimes, you want to run the block of code at least once before evaluating whether you would like to repeat it. Let's refactor our mind_reader method to do this:
def mind_reader magic_number
max_attempts = 3
attempt = 0
guess = nil
loop do
print "What's your guess?"
guess = gets.chomp.to_i
break if attempt >= max_attempts
break if guess == magic_number
attempt += 1
puts "Nope! Try again"
end
puts guess == magic_number ? "That's right!" : "You ran out of guesses, try again later!"
end
Here, we are using the break keyword to exit the loop based on a condition. The break keyword is useful because it allows us to jump out of the loop at any time, not just after the full block has been executed. This allows us to output the Try again statement at the end but only if we really are trying again.
The output should look like this:
Here is another form of the do/while syntax:
keep_looping = :yes
begin
print "Should we keep looping? "
keep_looping = gets.chomp.downcase.to_sym
end while keep_looping == :yes
The output should look like this:
Here, we are establishing a block of code using the begin and end keywords. Begin and end blocks are useful not just for do/while loops but also for defining blocks of code for catching exceptions or errors in code. We will learn more about this later in the book.
The loops we've seen so far are based on a condition that determines whether we should repeat the block of code or not. We can also decide to loop code blocks based on a set of data such as an array. This is known as iterating over a set. The following are common methods used to iterate over collections of data:
each is one of the most common methods for iterating over sets of data in Ruby. For instance, if we have an array of three items, we can decide to loop over each item in the array:
[1,2,3].each do |i|
puts "My item: #{i}"
end
The output should look like this:
Here, we have a three-item array and we call the each method on the array. This method returns an Enumerator, as follows:
[1,2,3].each.class #Enumerator
An enumerator is a Ruby class that allows iterations across a set of data. In order to iterate across a set of data, we also need a block of code and that block of code needs access to the singular item that is being worked on (or iterated over). This is where the do |i| syntax comes in. Do defines the block of code and |i| is the item in the array that the iterator is passing to the block of code. This is known as yielding a variable to the block.
The each method, as the name implies, simply iterates over each item in the array, passing it to the block. each is a core method that operates not just on arrays, but also any collection of data that includes hashes. Consider the following example:
parameters = {id: 1, email: "[email protected]", first_name: "Dany", last_name: "Targaryen"}
parameters.each do |key, value|
puts "#{key} has value: #{value}"
end
In the preceding code, we start by initializing a hash called parameters with some basic data of key-value pairs. We then call the each method on the parameters hash. Each key-value pair is yielded as separate arguments to the block within || characters. In the preceding example, the arguments are called key and value, but they can be have different names.
The output should look like this:
We can see that when we iterate over a hash, the block takes two arguments, the key and the value for each key/value pair in the hash. It's up to each collection type to decide how it implements the each method and what arguments it will provide to the block. There is no limit to the number of arguments; it just depends on what is appropriate for the collection type. In most cases, there will either be one or two arguments in the block.
The each_with_index method is very similar to the each method. However, as the name implies, this method not only provides the enumerated item in the loop but also the index of the item in the array. An example of when this is useful is when you may need to know the next or previous item in the array:
order_queue = ["bob", "mary", "suzie", "charles"]
order_queue.each_with_index do |person, index|
puts "Processing order for #{person} at index: #{index}"
if index < order_queue.length - 1
puts "Next up is: #{order_queue[index+1]}"
else
puts "#{person} is last in the queue"
end
end
The output should look like this:
Now, let's call each_with_index on a hash:
parameters = {id: 1, email: "[email protected]", first_name: "Bob"}
parameters.each_with_index do |key, value|
puts "Key: #{key}, Value: #{value}"
end
The output should look like this:
Here, we see that the key is actually an array of the key/value pair, and the value is the index. This is interesting. What's happening here is that the hash implements the each method so that it returns a key and value to the iterator block. Now that we are calling each_with_index, the implementation is also going to send through the index to the block. You might be tempted to do the following to get each key, value, and index in separate variables:
parameters = {id: 1, email: "[email protected]", first_name: "Bob"}
parameters.each_with_index do |key, value, index|
puts "Key: #{key}, Value: #{value}, index: #{index}"
end
The output should look like this:
We can see this doesn't get us what we want. Let's try it differently:
parameters = {id: 1, email: "[email protected]", first_name: "Bob"}
parameters.each_with_index do |(key, value), index|
puts "Key: #{key}, Value: #{value}, index: #{index}"
end
The output should look like this:
There we go. We've got each key, value, and index into separate variables by wrapping the key and value in parentheses in the block parameters. This is a special syntax in block parameters that allows arrays to be split into separate variables.
Often, we want to iterate on a collection, process those items, and return a new collection of items. This is where the map and collect methods come in. The following is the implementation of the map method:
[1,2,3].map do |i|
i + 5
end # [6,7,8]
The output should look like this:
The map method, similar to the each method, iterates over each item in the array, but it collects the result of each iteration of the block in a new array. As you can see in the preceding example, the new array is the result of each item plus five.
Something to keep in mind here that is not obvious about Ruby is that the last line of the block is what's returned to the iterator. You might think that you should use the return keyword but that is not how Ruby is implemented. We will learn more about methods and the return keyword in the next chapter.
Also, an alternative syntax you can use to define the iteration loop is with curly braces, as follows:
[1,2,3].each {|i| puts i}
The output should look like this:
In most cases in Ruby, you can replace do/end blocks with {} but there are some nuances, especially when working within IRB, to look out for.
Iterators are extremely handy for looping over any set or collection of data. For instance, here is how you can iterate over a hash that is really a collection of key/value pairs:
address = {country: "Spain",city: "Barcelona", post_code: "08001"}
address.each do |key, value|
puts "#{key}: #{value}"
end
The output should look like this:
Here, we call the each method on the hash, and what is yielded to the block are two variables. Of course, the names of the variables passed to the block do not matter, but the position of them does. The Ruby hash is implemented such that it iterates over each key/value pair, and, for each pair, the key is yielded as the first variable and the value is yielded as the second variable.
There is no limit to what can be yielded to the block; it just depends on the collection being iterated over and how it is implemented. Let's take a look at how to implement our own iterator to learn more about how yielding works.
Imagine you have an array of product prices and you want to write a method that applies an arbitrary tax rate to them:
def with_sales_tax array
array.map do |item|
yield item.to_f
end
end
prices = [5,25,"20",3.75,"5.25"]
sales_tax = 0.25
new_prices = with_sales_tax prices do |price|
price * sales_tax
end
The output should look like this:
Here, we have implemented a method that loops over the array passed in as a variable to the method and performs a common operation, .to_f, in order to sanitize the input and make sure all the items are floating-point numbers. Then, the method yields to the block associated with the call to the method. The transformation (adding a sales tax percentage) is the last line of the block, which is then passed as the last line of the call to map inside the method, which collects the sanitized and transformed prices and passes them back as the result of the method.
Blocks and yielding are one of the trickiest parts of learning Ruby when you first get started. Let's explore another way of writing the same code:
def with_sales_tax(array, &block)
array.map do |item|
block.call(item.to_f)
end
end
prices = [5,25,"20",3.75,"5.25"]
sales_tax = 0.25
new_prices = with_sales_tax prices do |price|
price * sales_tax
end
The output should look like this:
In this form, the block is explicitly passed as an argument to the method. An ampersand is used as a prefix to tell the Ruby interpreter that the argument is a proc object. A proc object is a block of transportable, runnable code and will be covered in Chapter 11, Introduction to Ruby on Rails I.
In this exercise, we will develop a method that accepts an array of numbers. It will yield two elements in the array to a block that performs a mathematical operation on them (addition, subtraction, division, and so on) and returns the result to be processed in the next iteration:
def math(array)
end
def math(array)
first_item = array.shift
while(array.length > 0) do
second_item = array.shift
first_item = yield first_item, second_item
end
return first_item
end
The output should look like this:
You'll notice that we capture the result of the block in the first_item variable. This is an important point. The return value of the block (even though you don't specify the return keyword) is what comes back from yield and you can capture it in a variable.
In our exercise, we then return to the top of the while block and evaluate whether there are items in the array to continue processing. If so, we shift the next one along as our second argument. In this case, now, our first argument is actually the result of the previous yield call.
While we've called this an arbitrary math function, really this implementation can be used for any processing of subsequent items in a set of data. This is actually how you could implement map/reduce, which is a programming paradigm for processing large datasets very quickly and is a common paradigm in functional programming languages.
def math(array)
first_item = array.shift
while(array.length > 1) do
second_item = array.shift || 0
third_item = array.shift || 0
first_item = yield first_item, second_item, third_item
end
return first_item
end
The output should look like this:
You'll notice that we modified a couple of lines to handle nil references:
second_item = array.shift || 0
third_item = array.shift || 0
Because we are processing three items at a time, this allows arrays with lengths that do not divide evenly into three to still be processed. This works great for addition, but will it work for all the other mathematical operations?
Also, a good follow-up exercise would be to implement this method and parameterize the number of items to process at a given time. As such, the usage would look like this:
math([3,4,5,6], 2) { |a,b| a + b }
math([3,4,5,6], 4) { |a,b,c,d| a + b + c + d }
In this activity, write a high/low guessing game. The computer will pick a random number and the player needs to guess what it is. If the player guesses incorrectly, the computer will respond with a higher or lower statement, indicating that the player should guess a higher or lower number, respectively. The program exits when the player guesses correctly.
The following steps will help you with the solution of the activity:
Similarly, if the input is No, display a thank you message, as shown in the following figure:
Here's the expected output:
Note
The solution for the activity can be found on page 462.
In this chapter, we learned about program flow. Program flow is a foundational concept in programming that allows programmers to change execution paths dynamically depending on any number of conditions. A core concept within program flow is understanding the Boolean data type along with its truthy operators, AND (&&) and OR (||).
We learned that Ruby supports different program flow options, such as if and unless, which are logical inverses of each other. The decision to use one or the other depends on the programmer, who should opt for readability and maintainability.
We also learned how to loop and were introduced to Ruby blocks, which are bundles of code that get executed along with each iteration of a loop. Bundling code into reusable chunks is also a foundational concept. Another way to bundle code is by using methods, which we've looked at only briefly so far. In the next chapter, we will go into greater depth about methods by learning how to define them, how to set different types of parameters, and how to return values from them.
18.191.237.79