9. Putting Things Together

  • What have we seen so far?

    • Values

    • Types

    • Variables

    • Print

    • Input

    • Functions

    • Booleans

    • Logic

    • If/Else

  • Each of the individual topics may feel simple on their own

  • The difficult part tends to be when putting these topics together to solve complex problems

9.1. Writing Bigger Programs

  • There is no single correct way to write programs, but there are some strategies

  • For now I recommend a bottom up, incremental approach

    1. Start with an empty function and have it return some arbitrary constant value (e.g., 0)

    2. Run the function and verify that it does what you expect

    3. Add one or two lines of code

    4. Run the function and verify that it does what you expect

    5. Repeat

  • This incremental strategy is great because a lot of the problem solving you will be doing will be incremental

  • Additionally, it helps you make sure everything is working along the way

    • If everything was working and you added two lines of code and suddenly it stops working, perhaps the issue is with the two new lines you wrote

  • We would still want to write tests for our completed functions, but you may find it difficult to debug a whole complete function when compared to one or two lines of code

9.2. Car Rental

../../_images/car_rental_sign.png
  • Here we solve a bigger problem than we are used to, but we will follow the incremental approach

  • In fact, we will take it to the extreme

  • Instead of just writing a few lines of code, we will make each part a function we can test easily

    • Regardless of how “simple” the part seems

Problem

A car rental place needs our help. They want a program to calculate how much a customer is to be charged based on their rental agreement, age, how far they drove, and how long they had the car.

  • We will get and record the customer’s:

    • Age

    • Rental agreement classification code (B or D)

    • Number of days rented

    • Starting odometer reading

    • Ending odometer reading

  • If the classification code is B

    • Base charge of $20.00/day

    • Plus $0.30 for every km driven

  • If the classification is D

    • Base charge of $50.00/day

    • Plus $0.30 for every km driven above the 100km/day average allowance

  • All renters under the age of 25 are charged an additional $10.00/day

9.2.1. An Incremental Solution

Step 1

  • Read the problem

Step 2

  • Understand the problem

    • This cannot be overstated — this is a big part of solving any problem

  • Half of the description is Input/Output (I/O)

  • We know how to do this, so we will start here

  • The other half of the description is the calculation

Step 3

  • Chip away at the problem

Note

Understand that the example below is only one possible implementation of a solution to this problem. There is literally an infinite number of ways one could go about solving this problem.

9.2.1.1. Input

1age = int(input('Age: '))
2classification = input('Classification Code: ')
3number_of_days = int(input('Number of Days Rented: '))
4starting_kms = float(input('Odometer reading at start: '))
5ending_kms = float(input('Odometer reading at end: '))
6
7total_charge = # Some function to do the total charge calculation
8
9print('The total charge is: ' + str(total_charge))

Note

In the above example, we would want to verify it is doing what we expect. Since user input is a little difficult to test with assert, we can simply print out the data and confirm that it is doing what we expect. For example, the following code could be added to the above example.

1print(age, type(age))
2print(classification, type(classification))
3print(number_of_days, type(number_of_days))
4print(starting_kms, type(starting_kms))
5print(ending_kms, type(ending_kms))
  • The above example of reading user input seems to be sufficient for what we need; however, we are far from solving the problem

  • Line 7 is currently non-functional; it is simply a placeholder for the actual total_charge calculation

    • If you were to run the example code, it would not work since total_charge is currently not being assigned to anything

  • In other words, we need to actually write some function to do the actual calculation for us

    • The calculation may seem complex, but let’s take the same approach as above

    • We will write the code we can and leave comments for the parts we still need to tackle

9.2.1.2. Calculating The Total Charge

 1def calculate_total_charge(some_number_of_parameters):
 2
 3    # Set up a variable for our total charge
 4    total_charge = 0
 5
 6    # Calculate the number of kilometres traveled.
 7    total_kms_traveled = ???
 8
 9    # Calculate the charge based on rental code
10    if rental_code == 'B':
11        # Base charge of $20.00/days + $0.30 for every km driven
12    else:
13        # Base charge of $50.00/days + $0.30 for every km driven above the 100km/day average allowance
14        average_kms = ???
15        num_kms_above_allowance = ???
16
17    # if they're under 25, add additional charge
18    if something :
19        ???
20
21    # Return the final total charge
22    return some_total_charge
  • Although this is an incomplete function, it did help us outline what we need to know in order to solve the problem

    • We need to know the total kilometers travelled

    • We need to know the average kms/day

    • We need to know the number of kms driven above the 100km/day average allowance

    • We need to do the actual rental agreement classification calculation

    • We need to add the extra charge for people under 25

9.2.1.3. Total Kilometers

  • A function to calculate the total number of kms

    • What do we know?

      • Odometer readings

 1def total_kms(odometer_start: float, odometer_finish: float) -> float:
 2    """
 3    This function calculates the total number of kilometers driven based
 4    on starting and ending odometer readings.
 5
 6    :param odometer_start: The number of kms the car had before renting
 7    :param odometer_finish: The number of kms the car had after returning
 8    :return: The total kms driven
 9    """
10
11    return odometer_finish - odometer_start
12
13assert 0 == total_kms(0, 0)
14assert 100 == total_kms(0, 100)
15assert -100 == total_kms(100, 0)
16assert 100.5 == total_kms(100.5, 201)
  • Even for a simple sub-problem like this, a named function has real benefits

    • total_kms is more readable than odometer_finish - odometer_start

    • It is easy to test in isolation

    • It keeps the incremental approach consistent

9.2.1.4. Average Kilometers Per Day

  • A function to calculate the daily average number of kms

    • What do we know?

      • We have a function to calculate the total kms

      • We also know the number of days the car was rented

 1def average_kms_per_day(num_days: float, num_kms: float) -> float:
 2    """
 3    Calculate the average number of kilometers driven per day
 4    over the rental period
 5
 6    :param num_days: The total number of days the car was rented
 7    :param num_kms: The total number of kilometers driven during the rental period
 8    :return: The average number of kilometers driven per day
 9    """
10
11    return num_kms / num_days
12
13
14assert 0 == average_kms_per_day(1, 0)
15assert 1 == average_kms_per_day(1, 1)
16assert -1 == average_kms_per_day(-1, 1)
17assert 0.5 == average_kms_per_day(3, 1.5)

9.2.1.5. Kilometers Above Allowable Average

  • Number of kms over the daily average allowance

  • What do we know?

    • Average kms/day given the function average_kms_per_day we wrote

 1def num_kms_above_average(avg_num_kms: float) -> float:
 2    """
 3    Calculates the number of kms the renter went over of their daily allowance.
 4    We will use the customer's average daily kms.
 5
 6    :param avg_num_kms: average number of kms driven per day
 7    :return: The number of kms over 100 they went (return 0 if it's less than 100)
 8    """
 9
10    # If the average kms traveled is above 100,
11    # return how much above, otherwise zero
12    if avg_num_kms > 100:
13        return avg_num_kms - 100
14    else:
15        return 0
16
17
18assert 0 == num_kms_above_average(100)
19assert 1 == num_kms_above_average(101)
20assert 0 == num_kms_above_average(99)
21assert 100 == num_kms_above_average(200)

Note

Notice that 100 is hardcoded in the function. There is a good argument for making it a constant instead, something like AVERAGE_DAILY_LIMIT = 100, which would make it clearer what the number represents and easier to change in the future.

9.2.1.6. Revisit Calculating the Total Charge

  • With the functions we wrote, solving the big calculate_total_charge becomes simpler

 1def calculate_total_charge(num_days: float, age: float, rental_code: str, odometer_start: float, odometer_finish: float) -> float:
 2    """
 3    Calculate how much the renter needs to be charged based on the rental code classification,
 4    the number of kms travelled and the age of the driver.
 5
 6    :param num_days: Number of days the car was rented.
 7    :param age: Age of the driver.
 8    :param rental_code: The rental code classification code (B or D).
 9    :param odometer_start: Odometer when the renter took the car.
10    :param odometer_finish: Odometer when the renter returned the car.
11    :return: The amount to charge the renter.
12    """
13    # Set up a variable for our total charge
14    total_charge = 0
15
16    # Calculate the number of kilometres traveled.
17    total_kms_traveled = total_kms(odometer_start, odometer_finish)
18
19    if rental_code == "B":
20        total_charge = 20.00 * num_days + 0.30 * total_kms_traveled
21    else:
22        average_kms = average_kms_per_day(num_days, total_kms_traveled)
23        num_kms_above_allowance = num_kms_above_average(average_kms)
24        total_charge = 50.00 * num_days + 0.30 * num_kms_above_allowance
25
26    # if they're under 25, add additional charge
27    if age < 25:
28        total_charge = total_charge + 10 * num_days
29
30    # Return the final total charge
31    return total_charge
32
33
34assert 20 == calculate_total_charge(1, 30, "B", 0, 0)
35assert 50 == calculate_total_charge(1, 30, "D", 0, 0)
36assert 30 == calculate_total_charge(1, 20, "B", 0, 0)
37assert 60 == calculate_total_charge(1, 20, "D", 0, 0)
38assert 50 == calculate_total_charge(1, 30, "B", 0, 100)
39assert 50 == calculate_total_charge(1, 30, "D", 0, 100)
40assert 60 == calculate_total_charge(1, 20, "B", 0, 100)
41assert 60 == calculate_total_charge(1, 20, "D", 0, 100)
42assert 190 == calculate_total_charge(2, 30, "B", 0, 500)
43assert 145 == calculate_total_charge(2, 30, "D", 0, 500)
44assert 210 == calculate_total_charge(2, 20, "B", 0, 500)
45assert 165 == calculate_total_charge(2, 20, "D", 0, 500)
  • Take the time to go over all the parts of this function

  • Slow down on anything that doesn’t feel clear

    • The new functions were used to simplify much of the calculation

    • The if for the rental classification simply evaluates the corresponding cost calculation

    • The if for the age adds an additional $10/day

Activity

Think about how you would write this differently

  • Would you use all the same functions?

  • Would you change how the functions worked?

  • Would you move where you called the functions?

  • Would you add additional functions?

  • Would you use constants? Where?

9.3. For Next Topic