Background:

Classes allow us to define new types for Python. We can first think of a class as defining a new container instead of a list, tuple, set, or dictionary, we can have our own collection of values, each with a chosen name rather than an index/key. We can then read out a value or update a value, much like reading or replacing the values in a list or dictionary. But we can also put methods in a class definition, giving us a way to specify the exact ways we should interact with values of this new type. Once we have created a class definition, we can create as many objects of the new type as we want and use them in our programs. We can create entirely new types during the design phase of writing a program. This enables us to think in terms of types (and instances of types) that better model what we see in the real world, instead of endlessly relying on lists or dictionaries (and having to remember exactly how we intended to use those things as proxies for the values we actually had in mind).

Exceptions allow us to recover from unusual events some unavoidable (user types in bad file name or bad input in general), some unexpected (such as IndexErrors when stepping through a list). Without exceptions, we tend to program in an "ask permission, then attempt" style of calculation. But using exceptions, we can instead program in a "try it first, ask for forgiveness" approach by writing code that specifies how to recover from exceptions that we allowed to occur.

What's Allowed?

As long as you don't import anything, you can use anything built-in or that you create. Now that we're really writing our own datatypes, the built-ins aren't really designed to solve our quite-specific problems! Just as we used to say "add your own helper functions and call them", you can also add your own helper methods and use them.

Task

We will implement some basic classes and methods to manage telephone plans. A PhonePlan can have multiple lines and include a record of multiple calls. By calling the methods, someone could manage the lines of a plan, calculate, and pay the bills.

Notes / Assumptions / Requirements

  • Don't import anything, and you can use all built-ins/methods that are otherwise available.
  • adding definitions: You may add additional methods and functions in support of your solution.
  • displaying things: __str__ and __repr__ are used by many other places in Python to get a string representation of the object. Specifically, str() calls __str__ , and repr() calls __repr__ , on any objects they receive as arguments. __str__ often generates a human-centric representation, appropriate for a human reading what is present. __repr__ often generates a Python-centric representation. The goal of __repr__ is actually to have a string that could be evaluated to re-generate an identical object. In general, we would quite prefer it to look like a valid constructor call, if possible. Ultimately, we can have the same representation in __str__ and __repr__ if we'd like; in fact, when we didn't explicitly describe two different string representations for __str__ and __repr__ to return, we can define one in terms of the other, like this:
def __repr__(self):
return str(self)
  • Just remember the original intent of __str__ vs __repr__ . It's good practice to define __init__ , __str__ , and __repr__ immediately before writing any extra methods in a Python class at a minimum, and perhaps also __eq__ as well.
  • what to return? Many methods don't specify a return value, and thus they return None after modifying the object.
  • handling exceptions: notice where we create exception types, and where we catch those exceptions. Be sure you don't catch the exception too early, or when not requested! When testing smaller parts of your code, it may be possible and even expected/required that specific inputs to a function/method will cause an exception to propagate (crash), rather than returning normally with a return value. We could extend this project to a full program with user interactions; this top-level layer of code would be a great place to catch more exceptions and ask for decisions and responses whenever we catch certain exception types. Since we're not writing the larger program that was described, there's no menu and user interaction present to do this particular style of interaction.
  • testing individual methods: you can also narrow down the focus of our tester by feeding it the name of a class (which only runs the init/str/repr/eq kinds of tests), or the method name of things that are needed in a particular class; this is the PhonePlan class. Note that one class may rely on other classes to work properly. We had to manually create this listing, and it's how the test cases were named. Here is the full list of test batches you can select:
    • classes (only basic init/str/repr/eq tests are performed for PhonePlan ): Line, Call, PlanType, PhonePlan, CallError, PhonePlanError
    • methods: add_line, remove_line, add_call, mins_by_line, calls_by_line, activateAll, deactivateAll, make_call, add_calls, remove_call, calculate_bill, pay_bill
    • for example: demo$ python3 tester6p.py yourcode.py Line PhonePlan add_line calls_by_line

Required Classes

Working through the classes in the given order is the simplest path to completion. Be sure to complete the init/str/repr/eq definitions before moving on to other classes that use them.

class Line:

  • def __init__( self, name, area_code, number, is_active=True): constructor of a telephone line: create/initialize instance variables for name , area_code , number and is_active . Assume area_code is a three-digit integer with no leading 0/1; assume number is a seven-digit integer with no leading 0/1.
  • def __str__(self): create/return a string as in this example: "703-9931530(GMU CS)"
  • def __repr__(self): create/return a string as in this example: "Line('GMU CS',703,9931530)"
  • def __eq__(self, other): determine if this object ( self ) is equivalent to other . Two lines are considered equal if they have the same area code and same number. Return True if they are equal; return False otherwise. Examples in the test cases.
    • By providing this method, == can be used to compare two lines based on area_code/number only.
  • def activate(self): set is_active of this object to be True .
  • def deactivate(self): set is_active of this object to be False .

class Call:

  • def __init__(self, caller, callee, length): create/initialize instance variables for all three non- self parameters. Assume caller and callee are Lines and that length is an integer. Check and raise CallError for the following cases:
    • If either caller or callee is inactive, raise CallError with error message as in this example: "line703-9931530(GMU CS)notactive" . If both caller and callee are inactive, only raise the error for caller .
    • If caller and callee are equivalent (with the same area_code and number), raise CallError with error message as in this example: "line703-9931530(GMU CS)cannotcallitself" . o If length is negative, raise CallError with error message as in this example: "negativecalllength:-5" .
  • def __str__(self): create/return a string as in this example: "Call(Line('GMU CS',703,9931530),Line('GMU',703,9931000),20)" Hint: when obtaining strings for the lines, how can you rely upon str or repr definitions of lines to make this a short/trivial method to write?
  • def __repr__(self): create/return a string identical to the __str__ output.
  • def is_local(self): return True if both caller and callee have the same area_code ; return False otherwise.

class CallError(Exception):

Be sure to include the (Exception) portion above in your class declaration, so that this is an extension of the Exception class (with all the exception properties that implies: it can be raise d).

  • def __init__(self, msg): create/initialize instance variable for msg .
  • def __str__(self): create/return a string based on msg as in this example: "CallError:negativecalllength:-5" #msg=="negativecalllength:-5"
  • def __repr__(self): create/return a string based on msg as in this example: "CallError('negativecalllength:-5')" #msg=="negativecalllength:-5"

class PhonePlanError(Exception):

Be sure to include the (Exception) portion above in your class declaration.

  • def __init__(self, msg): create/initialize instance variable for msg .
  • def __str__(self): create/return a string based on msg as in this example: "PhonePlanError:duplicatedlinetoadd" #msg=="duplicatedlinetoadd"
  • def __repr__(self): create/return a string based on msg as in this example: "PhonePlanError('duplicatedlinetoadd')" #msg=="duplicatedlinetoadd"

class PlanType:

  • def __init__(self, basic_rate, default_mins, rate_per_min, has_rollover=True): create/initialize instance variables for all four non-self parameters. You can assume all arguments are valid and there is no need to perform any checking.
  • def __str__(self): create/return a string as in this example: "PlanType(25.50,200,0.50,True)"
  • def __repr__(self): create/return a string identical to the __str__ output.

class PhonePlan:

  • def __init__(self, type, lines=None): create/initialize instance variables for type and the list of lines in the plan. When lines is not provided, use an empty list as its initial value. Create an instance variable calls to keep a list of calls made/received by any line of this plan, initialize it to be an empty list. Also create instance variables for balance , rollover_mins , and mins_to_pay , all starting at zero.
  • def __str__(self): create/return a string similar to: "PhonePlan(PlanType(25.50,200,0.50,True),[],[])" See test cases for more examples. Hint: when obtaining strings for the lists of lines/calls, how can you rely upon other str or repr definitions to make this a short/trivial method to write?
  • def __repr__(self): create/return a string identical to the __str__ output.
  • def activate_all(self): make sure every line in this plan is active after calling this method.
  • def deactivate_all(self): make sure every line in this plan is inactive after calling this method.
  • def add_call(self, call): add the given call to the end of the list of calls. Either caller or callee of the given call should be a line of this plan, otherwise do not append but raise a PhonePlanError with error message "callcannotbeadded" . If call can be added, check whether the call is billable based on the following rules:
    • A local call is free and not billable; Hint: use your Call class's is_local method to help.
    • A call made between two lines of this plan is free and not billable For a billable call, increment instance variable mins_to_pay based on the length of the call. All updates made in this method change instance variables directly and the method returns None .
  • def remove_call(self, call): remove the given call from the list of calls and returns None . If call is not present in the list, raise a PhonePlanError with error message "nosuchcalltoremove" .
  • def add_calls(self, calls): accept a list of calls , use add_call() to add each to the end of the list of calls and update mins_to_pay accordingly. Count and return the number of calls that are successfully added into the call record. Requirement and Hint: you must call add_call ; exception handling can help.
  • def make_call(self, caller, callee, length): create a call based on caller , callee , and length , then add it to the end of the list of calls and update mins_to_pay accordingly. Return True if the call is created and added successfully; return False if:
    • The call cannot be created due to a CallError ; or
    • The call cannot be added due to a PhonePlanError (neither caller nor callee belongs to this plan).
  • def mins_by_line(self, line): calculate and return the number of call minutes associated with the given line in this plan based on the current list of calls . A call is associated with a line if the line is either its caller or callee. Return zero if the given line is not included in this plan. Hint: Make sure your Line class's __eq__ method works, first.
  • def calls_by_line(self, line): calculate and return the number of calls associate with the given line in this plan based on the current list of calls . A call is associated with a line if the line is either its caller or callee. Return zero if the given line is not included in this plan.
  • def add_line(self, line): Add the given line to the end of the list of lines. If there is already a line in the plan by the same area_code and number, do not append but raise a PhonePlanError with error message "duplicatedlinetoadd" . All updates made in this method change instance variables directly and the method returns None . Hint: Make sure your Line class's __eq__ method works can help.
  • def remove_line(self, line): Remove the given line from the list of lines; remove calls that are only associated with the given line (not any other line of this plan) from the list of calls. A call is associated with a line if the line is either its caller or callee. If line is not present in the list of lines, perform no removal but raise a PhonePlanError with error message "nosuchlinetoremove" . All updates made in this method change instance variables directly and the method returns None . Hint: Make sure your Line class's __eq__ method works can help.
  • def calculate_bill(self): perform billing activity as if it is at the end of a pay period:
    • Calculate the amount to pay based on the billable minutes and plan type. Assume rules below:
    • If the number of minutes to pay ( mins_to_pay ) is no greater than default_min of the plan, no additional charge beyond the basic_rate ;
    • Otherwise, check if the remaining billable minutes can be covered by rollover_mins from the previous pay period;
    • Any remaining minutes will result in an additional charge beyond basic_rate using rate_per_min ;
    • Update balance and perform maintenance of the plan account:
    • Clear the list of calls and reset the number of minutes to pay ( mins_to_pay ) to zero;
    • Update balance. If the unpaid balance is too high (strictly greater than four times of the basic_rate ), deactivate all lines of the plan.
    • All updates made in this method change instance variables directly and the method returns None .
  • def pay_bill(self, amount=None): accept an integer amount , update balance, activate all lines if the pending balance drops under threshold (as defined above). You can assume the given amount is always an integer but a ValueError needs to be raised for negative amount with error message "amounttopaycannotbenegative" . If amount is not provided, pay the full unpaid balance. Note: a balance can be negative, indicating there is credit in this plan account. If amount is not provided and the account has no unpaid balance, no change should be made to the balance. All updates made in this method change instance variables directly and the method returns None

Example Session

>>> l = Line("GMU", 703, 9931000, True)
>>> l.name, l.area_code, l.number, l.is_active
('GMU', 703, 9931000, True)
>>> l.__str__()
'703-9931000(GMU)'
>>> l.__repr__()
"Line('GMU', 703, 9931000)"
>>> l.__eq__(Line("school", 703, 9931000))
True
>>> l.deactivate()
>>> l.is_active
False
>>> l.activate()
>>> l.is_active
True
>>>
>>> l2 = Line("George", 540, 5551234)
>>> l3 = Line("Mason", 703, 5551234)
>>> c = Call(l, l2, 20)
>>> str(c)
"Call(Line('GMU', 703, 9931000), Line('George',
540, 5551234), 20)"
>>> repr(c)
"Call(Line('GMU', 703, 9931000), Line('George',
540, 5551234), 20)"
>>> c.is_local()
False
>>> c2 = Call(l, l3, 30)
>>> c2.is_local()
True
>>>
>>> t = PlanType(30.0, 200, 0.50, True)
>>> t.basic_rate, t.default_mins, t.rate_per_min,
t.has_rollover
(30.0, 200, 0.5, True)
>>> str(t)
'PlanType(30.00, 200, 0.50, True)'
>>> repr(t)
'PlanType(30.00, 200, 0.50, True)'
>>>
>>> p = PhonePlan(t, [l])
>>> p.balance, p.mins_to_pay, p.rollover_mins
(0, 0, 0)
>>> str(p)
"PhonePlan(PlanType(30.00, 200, 0.50, True),
[Line('GMU', 703, 9931000)], [])"
>>> repr(p)
"PhonePlan(PlanType(30.00, 200, 0.50, True),
[Line('GMU', 703, 9931000)], [])"
>>>
>>> p.deactivate_all()
>>> l.is_active
False
>>> p.activate_all()
>>> l.is_active
True
>>>
>>> p.add_call(c)
>>> p.calls
[Call(Line('GMU', 703, 9931000), Line('George',
540, 5551234), 20)]
>>> p.mins_to_pay
20
>>> p.add_call(c2) #add local call
>>> p.calls
[Call(Line('GMU', 703, 9931000), Line('George',
540, 5551234), 20), Call(Line('GMU', 703,
9931000), Line('Mason', 703, 5551234), 30)]
>>> p.mins_to_pay
20
>>> p.remove_call(c)
>>> p.calls
[Call(Line('GMU', 703, 9931000), Line('Mason',
703, 5551234), 30)]
>>>
>>> # start over
... p = PhonePlan(t, [l])
>>> c3 = Call(l2, l3, 60)
>>> p.add_calls([c, c2, c3]) #c3 cannot add
2
>>> p.calls
[Call(Line('GMU', 703, 9931000), Line('George',
540, 5551234), 20), Call(Line('GMU', 703,
9931000), Line('Mason', 703, 5551234), 30)]
>>> p.mins_to_pay
20
>>>
>>> # start over
... p = PhonePlan(t, [l])
>>> p.make_call(l, l2, 20)
True
>>> p.calls
[Call(Line('GMU', 703, 9931000), Line('George',
540, 5551234), 20)]
>>> p.mins_to_pay
20
>>> p.make_call(l3, l, 15)
True
>>> p.calls
[Call(Line('GMU', 703, 9931000), Line('George',
540, 5551234), 20), Call(Line('Mason', 703,
5551234), Line('GMU', 703, 9931000), 15)]
>>> p.mins_to_pay
20
>>> p.make_call(l, l2, -10) #CallError
False
>>> p.make_call(l2, l3, 20) #PhonePlanError
False
>>> p.calls
[Call(Line('GMU', 703, 9931000), Line('George',
540, 5551234), 20), Call(Line('Mason', 703,
5551234), Line('GMU', 703, 9931000), 15)]
>>> p.mins_to_pay
20
>>> p.mins_by_line(l)
35
>>> p.calls_by_line(l)
2
>>> p.mins_by_line(l2) #l2 not in plan
0
>>> p.calls_by_line(l2) #l2 not in plan
0
>>>
>>> str(p)
"PhonePlan(PlanType(30.00, 200, 0.50, True),
[Line('GMU', 703, 9931000)], [Call(Line('GMU',
703, 9931000), Line('George', 540, 5551234), 20),
Call(Line('Mason', 703, 5551234), Line('GMU', 703,
9931000), 15)])"
>>> p.add_line(l2)
>>> p.make_call(l2,l3,20)
True
>>> str(p)
"PhonePlan(PlanType(30.00, 200, 0.50, True),
[Line('GMU', 703, 9931000), Line('George', 540,
5551234)], [Call(Line('GMU', 703, 9931000),
Line('George', 540, 5551234), 20),
Call(Line('Mason', 703, 5551234), Line('GMU', 703,
9931000), 15), Call(Line('George', 540, 5551234),
Line('Mason', 703, 5551234), 20)])"
>>> p.remove_line(l2)
>>> str(p)
"PhonePlan(PlanType(30.00, 200, 0.50, True),
[Line('GMU', 703, 9931000)], [Call(Line('GMU',
703, 9931000), Line('George', 540, 5551234), 20),
Call(Line('Mason', 703, 5551234), Line('GMU', 703,
9931000), 15)])"
>>>
>>> # start over, pre-set p
... c1 = Call(l,l2,10)
>>> c2 = Call(l2,l,75)
>>> c3 = Call(l3,l,40)
>>> c4 = Call(l,l3,50)
>>> p = PhonePlan(t,[l])
>>> p.calls = [c1,c2,c3,c4]
>>> p.mins_to_pay = 175
>>> p.calculate_bill() #basic rate only
>>> p.balance, p.rollover_mins, p.mins_to_pay,
p.calls
(30.0, 25, 0, [])
>>>
>>> t = PlanType(30.0,100,0.50)
>>> p = PhonePlan(t,[l])
>>> p.calls = [c1,c2,c3,c4]
>>> p.mins_to_pay = 175
>>> p.rollover_mins = 20
>>> p.calculate_bill()
>>> p.balance, p.rollover_mins, p.mins_to_pay,
p.calls
(57.5, 0, 0, [])
>>>
>>> t = PlanType(20.0,100,1,False)
>>> p = PhonePlan(t,[l])
>>> p.calls = [c1,c2,c3,c4]
>>> p.mins_to_pay = 175
>>> p.calculate_bill() #no rollover
>>> p.balance, p.rollover_mins, p.mins_to_pay,
p.calls
(95.0, 0, 0, [])
>>> l.is_active #balance too high, line inactive
False
>>> p.pay_bill()
>>> p.balance
0
>>> l.is_active
True
>>>
>>> # exception basics
... ce = CallError("error")
>>> str(ce)
'CallError: error'
>>> repr(ce)
"CallError('error')"
>>> raise ce
Traceback (most recent call last):
File "", line 1, in
__main__.CallError: CallError: error
>>>
>>> pe = PhonePlanError("problem")
>>> str(pe)
'PhonePlanError: problem'
>>> repr(pe)
"PhonePlanError('problem')"
>>> raise pe
Traceback (most recent call last):
File "", line 1, in
__main__.PhonePlanError: PhonePlanError: problem
>>>
>>> # trigger exceptions
... c = Call(l, l2, -30)
Traceback (most recent call last):
.....
__main__.CallError: CallError: negative call
length: -30
>>> c = Call(l, l, 10)
Traceback (most recent call last):
.....
__main__.CallError: CallError: line 703-
9931000(GMU) cannot call itself
>>> l.deactivate()
>>> c = Call(l, l2, 10)
Traceback (most recent call last):
.....
__main__.CallError: CallError: line 703-
9931000(GMU) not active
>>> p.add_call(Call(l2, l3, 10))
Traceback (most recent call last):
.....
__main__.PhonePlanError: PhonePlanError: call
cannot be added
>>> p.remove_call(Call(l2, l3, 10))
Traceback (most recent call last):
.....
__main__.PhonePlanError: PhonePlanError: no such
call to remove
>>> p.add_line(Line("school", 703, 9931000))
Traceback (most recent call last):
.....
__main__.PhonePlanError: PhonePlanError:
duplicated line to add
>>> p.remove_line(l3)
Traceback (most recent call last):
.....
__main__.PhonePlanError: PhonePlanError: no such
line to remove
>>> p.pay_bill(-30)
Traceback (most recent call last):
.....
ValueError: amount to pay cannot be negative
>>>
Academic Honesty!
It is not our intention to break the school's academic policy. Posted solutions are meant to be used as a reference and should not be submitted as is. We are not held liable for any misuse of the solutions. Please see the frequently asked questions page for further questions and inquiries.
Kindly complete the form. Please provide a valid email address and we will get back to you within 24 hours. Payment is through PayPal, Buy me a Coffee or Cryptocurrency. We are a nonprofit organization however we need funds to keep this organization operating and to be able to complete our research and development projects.