diff --git a/expressions.txt b/expressions.txt index 74f9b719cba435602fbafc8e493b634bc5f8ac4f..bcba8954c44f26fe3d42dd1d3162ff0e75ad841d 100644 --- a/expressions.txt +++ b/expressions.txt @@ -1,2 +1,6 @@ -1+(x^2)-(3*x) +2*(4^2) +(4+2)*4 +(4+2*(4-2)) +((3+2)*(12+2)) +1+(x^2.22)-(3*x) 2*x+0.5*(x^2+4)-1 \ No newline at end of file diff --git a/src/main/Application.java b/src/main/Application.java index f65d712b5d0113f5e12565164d6cb5b0604925c1..466f25baab48d96e8921b81867c1023640695c29 100644 --- a/src/main/Application.java +++ b/src/main/Application.java @@ -32,17 +32,25 @@ public class Application { } //Iterate through every String - for (String expression: expressions) { - System.out.println("Expression: " + expression); + for (String expressionString: expressions) { + System.out.println("Expression: " + expressionString); //Usage of the lexer, parser & evaluator try { //Create a list of tokens with the lexer - List<Lexer.Token> tokens = Lexer.lex(expression); + List<Lexer.Token> tokens = Lexer.lex(expressionString); System.out.println("Lexer created tokens:"); //prints out each token to the console for(Lexer.Token token: tokens) { System.out.println(token.toString()); } + //Parse all tokens with the parser and saves the result into the variable 'ast' + Parser parser = new Parser(); + Parser.Expression ast = parser.parse(tokens); + //Prints the ast out with an ASTPrinter + //the result can later be used to show the ast in a function plotter + ASTPrinter printer = new ASTPrinter(); + System.out.println("Printing: '" + printer.visit(ast) + "'"); + System.out.println(); } catch (Exception e) { e.printStackTrace(); @@ -51,22 +59,10 @@ public class Application { } /* - //Iterates through every String and parses them into multiple tokens - for (String value: expressions) { - Parser parser = new Parser(); - try { - Parser.Expression exp = parser.parse(tokens); - ASTPrinter printer = new ASTPrinter(); Evaluator evaluator = new Evaluator(); - System.out.println("Printing: '" + printer.visit(exp) + "'"); String s = printer.visit(exp); System.out.println("Evaluating: '" + evaluator.visit(exp) + "'"); SwingFunctionPlotter.plotFunction(s); - } - catch (ParserException e) { - System.out.println(e.getMessage()); - } - System.out.println(); }*/ } } diff --git a/src/main/Lexer.java b/src/main/Lexer.java index d84a782eea24adfdc713830ed8c2208b991caa4a..8b8b4ba2e2f51d1b53d776abf917c7d3550d2de9 100644 --- a/src/main/Lexer.java +++ b/src/main/Lexer.java @@ -93,7 +93,9 @@ public class Lexer { index++; } } - + if(result.isEmpty()) { + throw new LexerException("SyntaxError: lexer couldn't create any tokens out of the input"); + } return result; } } diff --git a/src/main/Parser.java b/src/main/Parser.java index ca46fc9dd4cded7646645b36a3b038cddc5b9468..e29e094a41aa98f6a70b70e0867c25957a6b2124 100644 --- a/src/main/Parser.java +++ b/src/main/Parser.java @@ -7,10 +7,17 @@ import java.util.List; public class Parser { + //This abstract class is only used for heredity + //It represents an expression in the form of an Abstract Syntax Tree(AST) + //A method 'accept' is implemented, it is used for allowing other classes to interact with it + //through the visiting pattern public abstract class Expression { public abstract <T> T accept(Visitor<T> visitor); } + //This class represents a binary operation + //it contains a left expression, an operator, and a right expression + //if this operation is capsuled by brackets, the value of 'capsuled' will be true, else it will be false public class BinaryOperation extends Expression { Expression leftExpression; String operator; @@ -31,6 +38,8 @@ public class Parser { } } + //This class represents a variable, with it holds a String 'variableName' that contains a character + //of the alphabet public class Variable extends Expression { String variableName; public Variable(String i, Boolean capsuled) { @@ -41,10 +50,13 @@ public class Parser { } } + //This abstract class is only used for heredity + //A value can either be a number or a decimal private abstract class Value extends Expression { } + //This class represents a number, it contains a String 'digits' that holds it value public class Number extends Value { public String digits; public Number(String i) { @@ -55,6 +67,7 @@ public class Parser { } } + //This class represents a decimal, it contains two numbers: the one before- and the one after the dot public class Decimal extends Value { public Number beforeDot; public Number afterDot; @@ -69,7 +82,7 @@ public class Parser { } //starting method of the parser - //parses a list of tokens + //parses a list of tokens and returns the AST (Abstract Syntax Tree) public Expression parse(List<Lexer.Token> list) throws ParserException { if(list.isEmpty()) { throw new ParserException("empty token list"); @@ -103,102 +116,158 @@ public class Parser { return ast; } - //checks if a String only contains an allowed operator with parseCharacter(...) - private boolean parseOperator(String operator) throws ParserException { - if(operator.length()>1) { - throw new ParserException("SyntaxError: invalid length for an operator: " + operator); + //called by parseExpression + //checks if there are multiple tokens left, which indicates that they build up a BinaryOperation + //It splits up the tokens into a left and right expression and with a recursive call of parseExpression + //each part will be determined, until all BinaryOperations consist of finite left and right expressions + //Then a new BinaryOperation will be returned, which saves the left- & right expression and the operator + //between them + private BinaryOperation parseBinaryOperation(List<Lexer.Token> ts) throws ParserException { + if(ts.isEmpty()) { + throw new ParserException("SyntaxError: empty token list"); } - if(!parseCharacter(operator, "+-*/^")) { - throw new ParserException("SyntaxError: invalid operator character: " + operator); + if(ts.size()==1) { + return null; + } + //Index will be determined in the next if/else statement + //It will indicate the position of the operator that separates the left- and right expression + int index; + int tokensInBracket = parseBrackets(ts); + boolean capsuled; + //If the result of parseBrackets(ts)+1 is the same as ts.size the current BinaryOperation is capsuled + if(tokensInBracket+1==ts.size()) { + //removes the tokens with the capsuling brackets + ts.remove(tokensInBracket); + ts.remove(0); + capsuled = true; + //Checks again for brackets + tokensInBracket = parseBrackets(ts); + //If no bracket is following, the operator needs to be at position 1 of ts + if(tokensInBracket==-1) { + index = 1; + } + else { + //If brackets are following, the operator is at position tokensInBracket+1 + index = tokensInBracket+1; + } + } + //If tokensInBracket is -1 this expression is not capsuled and the operator needs to be at position 1 of ts + else if (tokensInBracket==-1) { + capsuled = false; + index = 1; + } + //If both other cases were not fulfilled an inner expression is capsuled + //Then this BinaryOperation is not capsuled, no brackets need to be removed and the operator is at + //position tokensInBracket+1 + else { + capsuled = false; + index = tokensInBracket+1; + } + //Checks if the operator of this BinaryOperation is permitted + parseOperator(ts.get(index).getData()); + List<Lexer.Token> leftList = new LinkedList<>(); + //Splits the token list ts into a left and right expression + for(int iterator = 0; iterator<index; iterator++) { + leftList.add(ts.remove(0)); } - return true; + Expression leftExpression = parseExpression(leftList); + //Operator is now at position 0 + String operator = ts.remove(0).getData(); + Expression rightExpression = parseExpression(ts); + return new BinaryOperation(leftExpression, operator, rightExpression, capsuled); } + + //called by parseBinaryOperator + //checks if the first token is a bracket + //If yes: checks if the bracket gets closed correctly and returns the index of the closing bracket + //If no: returns -1 private int parseBrackets(List<Lexer.Token> ts) throws ParserException{ + if(ts.isEmpty()) { + throw new ParserException("SyntaxError: empty token list"); + } if(parseCharacter(ts.get(0).getData(), "(")) { int index; int lBrackets = 0; int rBrackets = 0; boolean found = false; + //Iterates through every token, until the closing bracket got found for(index=1; index<ts.size(); index++) { if(parseCharacter(ts.get(index).getData(), "(")) { lBrackets += 1; } else if(parseCharacter(ts.get(index).getData(), ")")) { rBrackets += 1; + //If more ')' got found then '(' the bracket got closed correctly and the loop can be interrupted if(rBrackets>lBrackets) { found = true; break; } } } + //If no matching closing bracket was found a ParserException will be thrown if(!found) { throw new ParserException("SyntaxError: brackets never got closed"); } + //If the closing bracket was placed immediately after the opening bracket a ParserException will be thrown if(index==1) { throw new ParserException("SyntaxError: no value inside brackets"); } + //Returns the index of the closing bracket return index; } return -1; } - private BinaryOperation parseBinaryOperation(List<Lexer.Token> ts) throws ParserException { - if(ts.isEmpty()) { - throw new ParserException("SyntaxError: empty token list"); - } - if(ts.size()==1) { - return null; - } - int index = 0; - int tokensInBracket = parseBrackets(ts); - boolean capsuled; - if(tokensInBracket+1==ts.size()) { - ts.remove(tokensInBracket); - ts.remove(0); - capsuled = true; - index = 1; - } - else if (tokensInBracket==-1) { - capsuled = false; - parseOperator(ts.get(1).getData()); - index = 1; + + //called by parseBinaryOperation + //checks if a String only contains an allowed operator with parseCharacter + //if not: throws a ParserException + private void parseOperator(String operator) throws ParserException { + if(operator.isEmpty()) { + throw new ParserException("RuntimeError: empty String instead of an operator (+, -, *, /, ^"); } - else { - capsuled = false; - index = tokensInBracket+1; + if(operator.length()>1) { + throw new ParserException("SyntaxError: invalid length for an operator: " + operator); } - List<Lexer.Token> leftList = new LinkedList<>(); - for(int iterator = 0; iterator<index; iterator++) { - leftList.add(ts.remove(0)); + if(!parseCharacter(operator, "+-*/^")) { + throw new ParserException("SyntaxError: invalid operator character: " + operator); } - Expression leftExpression = parseExpression(leftList); - String operator = ts.remove(0).getData(); - Expression rightExpression = parseExpression(ts); - return new BinaryOperation(leftExpression, operator, rightExpression, capsuled); } + //called by parseExpression + //Checks if a token is a variable: + //Yes: returns a new Variable that gets created with the received token + //No: returns null private Variable parseVariable(List<Lexer.Token> ts) throws ParserException { if (ts.isEmpty()) { throw new ParserException("SyntaxError: empty token list"); } + //If the TokenType is != VARIABLE the token is not a variable and null gets returned if (ts.get(0).getType() != Lexer.TokenType.VARIABLE) { return null; } + if (ts.get(0).getData().length()>1) { + throw new ParserException("Invalid size for a variable!"); + } String data = ts.remove(0).getData(); + //If the token is empty, throw a ParserException if (data.isEmpty()) { throw new ParserException("SyntaxError: empty token"); } - + //Checks if the variable is a character of the alphabet, if not the token is not valid if(!parseCharacter(data,"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz")) { throw new ParserException("Invalid variable character: " + data); } return new Variable(data, false); } - //called by method parseException(...) - //parses a token/list of tokens into a value + + //called by method parseException + //Checks if a token is a value: + //Yes: returns a new Number or Decimal that gets created with the received token + //No: returns null private Value parseValue(List<Lexer.Token> ts) throws ParserException { if (ts.isEmpty()) { throw new ParserException("SyntaxError: empty token list"); @@ -217,12 +286,13 @@ public class Parser { return parseNumber(data, false); } - //called by method parseValue(...) - //parses a decimal of a list of tokens & a string + //called by method parseValue + //parses a decimal of a string private Decimal parseDecimal(String data) throws ParserException { if(data.isEmpty()) { throw new ParserException("SyntaxError: empty data"); } + //Separates the String into the number before- and after the dot int dot = data.indexOf('.'); String number1 = data.substring(0,dot); String number2 = data.substring(dot+1); @@ -231,30 +301,41 @@ public class Parser { return new Decimal(beforeDot, afterDot); } - //called by method parseValue(...) + //called by method parseValue or parseDecimal //parses a String into a number + //the parameter afterDot will be set on 'true' if the number is used after a dot of a decimal private Number parseNumber(String data, boolean afterDot) throws ParserException{ if (data.isEmpty()) { throw new ParserException("RuntimeException: empty token"); } - + //if the number only contains the digit 0, it can be returned already if (data.startsWith("0") && data.length() == 1) { return new Number(data); } - if(afterDot) { + //Checks if all digits of the number are permitted + //If afterDot is false, the first digit is not allowed to be 0 + if(!afterDot) { parseDigitWithoutZero(data.substring(0,1)); - parseDigit(data.substring(1)); + + if (data.length()>1) { + parseDigit(data.substring(1)); + } } + //Checks if all digits of the number are permitted else { parseDigit(data); } return new Number(data); } - //called by method parseNumber(...) - //checks if a String only contains numbers(including zero) with parseCharacter(...) + //called by method parseNumber + //checks if a String only contains numbers(including zero) with parseCharacter + //if not, a ParserException is thrown private void parseDigit(String data) throws ParserException { for(int index=0; index<data.length(); index++) { + if(data.isEmpty()) { + throw new ParserException("RuntimeError: empty String instead of a digit(0-9)"); + } String character = Character.toString(data.charAt(index)); if(!parseCharacter(character, "0123456789")) { throw new ParserException("SyntaxError: unexpected character: " + character); @@ -262,9 +343,13 @@ public class Parser { } } - //called by method parseNumber(...) - //checks if a String only contains numbers(excluding zero) with parseCharacter(...) + //called by method parseNumber + //checks if a String only contains numbers(excluding zero) with parseCharacter + //if not, a ParserException is thrown private void parseDigitWithoutZero(String data) throws ParserException { + if(data.isEmpty()) { + throw new ParserException("RuntimeError: empty String instead of a digit(1-9)"); + } for(int index=0; index<data.length(); index++) { String character = Character.toString(data.charAt(index)); if(!parseCharacter(character, "123456789")) { @@ -273,8 +358,8 @@ public class Parser { } } - //called by methods parseOperator(...), parseDigit(...), parseDigitWithoutZero(...) - //checks if a certain string can be found in a string of allowed character + //called by multiple methods e.g. parserOperator, parseDigit + //checks if a certain string can be found in a String of allowed characters and returns the result private boolean parseCharacter(String data, String allowedCharacters) throws ParserException { if(data.isEmpty()) { throw new ParserException("RuntimeError: empty String"); diff --git a/src/test/ParserTest.java b/src/test/ParserTest.java index 0be7f64628e6179efc67d8d19221043802094627..2d70d2edda5239923794264e4d64b10ed8ad0982 100644 --- a/src/test/ParserTest.java +++ b/src/test/ParserTest.java @@ -1,4 +1,100 @@ package test; +import main.Lexer; +import main.Parser; +import org.junit.Test; + +import java.util.LinkedList; +import java.util.List; + public class ParserTest { + + @Test + void testParseVariable() { + List<Lexer.Token> tokens = new LinkedList<>(); + tokens.add(new Lexer.Token(Lexer.TokenType.VARIABLE,"x")); + var parser = new Parser(); + try { + var ast = parser.parse(tokens); + } + catch(Exception e) { + e.printStackTrace(); + } + } + + @Test + void testWrongVariableInput() { + List<Lexer.Token> tokens = new LinkedList<>(); + tokens.add(new Lexer.Token(Lexer.TokenType.VARIABLE,"xy")); + var parser = new Parser(); + try { + var ast = parser.parse(tokens); + } + catch(Exception e) { + e.printStackTrace(); + } + } + + @Test + void testParseNumber() { + List<Lexer.Token> tokens = new LinkedList<>(); + tokens.add(new Lexer.Token(Lexer.TokenType.NUMBER,"23")); + var parser = new Parser(); + try { + var ast = parser.parse(tokens); + } + catch(Exception e) { + e.printStackTrace(); + } + } + + @Test + void testWrongNumberInput() { + List<Lexer.Token> tokens = new LinkedList<>(); + tokens.add(new Lexer.Token(Lexer.TokenType.NUMBER,"02.4")); + var parser = new Parser(); + try { + var ast = parser.parse(tokens); + } + catch(Exception e) { + e.printStackTrace(); + } + } + + @Test + void testParseBinaryOperation() { + List<Lexer.Token> tokens = new LinkedList<>(); + tokens.add(new Lexer.Token(Lexer.TokenType.SPECIAL,"(")); + tokens.add(new Lexer.Token(Lexer.TokenType.NUMBER,"5")); + tokens.add(new Lexer.Token(Lexer.TokenType.SPECIAL,"*")); + tokens.add(new Lexer.Token(Lexer.TokenType.NUMBER,"2")); + tokens.add(new Lexer.Token(Lexer.TokenType.SPECIAL,")")); + tokens.add(new Lexer.Token(Lexer.TokenType.NUMBER,"*")); + tokens.add(new Lexer.Token(Lexer.TokenType.NUMBER,"3")); + var parser = new Parser(); + try { + var ast = parser.parse(tokens); + } + catch(Exception e) { + e.printStackTrace(); + } + } + + @Test + void testParseWrongBinaryOperation() { + List<Lexer.Token> tokens = new LinkedList<>(); + tokens.add(new Lexer.Token(Lexer.TokenType.SPECIAL,"(")); + tokens.add(new Lexer.Token(Lexer.TokenType.NUMBER,"5")); + tokens.add(new Lexer.Token(Lexer.TokenType.SPECIAL,"*")); + tokens.add(new Lexer.Token(Lexer.TokenType.NUMBER,"2")); + tokens.add(new Lexer.Token(Lexer.TokenType.SPECIAL,")")); + tokens.add(new Lexer.Token(Lexer.TokenType.NUMBER,"3")); + var parser = new Parser(); + try { + var ast = parser.parse(tokens); + } + catch(Exception e) { + e.printStackTrace(); + } + } }