diff --git a/tests/test_io.py b/tests/test_io.py index adf5731..e27ed55 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -1,4 +1,4 @@ -from visma.io.checks import getVariables, areTokensEqual, isTokenInToken +from visma.io.checks import getVariables, areTokensEqual, isTokenInToken, checkSyntax from visma.io.parser import tokensToString from visma.io.tokenize import getTerms, normalize from visma.functions.operator import Operator, Plus @@ -54,6 +54,39 @@ def test_isTokenInToken(): assert not isTokenInToken(varA, varE) +def test_checkSyntax(): + test1 = False + test2 = False + test3 = False + test4 = False + test5 = False + eqn1 = "2 + sin(2)" + boolean, log = checkSyntax(eqn1) + if (boolean is True and log == "Standard syntax is followed"): + test1 = True + assert test1 + eqn2 = "2 + (log2)^(e+2) + sinh(x + x^2)" + boolean, log = checkSyntax(eqn2) + if (boolean is False and log == "For function 'log', arguments are not enclosed within parentheses"): + test2 = True + assert test2 + eqn3 = "0.2 + .5" + boolean, log = checkSyntax(eqn3) + if (boolean is False and log == "Decimal point must be between two integers"): + test3 = True + assert test3 + eqn4 = "2 + (x+2)(x+3)" + boolean, log = checkSyntax(eqn4) + if (boolean is False and log == "There must be an operator between close parenthesis and open parenthesis"): + test4 = True + assert test4 + eqn5 = "2 + (x+2)*(x+3)" + boolean, log = checkSyntax(eqn5) + if (boolean is True and log == "Standard syntax is followed"): + test5 = True + assert test5 + + ############# # io.parser # ############# diff --git a/visma/io/checks.py b/visma/io/checks.py index 4ec97f6..c45470b 100644 --- a/visma/io/checks.py +++ b/visma/io/checks.py @@ -131,6 +131,111 @@ def getVariables(lTokens, rTokens=None, variables=None): return variables +def checkSyntax(eqnString): + """Checks if input follows standard syntax or not + + Arguments: + eqnString {string} -- equation to be executed + + Returns: + bool -- if valid or not + log -- Message for easy debugging + """ + matchingParenthesis = 0 # Counter used to check if any close parenthesis is encountered before open parenthesis + for index, val in enumerate(eqnString): + if (val == '('): + matchingParenthesis += 1 + elif (val == ')'): + matchingParenthesis -= 1 + if (matchingParenthesis == -1): + log = "Close parenthesis encountered before open parethesis" + return False, log + if (matchingParenthesis != 0): + log = "Each open parenthesis must have a unique corresponding close parenthesis" + return False, log + + # Clean equation i.e removing whitespaces + eqnString = eqnString.replace(" ", "") + stringLen = len(eqnString) + + # For now explicitly defining function of length 2, as there is only one such + # function and no point in iterating through whole list of function for that + funcOfLen = {2: ['ln'], 3: [], 4: [], 5: []} + + # List to check if decimal point is preceded by an integer + integers = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'] + operators = ['-', '+', '*', '/', '=', '<', '>', '<=', '>=', '^', '(', ',', ';'] + + for func in funcs: + if (len(func) in [3, 4, 5]): + funcOfLen[len(func)].append(func) + for func in funcSyms: + if (len(func) in [3, 4, 5]): + funcOfLen[len(func)].append(func) + + lenCheck = 2 # Length of function to be checked starting from 2 + while (lenCheck <= 5): + if (stringLen >= 2): # Any function will need atleast 2 characters to be called. Example simplify(ln) + for index, val in enumerate(eqnString): + if (index < stringLen + 1 - lenCheck): + if (eqnString[index:index+lenCheck] in funcOfLen[lenCheck]): + if (eqnString[index:index+lenCheck+1] in funcOfLen[lenCheck+1]): + if (stringLen < index + lenCheck + 4): + log = "Either the function '{}' has no arguments or arguments are not enclosed within parentheses".format(eqnString[index:index+lenCheck+1]) + return False, log + invalidCheck = (eqnString[index+lenCheck+1] != '(') + if invalidCheck: + log = "For function '{}', arguments are not enclosed within parentheses".format(eqnString[index:index+lenCheck+1]) + return False, log + invalidCheck = (eqnString[index+lenCheck+2] == ')') + if invalidCheck: + log = "Function '{}' has no argument".format(eqnString[index:index+lenCheck+1]) + return False, log + else: + if (stringLen < index + lenCheck + 3): + log = "Either the function '{}' has no arguments or arguments are not enclosed within parentheses".format(eqnString[index:index+lenCheck]) + return False, log + invalidCheck = (eqnString[index+lenCheck] != '(') + if invalidCheck: + log = "For function '{}', arguments are not enclosed within parentheses".format(eqnString[index:index+lenCheck]) + return False, log + invalidCheck = (eqnString[index+lenCheck+1] == ')') + if invalidCheck: + log = "Function '{}' has no argument".format(eqnString[index:index+lenCheck]) + return False, log + else: + break + lenCheck += 1 + else: + break + + for index, val in enumerate(eqnString): + if (val == '.'): + if (index == 0) or (index == stringLen-1): + log = "Decimal point must be between two integers" + return False, log + invalidCheck = (eqnString[index-1] not in integers or eqnString[index+1] not in integers) + if (invalidCheck): + log = "Decimal point must be between two integers" + return False, log + + elif (val == ')'): + if (index < stringLen-2): + invalidCheck1 = (eqnString[index+1] == '(') + invalidCheck2 = False + if (eqnString[index+2] == '('): + invalidCheck2 = (eqnString[index+1] not in operators) + if (invalidCheck1 + invalidCheck2): + log = "There must be an operator between close parenthesis and open parenthesis" + return False, log + elif (index == stringLen-2): + invalidCheck = (eqnString[index+1] in operators and eqnString[index+1] != ';') + if (invalidCheck): + log = "Open parenthesis or an operator cannot be at the end of an equation" + return False, log + return True, "Standard syntax is followed" + + def checkEquation(terms, symTokens): """Checks if input is a valid expression or equation diff --git a/visma/io/tokenize.py b/visma/io/tokenize.py index e30a8c3..969cd25 100644 --- a/visma/io/tokenize.py +++ b/visma/io/tokenize.py @@ -14,7 +14,7 @@ import math import copy -from visma.io.checks import isNumber, isVariable, getNumber, checkEquation, funcs, funcSyms +from visma.io.checks import isNumber, isVariable, getNumber, checkEquation, checkSyntax, funcs, funcSyms from visma.functions.structure import Function, Equation, Expression from visma.functions.constant import Constant from visma.functions.variable import Variable @@ -1561,6 +1561,8 @@ def tokenizer(eqnString): Returns: list -- function tokens list """ + if (not(checkSyntax(eqnString))[0]): + return _, tokens = constantConversion(preprocess(eqnString)) return tokens