
The Ternary Operator
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.
Exercise 3.03: Speed Determiner
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:
- Open a new session on IRB.
- First, we write the logic in pseudocode:
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
- Implement the logic in a method. Define the method as drive_decision and also the parameters we consider in the method.
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.
- Evaluate the method with different variables:
drive_decision(:green, :sunny, 100, 50)
drive_decision(:yellow, :rainy, 50, 25)
- Refactor the method using case/when and additional methods. First, we determine the method for drive_decision_when_green:
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
- Similarly, we define the method for drive_decision_when_yellow:
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
- We also define the method for drive_decision_when_red:
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
- Lastly, we define the drive_decision method depending on the three conditions defined before:
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:
Figure 3.20: Output for driving decisions
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.