From 32ecad0d8306f3b535e0509674a82e5c7d2bbfe1 Mon Sep 17 00:00:00 2001 From: Damien Dart Date: Tue, 7 Nov 2023 23:32:13 +0000 Subject: [PATCH] Add intial implementation of an RPN calculator. --- .editorconfig | 2 +- Taskfile.yml | 4 +- bin/rpncalc | 139 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 142 insertions(+), 3 deletions(-) create mode 100755 bin/rpncalc diff --git a/.editorconfig b/.editorconfig index f1b2203..196e0e8 100644 --- a/.editorconfig +++ b/.editorconfig @@ -12,5 +12,5 @@ indent_style = space insert_final_newline = true trim_trailing_whitespace = true -[{git-hometime,install,*.py}] +[{git-hometime,install,rpncalc,*.py}] indent_size = 4 diff --git a/Taskfile.yml b/Taskfile.yml index d055429..8fc09f8 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -51,12 +51,12 @@ tasks: lint:python:black: cmds: - - '.venv/bin/black --check --diff install bin/git-hometime bin/markdown-tidy' + - '.venv/bin/black --check --diff install bin/git-hometime bin/markdown-tidy bin/rpncalc' desc: 'Lint Python scripts with Black' lint:python:flake8: cmds: - - '.venv/bin/flake8 install bin/git-hometime bin/markdown-tidy' + - '.venv/bin/flake8 install bin/git-hometime bin/markdown-tidy bin/rpncalc' desc: 'Lint Python scripts with Flake8' lint:shell: diff --git a/bin/rpncalc b/bin/rpncalc new file mode 100755 index 0000000..0232d7e --- /dev/null +++ b/bin/rpncalc @@ -0,0 +1,139 @@ +#!/usr/bin/env python3 +""" +A simple command-line Reverse Polish Notation calculator. + +Inspired by . +""" + +# This file was written by Damien Dart, . This is +# free and unencumbered software released into the public domain. For +# more information, please refer to the accompanying "UNLICENCE" file. + +import decimal +import inspect +import readline + + +class RPNCalculator: + def __init__(self): + self._stack = [] + self._operations = {} + + def operation(self, code: str): + def decorator(func): + spec = inspect.getfullargspec(func) + + def op(*args): + arity = len(spec.args) + + if len(self._stack) < arity: + raise IndexError( + f"{code!a} requires {arity} item{'s'[:arity^1]} on stack" # noqa: E501 + ) + + arguments = [self._stack.pop() for _ in range(arity)] + result = func(*reversed(arguments)) + + self._stack.extend(result) + + self._operations[code] = op + + return func + + return decorator + + def keywords(self): + return self._operations.keys() + + def execute(self, command: str) -> decimal.Decimal: + for token in command.split(): + if token in self._operations: + self._operations[token]() + else: + try: + token = decimal.Decimal(token) + self._stack.append(token) + except decimal.InvalidOperation: + raise ValueError(f"unrecognised token: {token}") + + try: + return self._stack[0] + except IndexError: + return + + +def make_calculator() -> RPNCalculator: + calculator = RPNCalculator() + + @calculator.operation("+") + @calculator.operation("add") + def add(x, y): + return [x + y] + + @calculator.operation("-") + @calculator.operation("sub") + def subtract(x, y): + return [x - y] + + @calculator.operation("*") + @calculator.operation("mul") + def multiply(x, y): + return [x * y] + + @calculator.operation("/") + @calculator.operation("div") + def divide(x, y): + return [x / y] + + @calculator.operation("**") + @calculator.operation("pow") + def pow(x, y): + return [x**y] + + @calculator.operation("drop") + def drop(x): + return [] + + @calculator.operation("dup") + def dup(x): + return [x, x] + + @calculator.operation("swap") + def swap(x, y): + return [y, x] + + return calculator + + +def make_completer(calculator): + def complete(text, state): + keywords = calculator.keywords() + results = [x for x in keywords if x.startswith(text)] + [None] + + return results[state] + " " + + return complete + + +if __name__ == "__main__": + calculator = make_calculator() + + readline.parse_and_bind("tab: complete") + readline.set_completer(make_completer(calculator)) + + while True: + try: + line = input("> ").strip() + except (EOFError, KeyboardInterrupt): + print() + break + + try: + result = calculator.execute(line) + except decimal.DivisionByZero: + print("ERROR: division by zero") + except Exception as e: + print("ERROR:", e) + else: + if result is not None: + print(result)