From 126a6c85b60ad07fa29ab48315f0f195fb45d854 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Gassmann?= Date: Fri, 11 Jun 2021 01:39:49 +0200 Subject: [+] First version of the java calculator --- .../src/main/java/ch/bfh/CalculatorLexer.java | 106 ++++++++ calculator-java/src/main/java/ch/bfh/Main.java | 23 ++ calculator-java/src/main/java/ch/bfh/Token.java | 23 ++ .../java/ch/bfh/exceptions/LexerException.java | 5 + .../java/ch/bfh/exceptions/ParserException.java | 5 + .../main/java/ch/bfh/parser/ExpressionParser.java | 270 +++++++++++++++++++++ .../src/main/java/ch/bfh/parser/Parser.java | 16 ++ 7 files changed, 448 insertions(+) create mode 100644 calculator-java/src/main/java/ch/bfh/CalculatorLexer.java create mode 100644 calculator-java/src/main/java/ch/bfh/Main.java create mode 100644 calculator-java/src/main/java/ch/bfh/Token.java create mode 100644 calculator-java/src/main/java/ch/bfh/exceptions/LexerException.java create mode 100644 calculator-java/src/main/java/ch/bfh/exceptions/ParserException.java create mode 100644 calculator-java/src/main/java/ch/bfh/parser/ExpressionParser.java create mode 100644 calculator-java/src/main/java/ch/bfh/parser/Parser.java (limited to 'calculator-java/src/main/java/ch') diff --git a/calculator-java/src/main/java/ch/bfh/CalculatorLexer.java b/calculator-java/src/main/java/ch/bfh/CalculatorLexer.java new file mode 100644 index 0000000..ba10944 --- /dev/null +++ b/calculator-java/src/main/java/ch/bfh/CalculatorLexer.java @@ -0,0 +1,106 @@ +package ch.bfh; +import ch.bfh.exceptions.LexerException; + +// Lexer for classical arithmetic expressions with identifiers and assignments. +// Scans a source string char by char. + +public class CalculatorLexer { + + private String src; // source string for lexical analysis + private int idx; // current index in source + private int len; // length of source + + public CalculatorLexer() { } + + public void initLexer(String source) { + this.src = source; + idx = 0; + len = src.length(); + } + + // Consumes letters only and builds an identifier + private String identifier() { + StringBuffer s = new StringBuffer(); + + do { + s.append(src.charAt(idx)); + idx++; + } while (idx < len && Character.isLetter(src.charAt(idx))); + return s.toString(); + } + + // Consumes digits and convert integer part and decimal part + // Convert characters using the formula + // "3456.253" = [(((3+0)*10+4)*10+5)*10+6]+[0.1*2+0.01*5+0.001*3] + private double number() throws LexerException { + double v = 0; // accumulates the result + double factor = 0.1; // factor for decimal part + + do { // integer part + v = v * 10 + Character.digit(src.charAt(idx),30); + idx++; + } while (idx < len && Character.isDigit(src.charAt(idx))); + if (idx < len && src.charAt(idx) == '.') { // decimal point + idx++; + if (idx < len && Character.isDigit(src.charAt(idx))) { // decimal part + while (idx < len && Character.isDigit(src.charAt(idx))) { + v = v + (factor * Character.digit(src.charAt(idx),30)); + factor = factor * 0.1; + idx++; + } + } + else throw new LexerException("Illegal number: decimal part missing"); + } + return v; + } + + // Skips blanks, tabs, newlines + private void skip() { + char c; + while (idx < len) { + c = src.charAt(idx); + if (c==' ' || c=='\t' || c=='\n') idx++; + else break; + } + } + + // returns next token + public Token nextToken() throws LexerException { + Token tok = new Token(); + + skip(); + if (idx>=len) { + tok.str="EOL"; + tok.type=Token.EOL; + } + else + // is it a positive number? + if (Character.isDigit(src.charAt(idx))) { + tok.value = number(); + tok.type = Token.NUM; + tok.str = Double.toString(tok.value); + } + else + if (Character.isLetter(src.charAt(idx))) { + tok.value = 0; + tok.type = Token.ID; + tok.str = identifier(); + if (tok.str.compareTo("let")==0) tok.type = Token.LET; + if (tok.str.compareTo("exit")==0) tok.type = Token.END; + } + else { + switch (src.charAt(idx)) { + case '+': tok.type = Token.ADD; tok.str = "+"; break; + case '-': tok.type = Token.SUB; tok.str = "-"; break; + case '*': tok.type = Token.MUL; tok.str = "*"; break; + case '/': tok.type = Token.DIV; tok.str = "/"; break; + case '(': tok.type = Token.PAL; tok.str = "("; break; + case ')': tok.type = Token.PAR; tok.str = ")"; break; + case '=': tok.type = Token.EQU; tok.str = "="; break; + default : throw new LexerException("Illegal Token: '" + src.charAt(idx) + "'"); + } + idx++; + } + return tok; + } +} \ No newline at end of file diff --git a/calculator-java/src/main/java/ch/bfh/Main.java b/calculator-java/src/main/java/ch/bfh/Main.java new file mode 100644 index 0000000..b0dedf3 --- /dev/null +++ b/calculator-java/src/main/java/ch/bfh/Main.java @@ -0,0 +1,23 @@ +package ch.bfh; + +import ch.bfh.parser.ExpressionParser; + +import java.util.Arrays; + +public class Main { + + public static void main(String[] args){ + CalculatorLexer cl = new CalculatorLexer(); + + //TODO - command line reader instead of arg + + // Combines the arguments & init the lexer + cl.initLexer( + Arrays.stream(args).reduce("", (expression, arg) -> expression + arg) + ); + + ExpressionParser ep = new ExpressionParser(cl); + + System.out.println(ep.getValue()); + } +} diff --git a/calculator-java/src/main/java/ch/bfh/Token.java b/calculator-java/src/main/java/ch/bfh/Token.java new file mode 100644 index 0000000..93d4d02 --- /dev/null +++ b/calculator-java/src/main/java/ch/bfh/Token.java @@ -0,0 +1,23 @@ +package ch.bfh; + +// Various Tokens for arithmetic expressions based on integers +// with identifiers and assignments + +public class Token { + public int type; // token type + public double value; // numerical value for NUM + public String str; // token string + + public static final int EOL=0; // End Of Line + public static final int PAL=1; // Left Parenthesis + public static final int PAR=2; // Right Parenthesis + public static final int ADD=3; // operators + public static final int SUB=4; + public static final int MUL=5; + public static final int DIV=6; + public static final int NUM=7; // number + public static final int EQU=8; // equal + public static final int LET=9; // let + public static final int ID=10; // identifier + public static final int END=11; // exit +} diff --git a/calculator-java/src/main/java/ch/bfh/exceptions/LexerException.java b/calculator-java/src/main/java/ch/bfh/exceptions/LexerException.java new file mode 100644 index 0000000..082beee --- /dev/null +++ b/calculator-java/src/main/java/ch/bfh/exceptions/LexerException.java @@ -0,0 +1,5 @@ +package ch.bfh.exceptions; + +public class LexerException extends RuntimeException { + public LexerException(String s) { super(s); } +} \ No newline at end of file diff --git a/calculator-java/src/main/java/ch/bfh/exceptions/ParserException.java b/calculator-java/src/main/java/ch/bfh/exceptions/ParserException.java new file mode 100644 index 0000000..ca6d037 --- /dev/null +++ b/calculator-java/src/main/java/ch/bfh/exceptions/ParserException.java @@ -0,0 +1,5 @@ +package ch.bfh.exceptions; + +public class ParserException extends RuntimeException { + public ParserException(String s) { super(s); } +} \ No newline at end of file diff --git a/calculator-java/src/main/java/ch/bfh/parser/ExpressionParser.java b/calculator-java/src/main/java/ch/bfh/parser/ExpressionParser.java new file mode 100644 index 0000000..229e700 --- /dev/null +++ b/calculator-java/src/main/java/ch/bfh/parser/ExpressionParser.java @@ -0,0 +1,270 @@ +package ch.bfh.parser; + +import ch.bfh.CalculatorLexer; +import ch.bfh.Token; +import ch.bfh.exceptions.ParserException; + +import java.util.ArrayList; + +public class ExpressionParser extends Parser { + + protected ArrayList parsers = new ArrayList<>(); + protected ArrayList ops = new ArrayList<>(); + + public ExpressionParser(CalculatorLexer cl) { + this.cl = cl; + parse(); + } + + private ExpressionParser(CalculatorLexer cl, Token lastToken) { + this.cl = cl; + this.lastToken = lastToken; + parse(); + } + + @Override + protected void parse() { + Token token = cl.nextToken(); + Parser l = null; + Parser r = null; // left and right expressions + Token op = null; // operation of the current l and right expressions + + loop: + while (token != null && token.type != Token.EOL) { + switch (token.type) { + case Token.ADD: // '+' is not authorised if not op + if (l != null && op == null) { // if the '+' sign is between two term (op) + op = token; + this.ops.add(op); + } else if (l != null && r != null) { // if we already found a 'l op r' we roll + l = r; + r = null; + op = token; + this.ops.add(op); + }else if (l == null) + throw new ParserException("Factor cannot start with '+'."); + else + throw new ParserException("Invalid use of '+' was found in the Expression."); + break; + case Token.SUB: + if (l != null && op == null) { // if the '-' sign is between two term (op) + op = token; + break; + } else if (l != null && r != null) { // if we already found a 'l op r' we roll + l = r; + r = null; + op = token; + this.ops.add(op); + } // Then the '-' is the start of a new expression and not an operation + case Token.PAL: + case Token.NUM: + if (l == null) { + l = new TermParser(cl, token); + this.parsers.add(l); + token = l.lastToken; + } else { + r = new TermParser(cl, token); + this.parsers.add(r); + token = r.lastToken; + } + continue loop; + case Token.PAR: + if (lastToken.type == Token.PAL) { // if equal to '(' it means it was created by a Factor + lastToken = token; + break loop; + } else + throw new ParserException("No matching opening parenthesis were found."); + default: + //TODO - Replace default by each individual case + throw new ParserException("Malformed Expression."); + } + token = cl.nextToken(); + } + if (op != null && r == null) + throw new ParserException("Missing expression after last operation '"+op.str+"'"); + } + + @Override + public double getValue(){ + double result = 0.0; + if (!this.parsers.isEmpty()) { + result = this.parsers.get(0).getValue(); + + for (int i = 1; i < this.parsers.size(); i++) { + switch (this.ops.get(i-1).type){ + case Token.ADD: + result += this.parsers.get(i).getValue(); + break; + case Token.SUB: + result -= this.parsers.get(i).getValue(); + break; + } + } + } + return result; + } + + private class TermParser extends ExpressionParser { + + public TermParser(CalculatorLexer cl, Token lastToken) { + super(cl, lastToken); + } + + @Override + protected void parse() { + Token token = lastToken; + lastToken = null; + Parser l = null; + Parser r = null; // left and right expressions + Token op = null; // operation of the current l and right expressions + + loop: + while (token != null && token.type != Token.EOL) { + switch (token.type) { + case Token.ADD: + if (op != null) { + lastToken = token; + break loop; // going here on case 'n/+...' or 'n*+...' since '+' would be invalid + } + case Token.SUB: + case Token.NUM: + case Token.PAL: + if (l == null) { + l = new FactorParser(cl, token); + this.parsers.add(l); + token = l.lastToken; + continue loop; + } else if (op != null) { + r = new FactorParser(cl, token); + this.parsers.add(r); + token = r.lastToken; + lastToken = token; // in case we finished + continue loop; + } else { + lastToken = token; + break loop; // going here on case 'n+...' or 'n-...' since '+' and '-' are part of the parent expression + } + case Token.MUL: + case Token.DIV: + if (l != null && op == null) { // if the '*' or '/' signs are between two Factors (op) + op = token; + this.ops.add(op); + } else if (l != null && r != null) { // if we already found a 'l op r' we roll + l = r; + r = null; + op = token; + this.ops.add(op); + } else + throw new ParserException("Two '" + token.str + "' in a row were found."); + break; + case Token.PAR: // Going as high as possible --> possibly the end of an expression created by a Factor :) + lastToken = token; + break loop; + default: + //TODO - Replace default by each individual case + throw new ParserException("Malformed Expression."); + } + token = cl.nextToken(); + } + if (op != null && r == null) + throw new ParserException("Missing expression after last operation '"+op.str+"'"); + } + + @Override + public double getValue(){ + double result = 0.0; + if (!this.parsers.isEmpty()) { + result = this.parsers.get(0).getValue(); + + for (int i = 1; i < this.parsers.size(); i++) { + switch (this.ops.get(i-1).type){ + case Token.MUL: + result *= this.parsers.get(i).getValue(); + break; + case Token.DIV: + result /= this.parsers.get(i).getValue(); + break; + } + } + } + return result; + } + + private class FactorParser extends Parser { + + ExpressionParser expression; // only used when parenthesis are found + boolean isNegative = false; + boolean somethingWasParsed = false; + + public FactorParser(CalculatorLexer cl, Token lastToken) { + this.cl = cl; + this.lastToken = lastToken; + parse(); + } + + @Override + protected void parse() { + Token token = lastToken; + lastToken = null; + loop: + while (token != null && token.type != Token.EOL) { + switch (token.type) { + case Token.MUL: + case Token.DIV: + case Token.ADD: + if (somethingWasParsed) { // finished parsing the Factor + lastToken = token; // sending the token to the parent + break loop; + } else + throw new ParserException("Empty expression before '" + token.str + "'."); + case Token.SUB: + if (!somethingWasParsed) + if (!isNegative) + isNegative = true; + else + throw new ParserException("A factor cannot contain two '-'."); + else { // Meaning we could have parsed '-4' or '4' and '-' is the next digit which is op for the overall expression + lastToken = token; + break loop; + } + break; + case Token.NUM: + if (!somethingWasParsed) { + value = token.value; + somethingWasParsed = true; + } else + throw new ParserException("Missing operator between the two Factors."); + break; + case Token.PAL: + if (!somethingWasParsed) { + expression = new ExpressionParser(cl, token); + if (expression.lastToken.type != Token.PAR) + throw new ParserException("No matching closing parenthesis were found."); + somethingWasParsed = true; + } else + throw new ParserException("Missing operator between the two Factors."); + break; + case Token.PAR: // Going as high as possible --> possibly the end of an expression created by a Factor :) + if (somethingWasParsed) + lastToken = token; + else throw new ParserException("Missing expression before closing the parenthesis."); + break loop; + default: + //TODO - Replace default by each individual case + throw new ParserException("Malformed Expression."); + } + token = cl.nextToken(); + } + } + + @Override + public double getValue(){ + if (expression != null) + return expression.getValue(); + if (isNegative) + return value*(-1); + return value; + } + } + } +} diff --git a/calculator-java/src/main/java/ch/bfh/parser/Parser.java b/calculator-java/src/main/java/ch/bfh/parser/Parser.java new file mode 100644 index 0000000..755313a --- /dev/null +++ b/calculator-java/src/main/java/ch/bfh/parser/Parser.java @@ -0,0 +1,16 @@ +package ch.bfh.parser; + +import ch.bfh.CalculatorLexer; +import ch.bfh.Token; + +abstract class Parser{ + protected CalculatorLexer cl; + protected Token lastToken; + protected double value; + + protected abstract void parse(); + public double getValue(){ + return value; + } +} + -- cgit v1.2.3