Skip to content

Combining Selection

Learning Objectives

At the end of this sub-unit, students should

  • know how to combine selection statement.
  • know how to flatten selection statement if needed.

Multiple Selections

Recap that the syntax for if-statement always starts with the keyword if which can then be followed by elif and else. If we then have another if keyword appearing, this will constitutes a separate if-statement. For instance, in the example below, we have two if-statements.

1
2
3
4
5
6
7
8
if cond1:
  block1
elif cond2:
  block2
if cond3:
  block3
else:
  block4
1
2
3
4
5
6
7
8
if cond1:    # --+
  block1     #   | if-statement #1
elif cond2:  #   |
  block2     # --+
if cond3:    # --+
  block3     #   | if-statement #2
else:        #   |
  block4     # --+

One way to determine which if-statement a particular elif or else belongs to is by going up the code and find the nearest if keyword with the same indentation level. This way, we can see that the else keyword is attached to the second if keyword and the elif keyword is attached to the first if keyword.

This will affect the execution of the code. As we have discussed in the previous unit, we have the following property.

At most one of the blocks will be executed.

Since there are two if-statement in the example, there will be at most two blocks executed instead of one. Additionally, note that Line 5 will always be executed because it is the beginning of the second if-statement. An exception to this is of course when the program terminates due to an error before reaching Line 5.

What is the implication of this? We can actually rewrite the letter grade problem differently. First, note that the disjoint version can be easily rewritten by replacing all elif into if because a single value cannot satisfy more than one condition. Second, to highlight the differences more clearly, we will rewrite the overlapping version as follows.

# Assume mark is initialized
if mark >= 90:
  grade = "A"
elif mark >= 80:
  grade = "B"
elif mark >= 70:
  grade = "C"
elif mark >= 60:
  grade = "D"
else:
  grade = "F"
print(grade)

In the next solution, we will use multiple if-statement instead of a single if-statement with multiple elif keywords. We will refer to this version as multi-if version.

Incorrect Letter Grade

Note that simply changing all the elif keywords into if keywords in the overlapping version will give us a wrong solution. Try running the following code with mark = 87 to see the problem.

# Assume mark is initialized
if mark >= 90:
  print("A")
if mark >= 80:
  print("B")
if mark >= 70:
  print("C")
if mark >= 60:
  print("D")
else:
  print("F")
1
2
3
B
C
D

Letter Grade

# Assume mark is initialized
grade = "F"     # remain F if mark < 60
if mark >= 60:
  grade = "D"   # remain D if mark >= 60 and mark < 70
if mark >= 70:
  grade = "C"   # remain C if mark >= 70 and mark < 80
if mark >= 80:
  grade = "B"   # remain B if mark >= 80 and mark < 90
if mark >= 90:
  grade = "A"   # will be A if mark >= 90
print(grade)

To see why this code is correct, we can look at one of the letter grade. Say we look at grade = "C". We need to ask ourselves, what conditions will make the value of the variable grade remains C at Line 10?

  • It must be assigned "C" at Line 5.
    • Hence, it must satisfy the condition mark >= 70.
  • It must NOT be assigned "B" at Line 7.
    • Hence, it must not satisfy the condition mark >= 80.
  • It must NOT be assigned "A" at Line 9.
    • Hence, it must not satisfy the condition mark >= 90.

The solution to that implies mark is in the range of [70, 80). This corresponds exactly to the requirement.

Let us compare this solution with the previous solution. One interesting difference is the order in which the letter grade appears in the code. Using the overlapping version, we can see that "A" appears first whereas in the multi-if version, "F" appears first.

Another interesting property is that in the overlapping version, the variable grade will only ever be assigned a value once. You can try to trace the execution to see this fact. It can also be deduced from the use of if, elif, and else. Due to the following property of if-statement.

At most one of the blocks will be executed.

We can infer that only one of the assignment block will be executed. Now, in the multi-if version, the variable grade is potentially being assigned 5 times. Each of the assignment block may be executed. For instance, when mark is 99. In fact, the flowchart highlights this behavior properly as you can see below. The dashed path highlighted in blue is the path taken when mark is 77. Notice how there are 3 assignments to grade.

Grades

Alternatively, we can use the arrow on the code to show which lines are executed again. Compare the number of lines executed with the overlapping version.

If04

If05

So that is how multiple if-statements can be combined to perform more checks. This is actually not something new. If we can actually write the entire if-statement inside the rectangle as shown above, then it is really just a sequence of if-statements.

This is an example of the idea of composition in programming. One concept can actually be combined (i.e., be composed) with other concepts. The interactions between the concepts can be confusing. As such, we should try to use the concepts in the intended way and not mix concepts without knowing all the interactions. We are back to our motto of writing readable code.

Creation of Multiple Selections

One way to think about multiple selections is to first start with sequence and then expand or replace the block with the flowchart for if-statement.

Comp01A

Comp01B

Comp01C

Inner Selections

There is another way of composing the selection. We have discussed how selection can be sequenced. But note that if-statement is also a statement and a group of 1 or more statement is called a block. Since we can put a block inside an if-statement, we can also put another if-statement inside another if-statement.

How useful is this composition? Consider the case of determining if a year is a leap year or not. According to Wikipedia, we have the following definition.

Leap Year

Every year that is exactly divisible by four is a leap year, except for years that are exactly divisible by 100, but these centurial years are leap years if they are exactly divisible by 400. For example, the years 1700, 1800, and 1900 are not leap years, but the years 1600 and 2000 are.

Let us look at the definition more closely. We have three divisibility checks that we need to do.

  • Divisible by 4.
  • Divisible by 100.
  • Divisible by 400.

We know how to check for divisibility. The variable year is divisible by a value val if the following expression year % val == 0 evaluates to True.

More interestingly, we need to know how is the check actually done. The keyword except here implies that after we check if a year is divisible by 4, we still need to check if the year is also divisible by 100. Even worse, the phrase "... but these centurial years are leap years if they are exactly divisible by 400." implies that we then need to check if the year is divisible by 400 again. Rearranging this, we get the following hierarchy shown below. The hierarchy closely match the code!

  • Is year divisible by 4?
    • Is year divisible by 100?
      • Is year divisible by 400?
        • Leap year
      • Otherwise
        • Not leap year
    • Otherwise
      • Leap year
  • Otherwise
    • Not leap year
# Assume year is initialized
if year % 4 == 0:
  if year % 100 == 0:
    if year % 400 == 0:
      print("Leap Year")
    else:
      print("Not Leap Year")
  else:
    print("Leap Year")
else:
  print("Not Leap Year")
# Assume year is initialized
if year % 4 == 0:
  if year % 100 == 0:
    if year % 400 == 0:
      print("Leap Year")      # (div by 4) and (div by 100) and (div by 400)
    else:
      print("Not Leap Year")  # (div by 4) and (div by 100) and (NOT div by 400)
  else:
    print("Leap Year")        # (div by 4) and (NOT div by 100)
else:
  print("Not Leap Year")      # (NOT div by 4)

The analysis tab shows the conditions that are satisfied at each print. You should convince yourself that the code is correct so it is a start. However, readability may be a little compromised with the deep nesting. At line 5 and 7, we indent the line 3 times. The deeper the nesting, the harder it is to read given the limited width of the monitor and/or paper we have to work with.

Flattening

Often, we want to avoid such a deep nesting. The analysis of the conditions can actually help us. By definition, the conditions are all disjoint. This is achieved by negating the condition on the else branch. Negating the condition is indicated by the comment # NOT. As a code, this can be done by the not operator. More precisely, the conditions at each nesting is indicated below.

Nesting Condition Analysis

if cond1:
  # (cond1)
  if cond2:
    # (cond1) and (cond2)
    blockA
  elif cond3:
    # (cond1) and (not cond2) and (cond3)
    blockB
  else:
    # (cond1) and (not cond2) and (not cond3)
    blockC
elif cond4:
  # (not cond1) and (cond4)
  blockD
else:
  # (not cond1) and (not cond4)
  blockE

This analysis match exactly our analysis for the leap year problem above. So this at least give us confidence that we have not made major mistakes. But more interestingly, how do we convert this to a non-nested if-statement (a.k.a. flattened). The idea is to look at each blocks independently and wrap each inside an if-statement. We may also use elif keyword for the next block, but not exactly necessary because all the conditions are disjoint if we do this conversion correctly.

After we identify the blocks and wrap them inside an if-statement, we put the analyzed condition including the and operator. This gives us the following code. In the code below, we use elif as a good practice so that we do not evaluate the next condition if the previous condition is already satisfied. Note that we will still convert all the else keywords because there is a possibility some cases are not covered. However, based on the analysis, we know these conditions must be satisfied.

Flattened If-Statement

if (cond1) and (cond2):
  blockA
elif (cond1) and (not cond2) and (cond3):
  blockB
elif (cond1) and (not cond2) and (not cond3):
  blockC
elif (not cond1) and (cond4):
  blockD
elif (not cond1) and (not cond4):
  blockE

Flattened Leap Year

So let us apply this technique to the leap year problem. We will do this in the systematic way as above. Try to do it on your own first before looking at the code below.

Flattened Leap Year

In this case, we can actually use else for the last one because we can analyze that all cases are covered.

1
2
3
4
5
6
7
8
9
# Assume year is initialized
if (year % 4 == 0) and (year % 100 == 0) and (year % 400 == 0):
  print("Leap Year")
elif (year % 4 == 0) and (year % 100 == 0) and (not (year % 400 == 0)):
  print("Not Leap Year")
elif (year % 4 == 0) and (not (year % 100 == 0)):
  print("Leap Year")
else:
  print("Not Leap Year")
1
2
3
4
5
6
7
8
9
# Assume year is initialized
if (year % 4 == 0) and (year % 100 == 0) and (year % 400 == 0):
  print("Leap Year")
elif (year % 4 == 0) and (year % 100 == 0) and (year % 400 != 0):
  print("Not Leap Year")
elif (year % 4 == 0) and (year % 100 != 0):
  print("Leap Year")
else:
  print("Not Leap Year")

There is yet another way to flattened this. To arrive at this way of flattening, we need to look at the 4 prints very closely. We can identify that there are duplicates. In fact, we only have 2 different prints. However, these 2 prints are separated by two different conditions.

With some thinking --and by saying the condition in English-- we can say that a particular year is a leap year if "condition at Line 1 is true" OR"condition at Line 5 is true". That will guide our further attempt at flattening. The OR in the sentence above can be performed using the or operator which will give us the following flattened code.

Flattened Leap Year

1
2
3
4
if ((year % 4 == 0) and (year % 100 == 0) and (year % 400 == 0)) or ((year % 4 == 0) and (year % 100 != 0)):
  print("Leap Year")
else:
  print("Not Leap Year")

That is short but not exactly readable. Here is another tips: do not forget to apply previous lessons when learning new lessons. We can make code more readable by assigning expressions and using it later. Since we have duplicated expressions, we can compute once and use multiple times.

Flattened Leap Year

# Assume year is initialized
div4 = (year % 4 == 0)  # parentheses is not needed but good for readability
div100 = (year % 100 == 0)
div400 = (year % 400 == 0)

# Computation
if (div4 and div100 and div400) or (div4 and (not div100)):
  print("Leap Year")
else:
  print("Not Leap Year")

Numerical Properties

We can often get a better code by looking at the properties of numbers. In this case, we need to look at some properties of divisibility. We can see that if a year is divisible by 400, it must be divisible by 100 because 400 itself is divisible by 100. This can also be inferred by the absence of the condition div400 and (not div100) in our current flattened version. Furthermore, by the same reasoning, a year that is divisible by 100 must be divisible by 4.

How can this help us? Note that (div4 and div100 and div400) can be simplified into div400. The condition in the computation is now simpler due to this simplification. That makes the code much more readable although it may require more thinking to see that it is indeed equivalent to the previous version.

Flattened Leap Year

# Assume year is initialized
div4 = (year % 4 == 0)  # parentheses is not needed but good for readability
div100 = (year % 100 == 0)
div400 = (year % 400 == 0)

# Computation
if (div400) or (div4 and (not div100)):
  print("Leap Year")
else:
  print("Not Leap Year")

Summary

Summary of Composition

# if-statement 1
if cond1:
  then_block1
else:
  else_block1
# if-statement 2
if cond2:
  then_block2
else:
  else_block2
# outer if
if cond1:
  block1
  # inner if
  if cond2:
    block2
  else:
    block3
else:
  block4

Summary of Flattening

The first flattening is using conjunction (i.e., and) operator. There is no condition on the blocks. They can be all different.

1
2
3
4
5
6
7
if cond1:
  if cond2:
    blockA  # (cond1) and (cond2)
  else:
    blockB  # (cond1) and (not cond2)
else:
  blockC    # (not cond1)
1
2
3
4
5
6
if (cond1) and (cond2):
  blockA  # (cond1) and (cond2)
elif (cond1) and (not cond2):
  blockB  # (cond1) and (not cond2)
elif (not cond1):
  blockC  # (not cond1)

The second flattening is using disjunction (i.e., or) operator. The condition for this to work is that some blocks are identical.

1
2
3
4
5
6
7
8
if cond1:
  blockA  # same as below
elif cond2:
  blockB
elif cond3:
  blockA  # same as above
else:
  blockC
1
2
3
4
5
6
if (cond1) or (cond3):
  blockA  # combined
elif cond2:
  blockB
else:
  blockC