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