Add console interface: calculator loop, ASCII title and tests

MIL-003 Console interface.

- Add calculator() with recursive restart: y continues with the result,
  n starts a new calculation, q quits
- Show one ASCII title at start: a calculator with the lettering to
  its right
- Re-prompt on invalid numbers, operations and answers; restart on
  division by zero
- Add __main__.py for python -m calc and the calc console script
- Add interface tests with input() fed through monkeypatch

Closes #22
Closes #23
Closes #24
Closes #25

Task: MIL-003#1
Task: MIL-003#2
Task: MIL-003#3
Task: MIL-003#4
This commit is contained in:
2026-10-04 20:59:53 +08:00
parent 9030510773
commit 6d99810e30
5 changed files with 209 additions and 15 deletions
+9
View File
@@ -0,0 +1,9 @@
"""!
@file __main__.py
@brief Allows running the calculator with `python -m calc`.
"""
from calc.calculator import main
if __name__ == "__main__":
main()
+112
View File
@@ -0,0 +1,112 @@
"""!
@file calculator.py
@brief The interactive console calculator.
"""
from calc.constants import (
ANSWER_CONTINUE,
ANSWER_NEW,
ANSWER_QUIT,
ASCII_TITLE,
MESSAGE_DIVIDE_BY_ZERO,
MESSAGE_GOODBYE,
MESSAGE_INVALID_ANSWER,
MESSAGE_INVALID_NUMBER,
MESSAGE_INVALID_OPERATION,
PROMPT_AGAIN,
PROMPT_FIRST_NUMBER,
PROMPT_NEXT_NUMBER,
PROMPT_OPERATION,
)
from calc.operations import OPERATIONS
def format_number(value: float) -> str:
"""!
@brief Format a number without a trailing ".0" for whole values.
@param value The number to format.
@return The text to show the user.
"""
return str(int(value)) if value.is_integer() else str(value)
def read_number(prompt: str) -> float:
"""!
@brief Ask for a number until the user types a valid one.
@param prompt The text shown to the user.
@return The number typed by the user.
"""
while True:
try:
return float(input(prompt))
except ValueError:
print(MESSAGE_INVALID_NUMBER)
def read_operation() -> str:
"""!
@brief Show the available operations and ask for one until it is valid.
@return The symbol of the chosen operation, a key of OPERATIONS.
"""
for symbol in OPERATIONS:
print(symbol)
while True:
symbol = input(PROMPT_OPERATION).strip()
if symbol in OPERATIONS:
return symbol
print(MESSAGE_INVALID_OPERATION)
def read_answer(result: float) -> str:
"""!
@brief Ask whether to continue with the result, start anew or quit.
@param result The result of the last calculation.
@return One of ANSWER_CONTINUE, ANSWER_NEW or ANSWER_QUIT.
"""
while True:
answer = input(PROMPT_AGAIN.format(result=format_number(result)))
answer = answer.strip().lower()
if answer in (ANSWER_CONTINUE, ANSWER_NEW, ANSWER_QUIT):
return answer
print(MESSAGE_INVALID_ANSWER)
def calculator() -> None:
"""!
@brief Run one calculation session.
The user may continue with the previous result. Starting a new
calculation calls this function again (recursion), which gives a clean
restart. A division by zero also restarts the calculator.
"""
num1 = read_number(PROMPT_FIRST_NUMBER)
while True:
symbol = read_operation()
num2 = read_number(PROMPT_NEXT_NUMBER)
try:
result = OPERATIONS[symbol](num1, num2)
except ZeroDivisionError:
print(MESSAGE_DIVIDE_BY_ZERO)
calculator()
return
print(
f"{format_number(num1)} {symbol} {format_number(num2)} "
f"= {format_number(result)}"
)
answer = read_answer(result)
if answer == ANSWER_CONTINUE:
num1 = result
elif answer == ANSWER_NEW:
calculator()
return
else:
print(MESSAGE_GOODBYE)
return
def main() -> None:
"""!
@brief Program entry point: show the ASCII title and start the calculator.
"""
print(ASCII_TITLE)
calculator()
+6 -15
View File
@@ -29,26 +29,17 @@ MESSAGE_INVALID_ANSWER: str = "Please answer with one of the listed letters."
MESSAGE_DIVIDE_BY_ZERO: str = "You cannot divide by zero."
MESSAGE_GOODBYE: str = "Goodbye!"
## ASCII title shown when the program starts.
## ASCII title shown at start: a calculator with the lettering to its right.
ASCII_TITLE: str = r"""
____ _ _ _
/ ___|__ _ | | ___ _ _| | __ _| |_ ___ _ __
| | / _` | | |/ __| | | | |/ _` | __/ _ \| '__|
| |__| (_| | | | (__| |_| | | (_| | || (_) | |
\____\__,_| |_|\___|\__,_|_|\__,_|\__\___/|_|
"""
## ASCII calculator shown under the title.
ASCII_CALCULATOR: str = r"""
_____________________
| _________________ |
| | 0 | |
| |_________________| |
| ___ ___ ___ ___ |
| | 7 | 8 | 9 | | + | |
| |___|___|___| |___| |
| | 4 | 5 | 6 | | - | |
| |___|___|___| |___| |
| ___ ___ ___ ___ | ____ _ _ _
| | 7 | 8 | 9 | | + | | / ___|__ _ | | ___ _ _| | __ _| |_ ___ _ __
| |___|___|___| |___| | | | / _` | | |/ __| | | | |/ _` | __/ _ \| '__|
| | 4 | 5 | 6 | | - | | | |__| (_| | | | (__| |_| | | (_| | || (_) | |
| |___|___|___| |___| | \____\__,_| |_|\___|\__,_|_|\__,_|\__\___/|_|
| | 1 | 2 | 3 | | x | |
| |___|___|___| |___| |
| | . | 0 | = | | / | |