Python Arena

A command calculator

In this project you build a calculator that you use by typing commands, one per line. Each command is a word that says what to do, followed by the numbers to do it with: add 3 4 adds 3 and 4, and sum 1 2 3 4 adds up any number of numbers. The calculator answers every line, and keeps going until you type quit.

When it's finished, a session looks like this. The person types these lines:

add 3 4
div ans 2
sum 1 2 3 4
mul ans ans
pow 2 8
div 5 0
add 2 three
history
quit

and the calculator answers them, with its prompt > in front of each typed line:

> add 3 4
7
> div ans 2
3.5
> sum 1 2 3 4
10
> mul ans ans
100
> pow 2 8
Error: unknown command pow
> div 5 0
Error: can't divide by zero
> add 2 three
Error: three is not a number
> history
1: add 3 4 = 7
2: div 7 2 = 3.5
3: sum 1 2 3 4 = 10
4: mul 10 10 = 100
> quit
Bye!

A mistake never stops the program: it gets a message that says what's wrong, and the next line is read as usual. ans stands for the last answer, and history lists every calculation that worked.

What you need

Everything up to module 18: while loops with break and continue (modules 9 to 12), strings and their indexes (module 11), functions that take parameters and return values, with the program's own code under if __name__ == "__main__": (modules 13 and 14), lists and for loops (modules 15 and 16), f-strings (module 17), and the string methods split and replace (module 18).

How you build it

Each milestone starts from your own code of the milestone before, and adds one thing to it:

  1. Four operations: a function for each operation, and calculate, which answers one line.
  2. Errors in commands: unknown commands and missing numbers get a message instead of a crash.
  3. Is it a number? Your own check for numbers, which also lets in decimals such as 2.5.
  4. Any number of numbers: sum, max, min and mean take as many numbers as you type.
  5. History: the calculator remembers what it calculated.
  6. The last answer: ans in a command means the answer before it.

Some checks call your functions directly, as in the lessons, and others type lines into the whole program and compare everything it prints.

Milestones

  1. Four operations
  2. Errors in commands
  3. Is it a number?
  4. Any number of numbers
  5. History
  6. The last answer

Milestone 1: Four operations

The calculator reads one line at a time and answers it, until the line is quit. The starting code already has that loop in main: for every line, it prints what calculate returns. Your part is the calculating.

A command is a word and two whole numbers, separated by spaces:

  • add 3 4 adds the numbers: 7
  • sub 10 4 subtracts the second number from the first: 6
  • mul 6 7 multiplies them: 42
  • div 7 2 divides the first by the second: 3.5
  1. Next to add, write the functions subtract(a, b), multiply(a, b) and divide(a, b). Each one returns its result. divide uses /, so divide(7, 2) returns 3.5.
  2. Write calculate(line). It gets one line, such as "add 3 4", and returns the answer as a string: "7". It takes the line apart, turns the two numbers into integers with int, calls the function for the command, and turns the result into a string with str.

The string method split from module 18 takes a line apart at its spaces, and gives the pieces as a list of strings:

In the Python interpreter
>>> parts = "add 3 4".split()
>>> parts
['add', '3', '4']
>>> parts[0]
'add'
>>> int(parts[1]) + int(parts[2])
7

For now, every line is a correct command; the next milestone deals with mistakes. With the input add 3 4, sub 10 4, mul 6 7, div 7 2 and quit, the program prints:

> add 3 4
7
> sub 10 4
6
> mul 6 7
42
> div 7 2
3.5
> quit
Bye!

Milestone 2: Errors in commands

People make typing mistakes. Right now add 3 stops the whole program with an IndexError, because there is no parts[2], and div 5 0 stops it with a ZeroDivisionError. A calculator should answer a mistake with a message and carry on. Change calculate so that it returns these messages instead of crashing, checking them in this order:

  1. A command that isn't add, sub, mul or div returns Error: unknown command pow (with the word that was typed).
  2. A command with too few or too many numbers returns Error: add needs 2 numbers (with the command's name).
  3. Dividing by zero returns Error: can't divide by zero.

And one change in main: a line that is empty, or has only spaces, gets no answer at all. The calculator just shows its prompt again.

With the input add 3, pow 2 8, div 5 0, an empty line, mul 2 3 4, mul 2 3 and quit, the program prints:

> add 3
Error: add needs 2 numbers
> pow 2 8
Error: unknown command pow
> div 5 0
Error: can't divide by zero
>
> mul 2 3 4
Error: mul needs 2 numbers
> mul 2 3
6
> quit
Bye!

The numbers are still whole numbers; the next milestone deals with text that isn't a number at all.

Milestone 3: Is it a number?

Two problems are left with the numbers. add 2 three still crashes with a ValueError in int, and the calculator can't do decimals: add 2.5 1 crashes too. Both are solved by looking at the text before turning it into a number.

  1. Write is_number(text). It returns True when the text is a number the calculator accepts, and False otherwise. A number has at least one digit; apart from digits it may have one decimal point . anywhere, and a minus sign - only as its very first character. So 12, -3, 0.5 and -2.25 are numbers, but "", -, 1.2.3, 4- and 12a are not.
  2. Write to_number(text), for a text that is_number accepts. It returns the number as an integer when the text has no decimal point, and as a float when it has one: to_number("12") returns 12, and to_number("2.5") returns 2.5.
  3. In calculate, after the checks of the command and the count, check each number: the first one that isn't a number returns Error: three is not a number (with the text that was typed). Then turn the numbers into numbers with to_number instead of int.

The check calls is_number on several texts:

print(is_number("12"), is_number("-3"), is_number("0.5"), is_number("-2.25"))
print(is_number(""), is_number("-"), is_number("1.2.3"), is_number("4-"), is_number("12a"))

and expects:

True True True True
False False False False False

With the input add 2.5 1, mul -3 4, add 2 three, sub 1.5 0.25, div 1 0.0 and quit, the program prints:

> add 2.5 1
3.5
> mul -3 4
-12
> add 2 three
Error: three is not a number
> sub 1.5 0.25
1.25
> div 1 0.0
Error: can't divide by zero
> quit
Bye!

Milestone 4: Any number of numbers

Adding up a long column of numbers two at a time is slow. Four new commands take any number of numbers, at least one:

  • sum 1 2 3 4 adds them all up: 10
  • max 4 -2 9.5 3 gives the largest: 9.5
  • min 4 -2 9.5 3 gives the smallest: -2
  • mean 1 2 3 4 gives their average, the sum divided by how many there are: 2.5
  1. Write a function for each one: total(numbers), largest(numbers), smallest(numbers) and mean(numbers). Each takes a list of numbers and returns its result. (The names sum, max and min are Python's own functions, which yours may use; giving your functions the same names would hide them.)
  2. In calculate, these four commands are known as well. One of them without any numbers returns Error: sum needs at least 1 number (with the command's name). The numbers are checked with is_number as before, and every text that isn't a number gets the same message as before.

The two-number commands work as they did, add 1 2 3 is still an error, and the checks keep their order: the command, then the count, then each number, then dividing by zero.

With the input sum 1 2 3 4, max 4 -2 9.5 3, min 4 -2 9.5 3, mean 1 2 3 4, sum, mean 7 and quit, the program prints:

> sum 1 2 3 4
10
> max 4 -2 9.5 3
9.5
> min 4 -2 9.5 3
-2
> mean 1 2 3 4
2.5
> sum
Error: sum needs at least 1 number
> mean 7
7.0
> quit
Bye!

Milestone 5: History

Now the calculator remembers. Every calculation that works is saved in a list, as one string with the line that was typed and its answer: add 3 4 = 7. A line that gets an error message isn't saved.

  1. In main, keep the history in a list that starts empty, and add each calculation that worked to it. Every answer that isn't an error message is a calculation that worked.
  2. Write print_history(history), which prints the saved calculations numbered from 1, as 1: add 3 4 = 7. When the list is empty, it prints No calculations yet.
  3. Two new commands, which main handles itself before calling calculate. They aren't saved in the history:
    • history prints the history with print_history.
    • clear empties the history and prints History cleared.

With the input history, add 3 4, pow 2 2, mean 1 2, history, clear, history, mul 5 5, history and quit, the program prints:

> history
No calculations yet.
> add 3 4
7
> pow 2 2
Error: unknown command pow
> mean 1 2
1.5
> history
1: add 3 4 = 7
2: mean 1 2 = 1.5
> clear
History cleared.
> history
No calculations yet.
> mul 5 5
25
> history
1: mul 5 5 = 25
> quit
Bye!

Milestone 6: The last answer

A calculation often goes on from the one before: double that, then take a quarter of it. The word ans in a command stands for the last answer, so mul ans 2 doubles it.

  • ans is the answer of the last calculation that worked. Before the first one, it is 0.
  • An error message doesn't change it, and neither do history and clear.
  • In the history, a calculation is saved with the number in place of ans: mul 7 2 = 14, not mul ans 2 = 14.

Do it in main, before the line goes to calculate: put the last answer into the line where it says ans. The string method replace from module 18 does exactly that, and calculate then gets an ordinary line of numbers. Keep the last answer as a string, as calculate returns it, so it can go straight into the line.

With the input add 3 4, mul ans 2, div ans 4, sub ans ans, history and quit, the program prints:

> add 3 4
7
> mul ans 2
14
> div ans 4
3.5
> sub ans ans
0.0
> history
1: add 3 4 = 7
2: mul 7 2 = 14
3: div 14 4 = 3.5
4: sub 3.5 3.5 = 0.0
> quit
Bye!