diff --git a/benchmarks/pom.xml b/benchmarks/pom.xml index 1547906..657f85a 100644 --- a/benchmarks/pom.xml +++ b/benchmarks/pom.xml @@ -18,12 +18,12 @@ org.openjdk.jmh jmh-core - 1.35 + 1.36 org.openjdk.jmh jmh-generator-annprocess - 1.35 + 1.36 \ No newline at end of file diff --git a/core/pom.xml b/core/pom.xml index 589b46b..b70157d 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -13,8 +13,17 @@ org.junit.jupiter junit-jupiter - 5.9.0 + 5.9.3 test + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.1.2 + + + \ No newline at end of file diff --git a/core/src/main/java/me/fourteendoggo/mathexpressionparser/ExpressionParser.java b/core/src/main/java/me/fourteendoggo/mathexpressionparser/ExpressionParser.java index f621e9e..fa54b0f 100644 --- a/core/src/main/java/me/fourteendoggo/mathexpressionparser/ExpressionParser.java +++ b/core/src/main/java/me/fourteendoggo/mathexpressionparser/ExpressionParser.java @@ -1,11 +1,11 @@ package me.fourteendoggo.mathexpressionparser; -import me.fourteendoggo.mathexpressionparser.container.TokenList; import me.fourteendoggo.mathexpressionparser.exceptions.SyntaxException; import me.fourteendoggo.mathexpressionparser.function.FunctionCallSite; import me.fourteendoggo.mathexpressionparser.function.FunctionContext; import me.fourteendoggo.mathexpressionparser.utils.Assert; +import java.util.Objects; import java.util.function.DoubleBinaryOperator; import java.util.function.DoubleUnaryOperator; import java.util.function.ToDoubleFunction; @@ -22,11 +22,11 @@ private ExpressionParser() {} * @throws SyntaxException if the given expression is invalid or empty */ public static double parse(String input) { - Assert.notNull(input, "input was null"); + Objects.requireNonNull(input, "input was null"); Assert.isFalse(input.isEmpty(), "input was empty"); - TokenList tokens = new Tokenizer(input.toCharArray()).readTokens(); - return tokens.solve(); + Tokenizer tokenizer = new Tokenizer(input.toCharArray()); + return tokenizer.readTokens().solve(); } /** diff --git a/core/src/main/java/me/fourteendoggo/mathexpressionparser/Tokenizer.java b/core/src/main/java/me/fourteendoggo/mathexpressionparser/Tokenizer.java index 669ea61..7fba2a3 100644 --- a/core/src/main/java/me/fourteendoggo/mathexpressionparser/Tokenizer.java +++ b/core/src/main/java/me/fourteendoggo/mathexpressionparser/Tokenizer.java @@ -7,7 +7,6 @@ import me.fourteendoggo.mathexpressionparser.function.FunctionContext; import me.fourteendoggo.mathexpressionparser.tokens.Operand; import me.fourteendoggo.mathexpressionparser.tokens.Operator; -import me.fourteendoggo.mathexpressionparser.tokens.Token; import me.fourteendoggo.mathexpressionparser.tokens.TokenType; import me.fourteendoggo.mathexpressionparser.utils.Assert; import me.fourteendoggo.mathexpressionparser.utils.Utility; @@ -15,245 +14,268 @@ import java.util.function.IntPredicate; public class Tokenizer { - private static final char NULL_CHAR = '\0'; - private static final FunctionContainer FUNCTION_CONTAINER = FunctionContainer.withDefaultFunctions(); + private static final FunctionContainer functionContainer = FunctionContainer.withDefaultFunctions(); private final char[] source; - private final IntPredicate condition; private final TokenList tokens; + private final IntPredicate loopCondition; private int pos; public Tokenizer(char[] source) { this(source, current -> true); } - public Tokenizer(char[] source, IntPredicate condition) { + public Tokenizer(char[] source, IntPredicate loopCondition) { this.source = source; - this.condition = condition; + this.loopCondition = loopCondition; this.tokens = new TokenList(); } - static FunctionContainer getFunctionContainer() { - return FUNCTION_CONTAINER; - } - - private char current() { - return source[pos]; - } - - private void advance() { - pos++; - } - - private char peek() { - if (pos >= source.length) return NULL_CHAR; - return source[pos]; - } - - private void position(int pos) { - this.pos = pos; - } - - private boolean hasAndGet(char expected) { - if (peek() != expected) { - return false; - } - advance(); - return true; - } - - private void expectAndGet(char expected, String exceptionMessage) { - Assert.isTrue(hasAndGet(expected), exceptionMessage); - } - - private Tokenizer branchOff(IntPredicate newCondition, int newPos) { - Tokenizer res = new Tokenizer(source, newCondition); - res.position(newPos); - return res; + public static FunctionContainer getFunctionContainer() { + return functionContainer; } public TokenList readTokens() { - while (pos < source.length && condition.test(source[pos])) { - char current = source[pos]; + while (hasRemaining()) { + char current = advance(); switch (current) { - case ' ', '\r', '\t' -> advance(); - case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' -> pushOperand(current, false); - case '^' -> pushToken(Operator.POWER); - case '*' -> pushToken(Operator.MULTIPLICATION); - case '/' -> pushToken(Operator.DIVISION); - case '%' -> pushToken(Operator.MODULO); - case '+' -> pushToken(Operator.ADDITION); + case ' ', '\r', '\t' -> {} // no-op + case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' -> pushOperand(current); + case '*' -> tokens.pushToken(Operator.MULTIPLICATION); + case '/' -> tokens.pushToken(Operator.DIVISION); + case '+' -> tokens.pushToken(Operator.ADDITION); + case '%' -> tokens.pushToken(Operator.MODULO); + case '^' -> tokens.pushToken(Operator.BITWISE_XOR); case '-' -> { switch (tokens.getLastType()) { - case OPERAND -> pushToken(Operator.SUBTRACTION); - case OPERATOR -> pushOperand(NULL_CHAR, true); - default -> throw new SyntaxException("invalid position for a negative sign"); + case OPERAND -> tokens.pushToken(Operator.SUBTRACTION); + case OPERATOR -> pushNegativeOperand(); } } - case '(' -> { - // replace things like 2(3+4) with 2*(3+4) - pushMultiplicationIfNeeded(); - pushToken(readBrackets()); + case '<' -> { + switch (advanceOrThrow()) { + case '<' -> tokens.pushToken(Operator.LEFT_SHIFT); + case '=' -> tokens.pushToken(Operator.LESS_THEN_OR_EQUAL); + default -> { + tokens.pushToken(Operator.LESS_THEN); + pos--; // put the character after < back + } + } + } + case '>' -> { + switch (advanceOrThrow()) { + case '>' -> tokens.pushToken(Operator.RIGHT_SHIFT); + case '=' -> tokens.pushToken(Operator.GREATER_THEN_OR_EQUAL); + default -> { + tokens.pushToken(Operator.GREATER_THEN); + pos--; // put the character after > back + } + } + } + case '=' -> { + matchOrThrow('=', "expected another '=' for comparison"); + tokens.pushToken(Operator.EQUALS); } case 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z' -> { - // replace things like 2cos(3) with 2*cos(3) - pushMultiplicationIfNeeded(); + // support for things like 2cos(1) -> 2 * cos(1) + if (tokens.getLastType() == TokenType.OPERAND) { + tokens.pushToken(Operator.MULTIPLICATION); + } tokens.pushToken(readFunctionCall()); } - default -> throw new SyntaxException("unexpected character: " + current); + case '&' -> { + if (match('&')) { // already standing on the second '&' then + tokens.pushToken(Operator.LOGICAL_AND); + } else { + tokens.pushToken(Operator.BITWISE_AND); + } + } + case '|' -> { + if (match('|')) { + tokens.pushToken(Operator.LOGICAL_OR); + } else { + tokens.pushToken(Operator.BITWISE_OR); + } + } + case '(' -> { + // support for things like 2(1 + 1) -> 2 * (1 + 1) + if (tokens.getLastType() == TokenType.OPERAND) { + tokens.pushToken(Operator.MULTIPLICATION); + } + tokens.pushToken(readBrackets()); + } + case '!' -> { + if (currentOrThrow("expected an operand") == '=') { + advance(); + tokens.pushToken(Operator.NOT_EQUALS); + } else { // one of the highest priority operators, can be solved immediately + Tokenizer tokenizer = branchOff(loopCondition, pos); + double toBeNegated = tokenizer.readTokens().solve(); + pos = tokenizer.pos; + tokens.pushToken(new Operand(Utility.boolNot(toBeNegated))); + } + } + case '~' -> { // one of the highest priority operators, can be solved immediately + Tokenizer tokenizer = branchOff(loopCondition, pos); + int input = Utility.requireInt(tokenizer.readTokens().solve()); + pos = tokenizer.pos; + tokens.pushToken(new Operand(~input)); + } + default -> throw new SyntaxException("unexpected character " + current); } } + System.out.println(tokens); return tokens; } - private void pushOperand(char firstDigit, boolean negative) { - advance(); // we already read the first digit - tokens.pushToken(readOperand(firstDigit, negative)); + private boolean hasRemaining() { + return pos < source.length && loopCondition.test(source[pos]); } - private void pushToken(Token token) { - advance(); // assuming token is only one char - tokens.pushToken(token); + private char advance() { + return source[pos++]; } - private void pushMultiplicationIfNeeded() { - if (tokens.getLastType() == TokenType.OPERAND) { - tokens.pushToken(Operator.MULTIPLICATION); - } + private char advanceOrThrow() { + Assert.isTrue(hasRemaining(), "expected an operand"); + return advance(); } - private Operand readBrackets() { - Tokenizer tokenizer = branchOff(Utility::isBetweenBrackets, pos + 1); // enter expression - Operand result = new Operand(tokenizer.readTokens().solve()); - pos = tokenizer.pos; - Assert.isTrue(peek() == ')', "missing closing parenthesis"); - return result; + private char currentOrThrow(String message) { + Assert.isTrue(pos < source.length, message); + return source[pos]; } - private Operand readFunctionCall() { - FunctionCallSite function = FUNCTION_CONTAINER.getFunction(source, pos); - pos += function.getName().length(); // skip function name - expectAndGet('(', "missing opening parenthesis for function call"); - - FunctionContext parameters = function.allocateParameters(); - // only read parameters if the function supports it or if there are optional parameters filled in - // if current points to anything other than a closing parenthesis, we know we got a parameter - if (peek() != ')') { - Assert.isTrue(function.supportsArgs(), "did not expect any parameters for function %s", function.getName()); - Tokenizer paramTokenizer = new Tokenizer(source, Utility::isValidArgument); - paramTokenizer.position(pos); // position them to read the first parameter - - parameters.add(paramTokenizer.readTokens().solve()); - while (paramTokenizer.peek() == ',') { // extra param coming - int oldPos = paramTokenizer.pos; - paramTokenizer = paramTokenizer.branchOff(Utility::isValidArgument, oldPos + 1); - parameters.add(paramTokenizer.readTokens().solve()); - } - pos = paramTokenizer.pos; + private char currentOrDefault() { + if (pos < source.length) { + return source[pos]; } - expectAndGet(')', "missing closing parenthesis"); + return '\0'; + } - return new Operand(function.apply(parameters)); + private boolean match(char c) { + if (pos >= source.length || source[pos] != c) { + return false; + } + pos++; + return true; } - private Operand readOperand(char firstDigit, boolean negative) { - return new Operand(readDouble(firstDigit, negative)); + private void matchOrThrow(char expected, String message) { + Assert.isTrue(match(expected), message); } - private double readDouble(char firstDigit, boolean negative) { - IntResult beforeComma = readInt(firstDigit, negative); - if (!hasAndGet('.')) { // skips decimal point if present - beforeComma.alignState(); - return beforeComma.getValue(); - } - double decimalPart = readDecimalPart(); - beforeComma.alignState(); + private Tokenizer branchOff(IntPredicate newLoopCondition, int newPos) { + Tokenizer tokenizer = new Tokenizer(source, newLoopCondition); + tokenizer.pos = newPos; + return tokenizer; + } - if (beforeComma.negative) { - return beforeComma.getValue() - decimalPart; - } - return beforeComma.getValue() + decimalPart; + private void pushNegativeOperand() { + double value = -readDouble('0', false); + tokens.pushToken(new Operand(value)); + } + + private void pushOperand(char initialChar) { + double value = readDouble(initialChar); + tokens.pushToken(new Operand(value)); } - private IntResult readInt(char firstDigit, boolean negative) { - IntResult res = new IntResult(negative); - if (firstDigit != NULL_CHAR) { - res.push(firstDigit); - } + private double readDouble(char initialChar) { + return readDouble(initialChar, true); + } - loop: while (pos < source.length) { - char current = current(); + /** + * Reads a double starting at the current pos + * @param initialChar the first char of the number, or '0' if this was a negative sign + * @param readNumber whether the initialChar was actually part of the number, false if it accounts for a negative sign + * @return the read double + * @throws SyntaxException if the buffer contains some malformed double + */ + private double readDouble(char initialChar, boolean readNumber) { + double result = initialChar - '0'; + + loop: + while (pos < source.length) { + char current = advance(); switch (current) { - case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' -> res.push(current); - case '-' -> { - if (!res.empty) break loop; - res.setNegative(); + case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' -> { + result *= 10; + result += current - '0'; + readNumber = true; + } + case '.' -> { + Assert.isTrue(readNumber, "expected a number before the comma"); + double decimalPart = readDecimalPart(); + result += decimalPart; + return result; + } + default -> { + pos--; // read too far then + break loop; } - default -> { break loop; } } - pos++; // consume character } - return res; + if (!readNumber) { + // support for function calls of form -func() + Assert.isTrue(Utility.isLowercaseLetter(currentOrDefault()), "expected a number"); + return 1; // pushNegativeOperand() inverts this, so we're actually returning -1 + } + return result; } private double readDecimalPart() { int oldPos = pos; double result = 0; - long divider = 10; + double divider = 10; // always power of ten - loop: while (pos < source.length) { - char current = current(); + loop: + while (pos < source.length) { + char current = source[pos]; switch (current) { case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' -> { - int value = current - '0'; - result += (double) value / divider; + result += (current - '0') / divider; divider *= 10; - pos++; // consume character + pos++; } default -> { break loop; } } } - Assert.isTrue(pos > oldPos, "could not read the decimal part of a number"); + Assert.isTrue(pos > oldPos, "expected the decimal part of a number"); return result; } - private class IntResult { - private int value; - private boolean negative; // if we have a negative sign before the number - private boolean empty; // an absence of a number, meaning we haven't read any numbers + private Operand readBrackets() { + Tokenizer tokenizer = branchOff(Utility::isBetweenBrackets, pos); // enter expression + Operand result = new Operand(tokenizer.readTokens().solve()); - public IntResult(boolean negative) { - this.negative = negative; - this.empty = true; - } + Assert.isTrue(tokenizer.currentOrDefault() == ')', "missing closing parenthesis"); + pos = tokenizer.pos + 1; + return result; + } - public void setNegative() { - Assert.isFalse(negative, "cannot have two negative signs in a row"); - negative = true; - } + private Operand readFunctionCall() { + FunctionCallSite function = functionContainer.getFunction(source, pos - 1); // already incremented pos + pos += function.getName().length() - 1; + matchOrThrow('(', "missing opening parenthesis for function call"); - public void push(char digit) { - value = value * 10 + (digit - '0'); - empty = false; - } + FunctionContext parameters = function.allocateParameters(); + char maybeClosingParenthesis = currentOrThrow("missing closing parenthesis for function call " + function.getName()); + if (maybeClosingParenthesis != ')') { // arguments were provided + Assert.isTrue(function.supportsArgs(), "did not expect any parameters for function %s", function.getName()); - // support for -func() syntax - public void alignState() { - if (!empty) return; - Assert.isTrue(Utility.isLowercaseLetter(peek()), "expected a number"); - // if there's a function after the - sign, change our value to -1 to do -1 * func() - // negative is true, otherwise we wouldn't be here, so actual value is -1 - value = 1; - } + // do while doesn't really work here due to that + 1 + Tokenizer paramTokenizer = branchOff(Utility::isValidArgument, pos); + parameters.add(paramTokenizer.readTokens().solve()); - /* - the tokenizer checks this flag because of the -0.xxx case - remember this class is only used for reading a double, and we don't store the decimal part ourselves - so the tokenizer might wrongly assume that we store a positive int value because -0 == 0 - so -0.123 will be read as 0.123 if we didn't keep track of this flag - */ - public int getValue() { - return negative ? -value : value; + while (paramTokenizer.currentOrDefault() == ',') { + paramTokenizer = paramTokenizer.branchOff(Utility::isValidArgument, paramTokenizer.pos + 1); // + 1 to consume comma + parameters.add(paramTokenizer.readTokens().solve()); + } + pos = paramTokenizer.pos; // move to ')' (or something else if there's an error) } + matchOrThrow(')', "missing closing parenthesis for function call " + function.getName()); + + return new Operand(function.apply(parameters)); } } diff --git a/core/src/main/java/me/fourteendoggo/mathexpressionparser/container/CharTree.java b/core/src/main/java/me/fourteendoggo/mathexpressionparser/container/CharTree.java index 9241c34..bddc42e 100644 --- a/core/src/main/java/me/fourteendoggo/mathexpressionparser/container/CharTree.java +++ b/core/src/main/java/me/fourteendoggo/mathexpressionparser/container/CharTree.java @@ -43,26 +43,25 @@ public void insert(char[] word, T instance) { crawl = crawl.putChildIfAbsent(current, () -> new Node(current, inputValidator)); } char lastChar = word[word.length - 1]; - Node lastNode = crawl.putChild(lastChar, () -> new ValueHoldingNode<>(lastChar, inputValidator, instance)); + Node lastNode = crawl.putChild(lastChar, new ValueHoldingNode<>(lastChar, inputValidator, instance)); Assert.isFalse(lastNode instanceof ValueHoldingNode, "instance was already inserted"); } /** * Searches for a word in the given buffer, starting at the given position.
* The searching will stop when the end of the buffer is reached or the input validator rejects the current character.
- * @param buffer the buffer to search in + * @param buf the buffer to search in * @param pos the starting index * @return the value found or null if no value was found * @throws SyntaxException if a valid character is not present as a path in this tree */ @SuppressWarnings("unchecked") - public T search(char[] buffer, int pos) { + public T search(char[] buf, int pos) { Node crawl = root; char current; - while (pos < buffer.length && inputValidator.test(current = buffer[pos++])) { - Node child = crawl.children[current - 'a']; - Assert.notNull(child, "could not find node for character " + current); - crawl = child; + while (pos < buf.length && inputValidator.test(current = buf[pos++])) { + crawl = crawl.children[current - 'a']; + if (crawl == null) return null; } if (crawl instanceof ValueHoldingNode valueNode) { return (T) valueNode.heldValue; @@ -94,10 +93,10 @@ public Node putChildIfAbsent(char value, Supplier nodeSupplier) { return children[index]; } - public Node putChild(char value, Supplier nodeSupplier) { + public Node putChild(char value, Node node) { int index = indexOrThrow(value); Node oldValue = children[index]; - children[index] = nodeSupplier.get(); + children[index] = node; return oldValue; } diff --git a/core/src/main/java/me/fourteendoggo/mathexpressionparser/container/ConstantPool.java b/core/src/main/java/me/fourteendoggo/mathexpressionparser/container/ConstantPool.java deleted file mode 100644 index 3516d01..0000000 --- a/core/src/main/java/me/fourteendoggo/mathexpressionparser/container/ConstantPool.java +++ /dev/null @@ -1,5 +0,0 @@ -package me.fourteendoggo.mathexpressionparser.container; - -// TODO: wraps current FunctionContainer and adds constants and variables -public class ConstantPool { -} diff --git a/core/src/main/java/me/fourteendoggo/mathexpressionparser/container/TokenList.java b/core/src/main/java/me/fourteendoggo/mathexpressionparser/container/TokenList.java index c2c249b..f80e442 100644 --- a/core/src/main/java/me/fourteendoggo/mathexpressionparser/container/TokenList.java +++ b/core/src/main/java/me/fourteendoggo/mathexpressionparser/container/TokenList.java @@ -9,7 +9,7 @@ public class TokenList { private LinkedCalculation head, tail; - private TokenType lastType = TokenType.OPERATOR; + private TokenType lastType = TokenType.OPERATOR; // need to assure incoming type is different from the current one private int numCalculations; public void pushToken(Token token) { @@ -144,15 +144,13 @@ public String toString() { } StringBuilder builder = new StringBuilder(); - builder.append('['); - for (LinkedCalculation node = head; node != null; node = node.next) { if (builder.length() > 1) { builder.append(" -> "); } builder.append(node); } - return builder.append(']').toString(); + return builder.toString(); } /** @@ -188,6 +186,12 @@ public boolean mayExecuteFirst() { return operator.getPriority() >= next.operator.getPriority(); } + /** + * Pushes a token to this calculation object. + * The caller should check {@link #isComplete()} before to ensure they don't overwrite the same fields again, + * as this just sets fields and doesn't check anything. + * @param token the token to push, either an operand or an operator + */ public void pushToken(Token token) { switch (token.getType()) { case OPERAND -> right = (Operand) token; @@ -195,16 +199,25 @@ public void pushToken(Token token) { } } + /** + * @return true if this calculation is complete and can be solved + */ public boolean isComplete() { return operator != null && right != null; // left will never be null so no need to check } + /** + * Tries to simplify this calculation, effectively checking if its operator priority is {@link Operator#HIGHEST_PRIORITY}.
+ * Then solving the calculation and marking it as "incomplete" again to allow further adding of tokens. + * @return true, if this calculation can be simplified, false otherwise + */ public boolean simplify() { if (operator.getPriority() != Operator.HIGHEST_PRIORITY) { return false; } double result = solve(); left.setValue(result); + operator = null; right = null; return true; } @@ -214,7 +227,14 @@ public double solve() { } // some support to work with incomplete calculations f.e. [3,null,null] (3) + /** + * Either solves this calculation if complete, or returns the left operand if that's the only thing set, + * throws otherwise + * @return the first operands value, or the solved expressions value + * @throws SyntaxException if we hold both a left operand and an operator (how even would we solve that?) + */ public double tryToSolve() { + // illegal state of having a left operand and right operand but no operator should never occur if (operator == null) { return left.getValue(); } @@ -225,11 +245,11 @@ public double tryToSolve() { @Override public String toString() { if (operator == null) { // right also null - return "{" + left + "}"; + return "[" + left + "]"; } else if (right == null) { // only right null - return "{" + left + ", " + operator.getSymbol() + "}"; + return "[" + left + ", " + operator.getSymbol() + "]"; } - return "{" + left + ", " + operator + ", " + right + "}"; + return "[" + left + ", " + operator + ", " + right + "]"; } } } diff --git a/core/src/main/java/me/fourteendoggo/mathexpressionparser/function/FunctionCallSite.java b/core/src/main/java/me/fourteendoggo/mathexpressionparser/function/FunctionCallSite.java index dbe6db7..6f2f085 100644 --- a/core/src/main/java/me/fourteendoggo/mathexpressionparser/function/FunctionCallSite.java +++ b/core/src/main/java/me/fourteendoggo/mathexpressionparser/function/FunctionCallSite.java @@ -1,5 +1,7 @@ package me.fourteendoggo.mathexpressionparser.function; +import me.fourteendoggo.mathexpressionparser.symbol.Symbol; +import me.fourteendoggo.mathexpressionparser.symbol.SymbolType; import me.fourteendoggo.mathexpressionparser.utils.Assert; import java.util.function.ToDoubleFunction; @@ -8,7 +10,7 @@ * A placeholder for an invokable function. * @see FunctionContext */ -public class FunctionCallSite { +public class FunctionCallSite implements Symbol { private final String name; private final int minArgs, maxArgs; private final ToDoubleFunction function; @@ -27,6 +29,11 @@ public FunctionCallSite(String name, int minArgs, int maxArgs, ToDoubleFunction< this.function = function; } + @Override + public SymbolType getType() { + return SymbolType.FUNCTION; + } + public String getName() { return name; } diff --git a/core/src/main/java/me/fourteendoggo/mathexpressionparser/function/FunctionContainer.java b/core/src/main/java/me/fourteendoggo/mathexpressionparser/function/FunctionContainer.java index f2662e4..c2ff6ac 100644 --- a/core/src/main/java/me/fourteendoggo/mathexpressionparser/function/FunctionContainer.java +++ b/core/src/main/java/me/fourteendoggo/mathexpressionparser/function/FunctionContainer.java @@ -27,6 +27,7 @@ public static FunctionContainer withDefaultFunctions() { } private void insertDefaultFunctions() { + // trigonometric insertFunction("sin", Math::sin); insertFunction("cos", Math::cos); insertFunction("tan", Math::tan); @@ -38,25 +39,31 @@ private void insertDefaultFunctions() { insertFunction("tanh", Math::tanh); insertFunction("sqrt", Math::sqrt); insertFunction("cbrt", Math::cbrt); + + insertFunction("pow", Math::pow); insertFunction("log", Math::log); insertFunction("rad", Math::toRadians); insertFunction("floor", Math::floor); insertFunction("ceil", Math::ceil); - insertFunction("abs", d -> d < 0 ? -d : d); + insertFunction("abs", Math::abs); insertFunction("int", d -> (int) d); - insertFunction("and", (a, b) -> a != 0 && b != 0 ? 1 : 0); + // boolean + insertFunction("and", Utility::boolAnd); insertFunction("nand", (a, b) -> a != 0 && b != 0 ? 0 : 1); - insertFunction("or", (a, b) -> a != 0 || b != 0 ? 1 : 0); + insertFunction("or", Utility::boolOr); insertFunction("xor", (a, b) -> a != 0 ^ b != 0 ? 1 : 0); - insertFunction("not", a -> a == 0 ? 1 : 0); + insertFunction("not", Utility::boolNot); insertFunction("nor", (a, b) -> a != 0 || b != 0 ? 0 : 1); insertFunction("xnor", (a, b) -> a != 0 ^ b != 0 ? 0 : 1); + // no idea what this one is useful for, doubles are already representable as booleans + // maybe to transform a double to either 1 or 0 + insertFunction("bool", d -> d == 0 ? 0 : 1); // constants insertFunction(new FunctionCallSite("pi", 0, ctx -> Math.PI)); insertFunction(new FunctionCallSite("e", 0, ctx -> Math.E)); - // theoretical limit of Integer.MAX_VALUE + // theoretical limit of Integer.MAX_VALUE parameters insertFunction(new FunctionCallSite("min", 2, Integer.MAX_VALUE, ctx -> { double min = ctx.getDouble(0); for (int i = 1; i < ctx.size(); i++) { @@ -100,7 +107,7 @@ private void insertDefaultFunctions() { throw new SyntaxException(e.getMessage()); } })); - // must be handled with care as people can and will abuse this, maybe add a permission or straight up override this function? + // must be handled with care as people can and will abuse this, maybe add a permission or straight up overwrite this function? insertFunction(new FunctionCallSite("exit", 0, ctx -> { System.out.println("Exiting..."); System.exit(0); @@ -131,13 +138,13 @@ public void insertFunction(FunctionCallSite function) { functions.insert(name, function); } - public FunctionCallSite getFunction(char[] buffer, int fromPos) { - FunctionCallSite function = functions.search(buffer, fromPos); + public FunctionCallSite getFunction(char[] buf, int fromPos) { + FunctionCallSite function = functions.search(buf, fromPos); if (function == null) { // some unoptimized stuff, we are throwing an exception anyway, so it's not that bad - String bufferAsString = new String(buffer, fromPos, buffer.length - fromPos); - // there might be a better way to get the entered function name but as long as it works its good - String functionName = bufferAsString.split("[^a-zA-Z]")[0]; + // there might be a better way to get the entered function name + String bufAsString = new String(buf, fromPos, buf.length - fromPos); + String functionName = bufAsString.split("[^a-zA-Z]")[0]; throw new FunctionNotFoundException("function " + functionName + " not found"); } return function; diff --git a/core/src/main/java/me/fourteendoggo/mathexpressionparser/tokens/TokenType.java b/core/src/main/java/me/fourteendoggo/mathexpressionparser/tokens/TokenType.java index 7a45804..888c9b5 100644 --- a/core/src/main/java/me/fourteendoggo/mathexpressionparser/tokens/TokenType.java +++ b/core/src/main/java/me/fourteendoggo/mathexpressionparser/tokens/TokenType.java @@ -2,5 +2,5 @@ public enum TokenType { OPERATOR, - OPERAND + OPERAND, } diff --git a/core/src/main/java/me/fourteendoggo/mathexpressionparser/utils/Utility.java b/core/src/main/java/me/fourteendoggo/mathexpressionparser/utils/Utility.java index f6b3905..bec5060 100644 --- a/core/src/main/java/me/fourteendoggo/mathexpressionparser/utils/Utility.java +++ b/core/src/main/java/me/fourteendoggo/mathexpressionparser/utils/Utility.java @@ -2,7 +2,8 @@ public class Utility { private static final int[] COMMON_POWERS_OF_TEN = { - 1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000 + 1, 10, 100, 1000, 10_000, 100_000, 1_000_000, + 10_000_000, 100_000_000, 1_000_000_000 }; private static final String[] COMMON_ORDINAL_NAMES = { "first", "second", "third", "fourth", "fifth", "sixth", "seventh", "eighth", "ninth", "tenth" @@ -22,6 +23,32 @@ public static String getOrdinalName(int index) { return (index + 1) + "th"; } + public static int requireInt(double x) { + int intVal = (int) x; + Assert.isTrue(x == intVal, "an integer is required"); + return intVal; + } + + public static double boolToDouble(boolean x) { + return x ? 1 : 0; + } + + public static boolean doubleToBool(double x) { + return x != 0; + } + + public static double boolAnd(double a, double b) { + return (a != 0 && b != 0) ? 1 : 0; + } + + public static double boolOr(double a, double b) { + return (a != 0 || b != 0) ? 1 : 0; + } + + public static double boolNot(double x) { + return x == 0 ? 1 : 0; + } + public static boolean isLowercaseLetter(int c) { return c >= 'a' && c <= 'z'; } diff --git a/core/src/test/java/me/fourteendoggo/mathexpressionparser/ExpressionTester.java b/core/src/test/java/me/fourteendoggo/mathexpressionparser/ExpressionTester.java deleted file mode 100644 index 3713948..0000000 --- a/core/src/test/java/me/fourteendoggo/mathexpressionparser/ExpressionTester.java +++ /dev/null @@ -1,45 +0,0 @@ -package me.fourteendoggo.mathexpressionparser; - -import me.fourteendoggo.mathexpressionparser.container.TokenList; - -import java.io.BufferedReader; -import java.io.BufferedWriter; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Paths; - -public class ExpressionTester { - - public static void main(String[] args) throws IOException { - try (BufferedReader reader = Files.newBufferedReader(Paths.get("core/src/test/resources/tests.txt")); - BufferedWriter writer = Files.newBufferedWriter(Paths.get("core/src/test/resources/results.txt"))) { - String expression; - int errors = 0; - while ((expression = reader.readLine()) != null) { - if (expression.isBlank() || expression.startsWith("#")) continue; - - String expectedResult = reader.readLine(); - double result; - try { - result = ExpressionParser.parse(expression); - } catch (Throwable t) { - errors++; - writer.write("Expression %s threw an %s, error message: %s%n".formatted(expression, t.getClass().getSimpleName(), t.getMessage())); - continue; - } - - TokenList expectedTokens = new Tokenizer(expectedResult.toCharArray()).readTokens(); - double expected = expectedTokens.solve(); - if (result != expected) { - errors++; - writer.write("Expression %s returned %.8f instead of %.8f%n".formatted(expression, result, expected)); - } - } - if (errors == 0) { - System.out.println("✅ All tests passed"); - } else { - System.out.println("❌ " + errors + " tests failed, results got written to results.txt"); - } - } - } -} diff --git a/core/src/test/java/me/fourteendoggo/mathexpressionparser/FunctionContainerTest.java b/core/src/test/java/me/fourteendoggo/mathexpressionparser/FunctionContainerTest.java index ce38101..c242eb4 100644 --- a/core/src/test/java/me/fourteendoggo/mathexpressionparser/FunctionContainerTest.java +++ b/core/src/test/java/me/fourteendoggo/mathexpressionparser/FunctionContainerTest.java @@ -53,8 +53,8 @@ void testFunctionContextResizing() { IntStream.range(0, 20).forEach(parameters::add); assertEquals(20, parameters.size()); - char[] buffer = "max(bla bla bla)".toCharArray(); - assertEquals(19, container.getFunction(buffer, 0).apply(parameters)); + char[] buf = "max(bla bla bla)".toCharArray(); + assertEquals(19, container.getFunction(buf, 0).apply(parameters)); } @Test