diff --git a/equator.py b/equator.py index 60dea64..d4f703f 100644 --- a/equator.py +++ b/equator.py @@ -12,6 +12,7 @@ def printOutput(out): # No arguments, enter interpreter mode if len(sys.argv) == 1: print(f"{consts.NAME} (v{consts.VERSION})") + print(f"by {consts.AUTHOR}") print("Interpreter Mode") print("Press Ctrl+C to quit") try: diff --git a/lib/consts.py b/lib/consts.py index 9b3d7ff..2cd97c8 100644 --- a/lib/consts.py +++ b/lib/consts.py @@ -3,14 +3,15 @@ import decimal NAME = "Equator" -VERSION = "0.5.0" +VERSION = "0.5.1" +AUTHOR = "Miguel Guthridge" # Get 15 decimal places of precision - the max given by sympy MAX_PRECISION = 14 FRACTION_DENOM_LIMITER = 1_000_000_000 # The minimum of abs(log_10(d)) for exponent to be presented using -# exponent notation +# exponent notation by default MIN_EXP_LOG = 9 # Operators used to split string @@ -27,7 +28,8 @@ NEGATE = "neg" -CONSTANTS = { +# Numeric constants +NUM_CONSTANTS = { "pi": decimal.Decimal(math.pi), "e": decimal.Decimal(math.e), "oo": decimal.Decimal("inf"), diff --git a/lib/operation.py b/lib/operation.py index a874b2c..1536fee 100644 --- a/lib/operation.py +++ b/lib/operation.py @@ -1,3 +1,9 @@ +"""Contains functions for doing operations on things. +Mostly used inside the evaluate functions of segments and the like + +Author: Miguel Guthridge (hdsq@outlook.com.au) +""" + import math import sympy as sym from decimal import Decimal @@ -8,7 +14,15 @@ FUNCTION_OPERATOR_PRECEDENCE = 10 NO_OPERATION_PRECEDENCE = 10 -def operatorPrecedence(op: str): +def operatorPrecedence(op: str) -> int: + """Returns an int representing the precedence of an operation + + Args: + op (str): operator + + Returns: + int: precedence + """ if op in ['^']: return 3 elif op in ['*', '/']: return 2 elif op in ['+', '-']: return 1 @@ -49,6 +63,21 @@ def zeroRound(num): return num def doOperation(operator: str, a, b): + """Function for handling the logic of an operation + In the future we may move to a method where operation token types implement + their own operate function, to remove nasty things like this + + Args: + operator (str): operation to do + a (Operatable): left + b (Operatable): right + + Raises: + ValueError: unrecognised operation (something is horribly wrong) + + Returns: + Operatable: result + """ a = conditionalDecimal(a) b = conditionalDecimal(b) if operator == '^': @@ -68,6 +97,17 @@ def doOperation(operator: str, a, b): return res def doFunction(func: str, a): + """Function for handling logic of evaluating functions. + In the future this may be moved into subclasses for each function, in the + hope of tidying things up + + Args: + func (str): function to do + a (Operatable): thing to operate on + + Returns: + Operatable: result + """ if func == "sqrt": return sym.sqrt(a) elif func == "sin": @@ -85,9 +125,9 @@ def doFunction(func: str, a): elif func == "abs": return sym.Abs(a) elif func == "deg": - return a * 180 / consts.CONSTANTS["pi"] + return a * 180 / consts.NUM_CONSTANTS["pi"] elif func == "rad": - return a / 180 * consts.CONSTANTS["pi"] + return a / 180 * consts.NUM_CONSTANTS["pi"] elif func == consts.NEGATE: return -a elif func == "exp": @@ -101,8 +141,17 @@ def doFunction(func: str, a): return sym.log(a, base) def getConstant(const: str): - if const in consts.CONSTANTS: - return str(consts.CONSTANTS[const]) + """Returns the representation of a constant as a stringified decimal + r the original string if it isn't a constant + + Args: + const (str): potential constant to replace + + Returns: + str: representation of constant if applicable otherwise original str + """ + if const in consts.NUM_CONSTANTS: + return str(consts.NUM_CONSTANTS[const]) else: return const diff --git a/lib/parse.py b/lib/parse.py index 6a4f938..8d751b1 100644 --- a/lib/parse.py +++ b/lib/parse.py @@ -52,7 +52,7 @@ def parseToken(word: str, unwrap_symbols=True): return tokens.Operator(word) else: # Parse symbols and constants - if word in consts.CONSTANTS: + if word in consts.NUM_CONSTANTS: return tokens.Constant(word) else: return tokens.Symbol(word) diff --git a/lib/segment.py b/lib/segment.py index bd5a393..68d5850 100644 --- a/lib/segment.py +++ b/lib/segment.py @@ -1,22 +1,48 @@ +"""Class definitions for segments (collections of tokens in a parsable order) +These contain: + - Definitions for managing order of operations + - Definitions for stringifying collections of tokens + - Definitions for evaluating segments + +Author: Miguel Guthridge (hdsq@outlook.com.au) +""" + from . import tokens from . import consts from . import operation class Segment(tokens.Token): + """Hierarchy of tokens in a form that can be simplified and calculated with + """ def __init__(self, contents: list): self._contents = contents # Don't even bother trying to parse if there's nothing there if len(self._contents) == 0: return - self.parseBrackets() - self.parseFunctions() - self.parseOperators(['^']) - self.parseOperators(['*', '/']) - self.parseLeadingNegative() - self.parseOperators(['+', '-']) - self.parseOperators(['=']) + self._parseBrackets() + self._parseFunctions() + self._parseOperators(['^']) + self._parseOperators(['*', '/']) + self._parseLeadingNegative() + self._parseOperators(['+', '-']) + self._parseOperators(['=']) - def stringify(self, num_type=None): + def stringify(self, num_type:str=None): + """Returns a string representing the segment + Unlike str(obj), has options to control fancy stringification + + Args: + num_type (str, optional): + Option for number stringification mode. Is passed onto + number-type tokens to control how they are represented. + Defaults to None. + + Raises: + ValueError: Error's with user input + + Returns: + str: string representation of this segment + """ if len(self._contents) == 0: return "0" elif len(self._contents) == 1: @@ -37,14 +63,63 @@ def stringify(self, num_type=None): r_str = "(" + r_str + ")" return f"{l_str} {op.stringify(num_type)} {r_str}" - + + def evaluate(self): + """Returns the evaluation of this segment + + Raises: + ValueError: Error when evaluating + + Returns: + Operatable: The result of the conversion: stringify for proper result + in a meaninful format + """ + if len(self._contents) == 0: + return 0.0 + + if len(self._contents) == 1: + return self._contents[0].evaluate() + + elif len(self._contents) == 3: + op = self._contents[1] + a = self._contents[0] + b = self._contents[2] + return operation.doOperation(op, a.evaluate(), b.evaluate()) + + else: + raise ValueError("Evaluation error: couldn't evaluate segment:\nBad content length\n" + + repr(self)) + + def getOperatorPrecedence(self): + """Returns the precedence of the top operator of this segment, as per + operationPrecedence function in operator.py + + Raises: + ValueError: Middle contents isn't an operator + ValueError: Contents are bad length - this shouldn't happen + + Returns: + int: operator precedence + """ + if len(self._contents) in [0, 1]: + return operation.NO_OPERATION_PRECEDENCE + elif len(self._contents) == 3: + if isinstance(self._contents[1], tokens.Operator): + return operation.operatorPrecedence(self._contents[1]) + else: + raise ValueError("Precedence error: failed to get operator for:\n" + + repr(self)) + else: + raise ValueError("Precedence error: Bad content length for\n" + + repr(self)) + def __str__(self): return self.stringify() def __repr__(self) -> str: return "Segment(" + repr(self._contents) + ")" - def parseBrackets(self): + def _parseBrackets(self): # List after parse out = [] # Items collected in bracket @@ -84,7 +159,7 @@ def parseBrackets(self): self._contents = out - def parseFunctions(self): + def _parseFunctions(self): if len(self._contents) < 2: return out = [] @@ -105,7 +180,7 @@ def parseFunctions(self): out.append(self._contents[-1]) self._contents = out - def parseLeadingNegative(self): + def _parseLeadingNegative(self): # Expands '-x' to '(0 - x)' if len(self._contents) < 2: return @@ -132,7 +207,7 @@ def parseLeadingNegative(self): self.contents = out""" - def parseOperators(self, operators: list): + def _parseOperators(self, operators: list): # Check for starting and ending with operators for op in operators: @@ -156,8 +231,14 @@ def parseOperators(self, operators: list): and str(self._contents[i]) in operators and not found: skip = 2 found = True + # Check for negative before lower signs + if isinstance(self._contents[i-1], tokens.Operator) \ + and self._contents[i] == '-': + out.append(self._contents[i-1]) + out.append(NegateFunction(self._contents[i+1])) + skip += 1 # Check for leading negative - if self._contents[i+1] == '-': + elif self._contents[i+1] == '-': if len(self._contents) == i + 2: raise ValueError("Parser Error: Expected value after leading negative") neg = NegateFunction(self._contents[i+2]) @@ -176,39 +257,10 @@ def parseOperators(self, operators: list): raise ValueError("Parser error: expected full expression after operator group") self._contents = out - - def evaluate(self): - - if len(self._contents) == 0: - return 0.0 - - if len(self._contents) == 1: - return self._contents[0].evaluate() - - elif len(self._contents) == 3: - op = self._contents[1] - a = self._contents[0] - b = self._contents[2] - return operation.doOperation(op, a.evaluate(), b.evaluate()) - - else: - raise ValueError("Evaluation error: couldn't evaluate segment:\nBad content length\n" - + repr(self)) - - def getOperatorPrecedence(self): - if len(self._contents) in [0, 1]: - return operation.NO_OPERATION_PRECEDENCE - elif len(self._contents) == 3: - if isinstance(self._contents[1], tokens.Operator): - return operation.operatorPrecedence(self._contents[1]) - else: - raise ValueError("Precedence error: failed to get operator for:\n" - + repr(self)) - else: - raise ValueError("Precedence error: Bad content length for\n" - + repr(self)) - + class Function(Segment): + """Segment representing a function operation + """ def __init__(self, type: tokens.Symbol, on: Segment): self._op = type self._on = on @@ -219,17 +271,37 @@ def __str__(self): def __repr__(self) -> str: return f"Function({self._op}, {self._on})" - def stringify(self, num_type): + def stringify(self, num_type: str): + """Returns string version of function, for presenting to the user + + Args: + num_type (str): how to represent numbers + + Returns: + str: string representation of string and its contents + """ return f"{self._op.stringify()}({self._on.stringify()})" def evaluate(self): + """Returns evaluation of the function + + Returns: + Operatable: result of function + """ e = self._on.evaluate() return operation.doFunction(str(self._op), e) def getOperatorPrecedence(self): + """Returns operator precedence of function + + Returns: + int: precedence + """ return operation.FUNCTION_OPERATOR_PRECEDENCE class NegateFunction(Function): + """Special function for representing leading negatives + """ def __init__(self, on: Segment): self._op = consts.NEGATE self._on = on @@ -238,4 +310,12 @@ def __str__(self): return self.stringify(None) def stringify(self, num_type): + """Return string representing contents + + Args: + num_type (str): number representation mode + + Returns: + str: contents + """ return f"-{self._on.stringify(num_type)}" diff --git a/lib/tokens.py b/lib/tokens.py index 11c5a57..60aae8f 100644 --- a/lib/tokens.py +++ b/lib/tokens.py @@ -49,7 +49,7 @@ def asMultipleOf(a: Decimal, b: str): str | None: a in terms of b or a normally (but None if doing so is unreasonable) """ # FIXME: present as pi/3 rather than 1/3*pi - ret = str(Fraction(a / consts.CONSTANTS[b]).limit_denominator(consts.FRACTION_DENOM_LIMITER)) + ret = str(Fraction(a / consts.NUM_CONSTANTS[b]).limit_denominator(consts.FRACTION_DENOM_LIMITER)) if ret == "0": return None if len(ret) < 10: @@ -69,7 +69,7 @@ def asPowerOf(a: Decimal, b: str): Returns: str | None: a as a power of b or a normally (but None if doing so is unreasonable) """ - ret = str(Fraction(math.log(a, consts.CONSTANTS[b])).limit_denominator(consts.FRACTION_DENOM_LIMITER)) + ret = str(Fraction(math.log(a, consts.NUM_CONSTANTS[b])).limit_denominator(consts.FRACTION_DENOM_LIMITER)) # Add brackets if it's a fraction if "/" in ret: ret = f"({ret})" @@ -180,7 +180,7 @@ class Constant(Number): Stringifies to the name of the constant """ def evaluate(self): - return consts.CONSTANTS[self._contents] + return consts.NUM_CONSTANTS[self._contents] def __str__(self) -> str: return self._contents diff --git a/tests/__init__.py b/tests/__init__.py index e69de29..3e71d9a 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -0,0 +1 @@ +# Almost empty file diff --git a/tests/helpers.py b/tests/helpers.py index 8e3fe18..404ef3c 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -1,5 +1,22 @@ +"""Contains helper functions for testing output + +Author: Miguel Guthridge (hdsq@outlook.com.au) +""" + +def removeSpacing(s: str): + """Remove spaces from a string""" + return s.replace(" ", "") def simplifyEq(results: list[dict]): + """Returns list of dictionaries + Each dict contains one set of unique results + + Args: + results (list[dict]): results of equate function + + Returns: + list[dict]: parsed results + """ ret = [] for r in results: d = dict() @@ -8,8 +25,13 @@ def simplifyEq(results: list[dict]): ret.append(d) return ret -def removeSpacing(s: str): - return s.replace(" ", "") - def simplifyExp(results: list[str]): + """Simplify results of an expression by removing spacing for each result + + Args: + results (list[str]): output from equate function + + Returns: + list[str]: parsed output from equate function + """ return [removeSpacing(s) for s in results] diff --git a/tests/test_basic.py b/tests/test_basic.py index 2cf2e64..7f206cd 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -1,4 +1,6 @@ """Ensure that the most basic of the basic features work + +Author: Miguel Guthridge (hdsq@outlook.com.au) """ from ..lib.smart_equate import equate diff --git a/tests/test_exponent_notation.py b/tests/test_exponent_notation.py index 1da403e..8ec7660 100644 --- a/tests/test_exponent_notation.py +++ b/tests/test_exponent_notation.py @@ -1,4 +1,6 @@ """Ensure that eponent notation is calculated correctly + +Author: Miguel Guthridge (hdsq@outlook.com.au) """ from .helpers import simplifyExp @@ -34,6 +36,6 @@ def test_decimal_presentation_huge(): assert equate("1E-32") == ["1e-32"] def test_decimal_presentation_huge_no_round(): - # Ensure numbers are presented in scientific notation even if they're - # really long + """Ensure numbers are presented in scientific notation even if they're + really long, and can't just be rounded away""" assert equate("52^8") == ["5.3459728531456e+13"] diff --git a/tests/test_fractions.py b/tests/test_fractions.py index 9e3bb98..f1a6204 100644 --- a/tests/test_fractions.py +++ b/tests/test_fractions.py @@ -1,4 +1,6 @@ """Ensure that results are presented as fractions where possible + +Author: Miguel Guthridge (hdsq@outlook.com.au) """ from ..lib.smart_equate import equate @@ -10,5 +12,5 @@ def test_basic(): assert simplifyExp(equate("0.5")) == ["1/2"] def test_with_symbols(): - #assert equate("1/2 * x") == ["x / 2"] + #assert simplifyExp(equate("1/2 * x")) == ["x/2"] pass diff --git a/tests/test_functions.py b/tests/test_functions.py index 829c278..a8162e4 100644 --- a/tests/test_functions.py +++ b/tests/test_functions.py @@ -1,4 +1,6 @@ """Checks to make sure that functions provide the correct output + +Author: Miguel Guthridge (hdsq@outlook.com.au) """ from .helpers import simplifyExp diff --git a/tests/test_leading_negative.py b/tests/test_leading_negative.py new file mode 100644 index 0000000..ace6d85 --- /dev/null +++ b/tests/test_leading_negative.py @@ -0,0 +1,27 @@ +"""Test for ensuring that leading negatives are interpreted correctly + +Author: Miguel Guthridge (hdsq@outlook.com.au) +""" + +from .helpers import simplifyExp, simplifyEq + +from ..lib.smart_equate import equate + +def test_starting_negative(): + assert equate("-1") == ["-1"] + +def test_mul_div(): + assert equate("4 * -1") == ["-4"] + assert equate("4/-2") == ["-2"] + +def test_neg_to_power(): + assert simplifyExp(equate("-2^2 *-4^-3")) == ["1/16"] + assert equate("-2^2") == ["-4"] + +def test_to_neg_power(): + assert simplifyExp(equate("4^-1")) == ["1/4"] + assert simplifyExp(equate("2^-3/2")) == ["1/16"] + +def test_equality(): + assert simplifyEq(equate("-1 = -x")) == [{"x": "1"}] + assert simplifyEq(equate("x = -2 * -3")) == [{"x": "6"}] diff --git a/tests/test_order_of_operations.py b/tests/test_order_of_operations.py index ac2e477..03f7857 100644 --- a/tests/test_order_of_operations.py +++ b/tests/test_order_of_operations.py @@ -1,4 +1,6 @@ -"""Ensure that order of operations are considered correctly +"""Ensure that order of operations are interpreted correctly + +Author: Miguel Guthridge (hdsq@outlook.com.au) """ from .helpers import simplifyExp @@ -17,14 +19,6 @@ def test_operations_power(): assert equate("2 * 2^2") == ["8"] assert equate("2^3^2") == ["64"] -def test_leading_negative(): - assert equate("-1") == ["-1"] - assert equate("-2^2") == ["-4"] - assert simplifyExp(equate("4^-1")) == ["1/4"] - assert equate("4 * -1") == ["-4"] - assert simplifyExp(equate("-2^2 *-4^-3")) == ["1/16"] - assert simplifyExp(equate("2^-3/2")) == ["1/16"] - def test_brackets(): assert equate("(2 + 1) / 3") == ["1"] assert equate("2^(1+1)") == ["4"] @@ -34,3 +28,6 @@ def test_brackets(): def test_exponent_operations(): assert equate("1E+1 + 2") == ["12"] assert equate("1.25E1 - 0.5") == ["12"] + +def test_equals(): + pass diff --git a/tests/test_output_formatters.py b/tests/test_output_formatters.py index 1af44ad..879f71b 100644 --- a/tests/test_output_formatters.py +++ b/tests/test_output_formatters.py @@ -1,4 +1,6 @@ """Ensure that output formatters format output correctly + +Author: Miguel Guthridge (hdsq@outlook.com.au) """ from .helpers import simplifyExp diff --git a/tests/test_smart_representation.py b/tests/test_smart_representation.py index 1fe7f70..693c2c1 100644 --- a/tests/test_smart_representation.py +++ b/tests/test_smart_representation.py @@ -1,4 +1,7 @@ """Ensure that calculations involving smart representation work correctly +This includes representing in terms of pi, e, and as square roots + +Author: Miguel Guthridge (hdsq@outlook.com.au) """ from .helpers import simplifyExp