Exercises - Decisions and debugging

Module 4: conditional logic, testing, and locating defects

Name:

Student number:

Choose one answer for each multiple-choice question. Answer the open questions with modular pseudocode or flowcharts, and give your test cases as a table of inputs and expected outputs.

M1) Freezer alarm

A cold store keeps frozen food at -18 °C. A sensor in each freezer sends a temperature reading in degrees Celsius every minute. When the sensor fails to measure, it sends None instead of a number. The function freezer_status must return:

  • "No reading" when the reading is None;
  • "Alarm" when the reading is above -15.0 °C;
  • "OK" otherwise.

Listing 1 shows the implementation that a programmer submitted.

Listing 1: A function that classifies one freezer reading (Question 1).
def freezer_status(reading: float | None) -> str:
    if not reading:
        return "No reading"
    if reading > -15.0:
        return "Alarm"
    return "OK"

A technician runs each of the following checks on its own. Which check fails?

  1. assert freezer_status(None) == "No reading"
  2. assert freezer_status(-18.0) == "OK"
  3. assert freezer_status(0.0) == "Alarm"
  4. assert freezer_status(-15.0) == "OK"

M2) Who can be president?

The U.S. Constitution lists three qualifications for the presidency:

  • The person must be at least 35 years old.
  • The person must be a natural-born citizen1 of the United States.
  • The person must have been a resident in the United States for at least 14 years.

In Listing 2, the main function receives a person’s age, citizenship status, and years of residency as parameters. It calls check_eligibility and prints a message based on the Boolean value returned by that function.

Listing 2: The is_citizen parameter is True when the person was a U.S. citizen from birth and False otherwise.
def main(age: int, is_citizen: bool, years_resident: int) -> None:
    if check_eligibility(age, is_citizen, years_resident):
        print("You are eligible.")
    else:
        print("You are not eligible.")

In Listing 3, we have several implementations of check_eligibility. Each function should return True only when all three requirements are met and False otherwise.

Which implementation of check_eligibility correctly determines whether a person is eligible to become President based on their age, citizenship status, and years of residency?

Listing 3: Alternative implementations of check_eligibility for determining whether a person is eligible to become President (Question 2).
(a)
def check_eligibility(
    age: int, is_citizen: bool, years_resident: int
) -> bool:
    if age >= 35 and years_resident >= 14:
        return is_citizen
    else:
        return False
(b)
def check_eligibility(
    age: int, is_citizen: bool, years_resident: int
) -> bool:
    if age >= 35 or is_citizen or years_resident >= 14:
        return True
    else:
        return False
(c)
def check_eligibility(
    age: int, is_citizen: bool, years_resident: int
) -> bool:
    if age < 35 or years_resident < 14:
        return False
    else:
        return True
(d)
def check_eligibility(
    age: int, is_citizen: bool, years_resident: int
) -> bool:
    is_old_enough = age >= 35
    has_enough_residency = years_resident >= 14
    return is_old_enough and is_citizen and has_enough_residency

M3) Crane wind limits

A construction site limits crane work by the wind speed measured at the top of the crane. Table 1 gives the action for each wind speed w in meters per second.

Table 1: The site’s wind rule for crane work (Question 3).
Wind speed w (m/s) Action
w < 10 Normal
10 ≤ w < 15 Caution
15 ≤ w < 20 Stop lifting
w ≥ 20 Secure crane

An engineer wrote the pseudocode in Listing 4 for the rule.

Listing 4: Pseudocode for the crane wind rule (Question 3).
Input: Wind speed w in m/s
Output: Crane action
Start
   If w >= 10
      Output Caution
   Else If w >= 15
      Output Stop lifting
   Else If w >= 20
      Output Secure crane
   Else
      Output Normal
   End If
End

The engineer traces the pseudocode for every wind speed in a test set and compares each output with Table 1. With which test set do all tests pass, even though the pseudocode is defective?

  1. {4, 9.9, 12}
  2. {5, 16}
  3. {22}
  4. {9.9, 10, 14.9, 15}

M4) Delivery fee

A web shop charges a delivery fee in euros. Members never pay the standard fee. Other customers pay no standard fee on orders of €50.00 or more, and €4.95 below that. An order with a bulky item, such as a bookcase, costs an extra €15.00 on top of whatever the standard fee is, including when that fee is 0.

Figure 1 shows the rule as a flowchart.

Figure 1: Flowchart of the delivery fee (Question 4).

Listing 5 shows 4 implementations of delivery_fee.

Which implementations in Listing 5 follow the flowchart in Figure 1 for every input?

Listing 5: Four implementations of delivery_fee (Question 4).
(a)
def delivery_fee(
    total: float, is_member: bool, is_bulky: bool
) -> float:
    if is_member or total >= 50:
        fee = 0.0
    else:
        fee = 4.95
    if is_bulky:
        fee = fee + 15.0
    return fee
(b)
def delivery_fee(
    total: float, is_member: bool, is_bulky: bool
) -> float:
    if is_member or total >= 50:
        fee = 0.0
    elif is_bulky:
        fee = 4.95 + 15.0
    else:
        fee = 4.95
    return fee
(c)
def delivery_fee(
    total: float, is_member: bool, is_bulky: bool
) -> float:
    fee = 0.0
    if not is_member or total < 50:
        fee = 4.95
    if is_bulky:
        fee = fee + 15.0
    return fee
(d)
def delivery_fee(
    total: float, is_member: bool, is_bulky: bool
) -> float:
    fee = 0.0
    if not is_member and total < 50:
        fee = 4.95
    if is_bulky:
        fee = fee + 15.0
    return fee

M5) Printing cost

At a university library, print jobs of up to 10 pages are free for everyone. Longer jobs cost €0.05 per page for students and €0.10 per page for staff. A programmer wrote the rule as one conditional expression, shown in Listing 6.

Listing 6: The printing cost of one job as a conditional expression (Question 5).
def print_cost(pages: int, is_student: bool) -> float:
    return 0.0 if pages <= 10 else 0.05 * pages if is_student else 0.10 * pages

Python groups nested conditional expressions from right to left: A if c1 else B if c2 else C means A if c1 else (B if c2 else C). It evaluates c1 first.

Which flowcharts in Figure 2 give the same cost as Listing 6 for every job?

(a) Candidate flowchart
(b) Candidate flowchart
(c) Candidate flowchart
(d) Candidate flowchart
Figure 2: Four flowcharts for the printing cost (Question 5).

M6) Gym entry gate

A university gym opens its entry gate with a member card. The gate first checks that the card is valid, then that the gym is open, and finally that this month’s fee has been paid. The gym is open from 6:00 until 23:00, so the hour hour of arrival must satisfy 6 ≤ hour < 23. Figure 3 shows the decision.

Figure 3: Flowchart of the gym entry gate (Question 6).

A tester wants a test set that makes every Yes arrow and every No arrow of the 3 decisions run at least once. Table 2 lists 4 candidate test sets. Each test case is written as (card_valid, has_paid, hour).

Table 2: Four candidate test sets for the gym gate (Question 6).
Set Test cases
1 (True, True, 10), (False, True, 10), (True, False, 10)
2 (True, True, 10), (False, False, 3), (True, True, 23), (True, False, 6)
3 (True, True, 6), (True, True, 22), (True, False, 12), (False, True, 12)
4 (False, True, 10), (False, False, 3), (True, True, 23), (True, False, 23)

Which test set in Table 2 runs every arrow of Figure 3 at least once?

  1. Set 1
  2. Set 2
  3. Set 3
  4. Set 4

M7) Call center review

A call center reviews every shift whose average call lasted more than 8 minutes. The average is the total minutes on the phone divided by the number of calls. A shift can have 0 calls, for example during a phone outage, and such a shift is not reviewed.

The function needs_review(calls, total_minutes) must return True when the shift needs a review and False otherwise. It must never stop with an error. Listing 7 shows 4 implementations.

Which implementations in Listing 7 meet the specification for every shift, including a shift with 0 calls?

Listing 7: Four implementations of needs_review (Question 7).
(a)
def needs_review(
    calls: int, total_minutes: float
) -> bool:
    return total_minutes / calls > 8 and calls > 0
(b)
def needs_review(
    calls: int, total_minutes: float
) -> bool:
    return calls > 0 and total_minutes / calls > 8
(c)
def needs_review(
    calls: int, total_minutes: float
) -> bool:
    return calls > 0 or total_minutes / calls > 8
(d)
def needs_review(
    calls: int, total_minutes: float
) -> bool:
    if calls == 0:
        return False
    return total_minutes / calls > 8

M8) Order status

A web shop shows each order’s status from 2 facts: whether it has been paid, and whether it has been shipped. Table 3 gives the required status for each combination. An order that was shipped without being paid points to a mistake in the warehouse, so staff must check it.

Table 3: Required status for every combination of the 2 facts (Question 8).
Paid Shipped Status
False False Awaiting payment
True False Packing
True True On its way
False True Check order

A programmer wrote the match statement in Listing 8. The pattern _ matches any value.

Listing 8: A match statement for the order status (Question 8).
def order_status(paid: bool, shipped: bool) -> str:
    match (paid, shipped):
        case (False, _):
            return "Awaiting payment"
        case (True, False):
            return "Packing"
        case (True, True):
            return "On its way"
        case (False, True):
            return "Check order"

Which statement about Listing 8 is true?

  1. order_status(False, True) returns "Check order", because that case matches the inputs exactly.
  2. The last case can never run, so assert order_status(False, True) == "Check order" fails.
  3. Python refuses to run the function, because 2 cases match the same inputs.
  4. order_status(True, True) returns "Awaiting payment", because _ matches any value.

M9) Surge multiplier

A ride-hailing app raises its prices when riders wait and few drivers are free. The price of a ride is multiplied by a surge multiplier that depends on the number of waiting ride requests and the number of available drivers in an area. Listing 9 gives the rule.

Listing 9: Pseudocode for the surge multiplier (Question 9).
Input: Requests, the number of waiting ride requests
       Drivers, the number of available drivers
Output: Multiplier
Start
   If Drivers = 0
      Output 3.0
   Else
      Set Ratio to Requests / Drivers
      If Ratio >= 2
         Output 2.0
      Else If Ratio > 1
         Output 1.5
      Else
         Output 1.0
      End If
   End If
End

A tester wrote the test cases in Table 4 before anyone implemented the rule. The expected multiplier of each case should follow from the pseudocode.

Table 4: Test cases for the surge multiplier (Question 9).
Test Requests Drivers Expected multiplier
T1 10 5 2.0
T2 0 0 3.0
T3 5 5 1.5
T4 6 5 1.5

Which test case in Table 4 has an expected multiplier that does not follow from Listing 9?

  1. T1
  2. T2
  3. T3
  4. T4

M10) Ticket price errors

A museum sells tickets at 3 prices. Children under 12 pay €5.00, visitors aged 65 or over pay €7.50, and everyone else pays €10.00. Three students each wrote a small program for the rule. Listing 10 shows the 3 programs with the output from running each one directly below its code. In Program 2, the user enters 70 at the prompt.

Listing 10: Three programs for the ticket price (Question 10).
(a) Program 1: ticket_price(8) is requested; Python rejects the code before it can run.
def ticket_price(age: int) -> float:
    if age < 12
        return 5.0
    elif age >= 65:
        return 7.5
    else:
        return 10.0


print(ticket_price(8))
  Cell In[2], line 2
    if age < 12
               ^
SyntaxError: expected ':'
(b) Program 2: the user enters 70; execution stops at the comparison inside ticket_price.
def ticket_price(age: int) -> float:
    if age < 12:
        return 5.0
    elif age >= 65:
        return 7.5
    else:
        return 10.0


age_typed = input("Age: ")
print(ticket_price(age_typed))
Age: 70
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[3], line 11
      7         return 10.0
      8 
      9 
     10 age_typed = input("Age: ")
---> 11 print(ticket_price(age_typed))

Cell In[3], line 2, in ticket_price(age)
      1 def ticket_price(age: int) -> float:
----> 2     if age < 12:
      3         return 5.0
      4     elif age >= 65:
      5         return 7.5

TypeError: '<' not supported between instances of 'str' and 'int'
(c) Program 3: ticket_price(12) runs to completion and prints 5.0.
def ticket_price(age: int) -> float:
    if age <= 12:
        return 5.0
    elif age >= 65:
        return 7.5
    else:
        return 10.0


print(ticket_price(12))
5.0
TipHint: error types
  • A syntax error means Python cannot read the code as valid Python. It is reported before the program starts.
  • A runtime error stops a program while it is running. The last line of its traceback names the error Python found.
  • A logic error means the program runs but gives a result that breaks the stated rule. Python may report no error, so check the output against the rule.

Which set of changes would make all three programs follow the museum’s rule?

  1. Add the missing colon in Program 1; change age: int to age: str in Program 2; change age <= 12 to age < 12 in Program 3.
  2. Add the missing colon in Program 1; pass int(age_typed) to ticket_price in Program 2; change age <= 12 to age < 12 in Program 3.
  3. Add the missing colon in Program 1; pass int(age_typed) to ticket_price in Program 2; change age >= 65 to age > 65 in Program 3.
  4. Add the missing colon in Program 1; pass int(age_typed) to ticket_price in Program 2; leave Program 3 unchanged because it runs without an error.

M11) Stepping through a call in VS Code

A delivery charge looks wrong, so you pause the program in the VS Code debugger. In Listing 11, Breakpoint A is on the call to delivery_fee, and Breakpoint B is on print(total). Both breakpoints are enabled. Execution is paused at Breakpoint A, before that line runs, so fee does not have a value yet.

Listing 11: Program paused before a delivery charge is calculated.
def delivery_fee(distance_km: int) -> int:
    if distance_km <= 5:
        return 2
    return 5


def order_total(subtotal: int, distance_km: int) -> int:
    fee = delivery_fee(distance_km)  # Breakpoint A
    return subtotal + fee


def main() -> None:
    total = order_total(20, 6)
    print(total)  # Breakpoint B


main()

You want to inspect distance_km inside delivery_fee, return to order_total, finish the assignment to fee, inspect it on the following line, and then run to Breakpoint B. Which sequence of VS Code debugger controls does that?

  1. Step Into, Step Out, Step Over, Continue
  2. Step Into, Step Out, Continue
  3. Step Over, Step Out, Step Over, Continue
  4. Step Into, Continue, Step Over, Continue

M12) Line execution order in nested calls

The main function in Listing 12 passes the result of double(3) to add_one. The comments A to E mark lines in the function bodies. Consider only those marked lines when tracing the program.

Listing 12: Nested calls with five marked lines to trace.
def add_one(number: int) -> int:
    result = number + 1  # A
    return result         # B


def double(number: int) -> int:
    result = number * 2  # C
    return result         # D


def main() -> None:
    value = add_one(double(3))
    print(value)  # E


main()

In what order does Python execute the marked lines, and what does the program print?

  1. C, D, A, B, E; prints 7
  2. A, B, C, D, E; prints 8
  3. C, A, D, B, E; prints 7
  4. E, C, D, A, B; prints 7

M13) Boxes per pallet

A warehouse scanner sends the number of boxes in a delivery as text, such as "120". The function boxes_per_pallet in Listing 13 converts the text to a whole number and divides the boxes evenly over the pallets. For a valid delivery, the box count is nonnegative, the pallet count is positive, and the box count is divisible by the pallet count. The calls below also test invalid inputs. When the text is not a whole number, the function returns -1 so that a clerk can count the delivery by hand.

Listing 13: Dividing scanned boxes over pallets, with 4 calls (Question 13).
def boxes_per_pallet(total_text: str, pallets: int) -> int:
    try:
        total = int(total_text)
        return total // pallets
    except ValueError:
        return -1


print(boxes_per_pallet("120", 4))
print(boxes_per_pallet("96", 4))
print(boxes_per_pallet("12.5", 4))
print(boxes_per_pallet("120", 0))
TipError-type hints

You do not need to memorize the error names. int(text) raises a ValueError when the text cannot be converted to an integer. Division or floor division by 0 raises a ZeroDivisionError.

What happens when Listing 13 is run?

  1. It prints 30, 24, -1, and -1.
  2. It prints 30, 24, and 3, then stops with a ZeroDivisionError.
  3. It prints 30, 24, 3, and -1.
  4. It prints 30, 24, and -1, then stops with a ZeroDivisionError.

M14) Shipping label traceback

A parcel service charges shipping per kilogram. Parcels within the Netherlands ("NL") cost 120 cents per kilogram, parcels to other EU countries ("EU") cost 250 cents, and all other destinations cost 400 cents. The program in Listing 14, saved as shipping.py, prints the text for a shipping label.

Listing 14: A program that prints a shipping label for a 2 kg parcel to the United Kingdom (Question 14).
def rate_per_kg(zone: str) -> int:
    if zone == "NL":
        return 120
    elif zone == "EU":
        return 250
    elif zone == "World":
        return 400


def shipping_cost(weight: int, zone: str) -> int:
    return weight * rate_per_kg(zone)


def label_text(weight: int, zone: str) -> str:
    cost = shipping_cost(weight, zone)
    return f"{zone}: {cost} cents"


print(label_text(2, "UK"))

Running the program produces the traceback in Listing 15.

Listing 15: The traceback of Listing 14 (Question 14).
Traceback (most recent call last):
  File "shipping.py", line 19, in <module>
    print(label_text(2, "UK"))
          ^^^^^^^^^^^^^^^^^^^
  File "shipping.py", line 15, in label_text
    cost = shipping_cost(weight, zone)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "shipping.py", line 11, in shipping_cost
    return weight * rate_per_kg(zone)
           ~~~~~~~^~~~~~~~~~~~~~~~~~~
TypeError: unsupported operand type(s) for *: 'int' and 'NoneType'

Where is the defect, and why?

  1. In rate_per_kg. Its last branch tests zone == "World", so any other destination reaches the end of the function, which returns None.
  2. On line 11 of shipping_cost. Python cannot multiply a number by a function call, so the rate must be stored in a variable first.
  3. On line 19. "UK" is not a valid zone, so the call must pass "World" or "EU" instead.
  4. In shipping_cost. It must catch the TypeError with try-except and return 0.

Q1) Dispatch an elevator

An office building has two elevators, A and B. Someone presses Up or Down on a floor. The controller chooses which elevator answers the call.

Each elevator has a current floor and a target floor. If these are equal, the elevator is idle. Otherwise, it moves toward its target. Assume that the call and all floors are valid and both elevators are working.

Travel rule. An idle elevator goes straight to the call. A moving elevator can stop for a call on its current route when the call floor is strictly ahead of it, no farther than its target, and the pressed direction matches its movement. It then travels straight to the call. In every other case, it first reaches its target and then travels to the call. Count one floor of travel for each floor crossed. For example, abs(2 - 5) = 3.

Choice rule. The elevator with the shorter travel distance answers. If their distances are equal, A answers.

For example, A is idle on floor 4. B is on floor 9, traveling down to floor 1. Someone on floor 6 presses Down. A travels 2 floors; B stops on its current route after 3 floors. The controller chooses A.

Your task. In pseudocode or flowcharts, design these two procedures:

  1. Travel Distance: given one elevator’s floor and target, the call floor, and the pressed direction, return its travel distance to the call.
  2. Choose Elevator: given both elevators and the call, call Travel Distance for each elevator and return A or B.

Name each procedure and state its inputs and output. Then complete Table 5. Show both distances and the chosen elevator. An elevator is written as current floor → target floor.

Table 5: Calls to trace through the dispatch design.
Case A B Call Distance A Distance B Choice
On the way 2 → 2 7 → 0 floor 5, Down
Moving away 2 → 2 7 → 12 floor 5, Down
Equal distances 3 → 3 9 → 9 floor 6, Up

For a moving elevator, compare the call floor with both its current floor and its target. If it cannot stop on the way, add the distance to its target to the distance from that target to the call.

Footnotes

  1. The citizenship requirement concerns when a person became a U.S. citizen. For example, Elon Musk — a South African-born entrepreneur — and Arnold Schwarzenegger — an Austrian-born actor and politician — are both naturalized citizens, so they are not eligible to become President.↩︎