├── .gitignore ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── example.fndr ├── .gitattributes ├── src └── redempt │ └── crunch │ ├── functional │ ├── EvaluationEnvironment.java │ ├── ArgumentList.java │ ├── FunctionCall.java │ ├── Function.java │ └── ExpressionEnv.java │ ├── token │ ├── Token.java │ ├── Value.java │ ├── TokenType.java │ ├── LiteralValue.java │ ├── LazyVariable.java │ ├── Constant.java │ ├── UnaryOperation.java │ ├── BinaryOperation.java │ ├── UnaryOperator.java │ └── BinaryOperator.java │ ├── exceptions │ ├── ExpressionEvaluationException.java │ └── ExpressionCompilationException.java │ ├── data │ ├── Pair.java │ ├── FastNumberParsing.java │ └── CharTree.java │ ├── Variable.java │ ├── ShuntingYard.java │ ├── Crunch.java │ ├── CompiledExpression.java │ └── ExpressionParser.java ├── settings.gradle ├── LICENSE ├── gradlew.bat ├── test └── redempt │ └── crunch │ └── test │ └── CrunchTest.java ├── README.md └── gradlew /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .idea/ 3 | .gradle/ 4 | *.iml 5 | build/ 6 | out/ 7 | solve/ -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boxbeam/Crunch/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /example.fndr: -------------------------------------------------------------------------------- 1 | # Example: Return the sum of a string 2 | # of space-separated integers 3 | $strsum = {$.split(" ").map(int).sum()} 4 | 5 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # 2 | # https://help.github.com/articles/dealing-with-line-endings/ 3 | # 4 | # These are explicitly windows files and should use crlf 5 | *.bat text eol=crlf 6 | 7 | -------------------------------------------------------------------------------- /src/redempt/crunch/functional/EvaluationEnvironment.java: -------------------------------------------------------------------------------- 1 | package redempt.crunch.functional; 2 | 3 | /** 4 | * Legacy, redirects to {@link ExpressionEnv} 5 | */ 6 | public class EvaluationEnvironment extends ExpressionEnv { 7 | 8 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /src/redempt/crunch/token/Token.java: -------------------------------------------------------------------------------- 1 | package redempt.crunch.token; 2 | 3 | /** 4 | * Represents a parsed token 5 | * @author Redempt 6 | */ 7 | public interface Token { 8 | 9 | /** 10 | * @return The type of this Token 11 | */ 12 | TokenType getType(); 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/redempt/crunch/exceptions/ExpressionEvaluationException.java: -------------------------------------------------------------------------------- 1 | package redempt.crunch.exceptions; 2 | 3 | public class ExpressionEvaluationException extends RuntimeException { 4 | 5 | public ExpressionEvaluationException(String message) { 6 | super(message); 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/redempt/crunch/token/Value.java: -------------------------------------------------------------------------------- 1 | package redempt.crunch.token; 2 | 3 | /** 4 | * Represents a lazy value which can be evaluated 5 | * @author Redempt 6 | */ 7 | public interface Value extends Token, Cloneable { 8 | 9 | double getValue(double[] variableValues); 10 | Value getClone(); 11 | 12 | } 13 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * This file was generated by the Gradle 'init' task. 3 | * 4 | * The settings file is used to specify which projects to include in your build. 5 | * 6 | * Detailed information about configuring a multi-project build in Gradle can be found 7 | * in the user manual at https://docs.gradle.org/6.0/userguide/multi_project_builds.html 8 | */ 9 | 10 | rootProject.name = 'Crunch' -------------------------------------------------------------------------------- /src/redempt/crunch/data/Pair.java: -------------------------------------------------------------------------------- 1 | package redempt.crunch.data; 2 | 3 | public class Pair { 4 | 5 | private final K first; 6 | private final V second; 7 | 8 | public Pair(K first, V second) { 9 | this.first = first; 10 | this.second = second; 11 | } 12 | 13 | public K getFirst() { 14 | return first; 15 | } 16 | 17 | public V getSecond() { 18 | return second; 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /src/redempt/crunch/token/TokenType.java: -------------------------------------------------------------------------------- 1 | package redempt.crunch.token; 2 | 3 | /** 4 | * An enum which represents the various types of Tokens 5 | * @author Redempt 6 | */ 7 | public enum TokenType { 8 | UNARY_OPERATOR, 9 | BINARY_OPERATOR, 10 | LITERAL_VALUE, 11 | VARIABLE, 12 | BINARY_OPERATION, 13 | UNARY_OPERATION, 14 | ARGUMENT_LIST, 15 | FUNCTION, 16 | FUNCTION_CALL, 17 | LAZY_VARIABLE; 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/redempt/crunch/token/LiteralValue.java: -------------------------------------------------------------------------------- 1 | package redempt.crunch.token; 2 | 3 | public class LiteralValue implements Value { 4 | 5 | private final double value; 6 | 7 | public LiteralValue(double value) { 8 | this.value = value; 9 | } 10 | 11 | @Override 12 | public TokenType getType() { 13 | return TokenType.LITERAL_VALUE; 14 | } 15 | 16 | @Override 17 | public double getValue(double[] variableValues) { 18 | return value; 19 | } 20 | 21 | public String toString() { 22 | return value + ""; 23 | } 24 | 25 | public LiteralValue getClone() { 26 | return new LiteralValue(value); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/redempt/crunch/functional/ArgumentList.java: -------------------------------------------------------------------------------- 1 | package redempt.crunch.functional; 2 | 3 | import redempt.crunch.token.Token; 4 | import redempt.crunch.token.TokenType; 5 | import redempt.crunch.token.Value; 6 | 7 | /** 8 | * Represents a list of arguments being passed to a Function 9 | * @author Redempt 10 | */ 11 | public class ArgumentList implements Token { 12 | 13 | private final Value[] arguments; 14 | 15 | public ArgumentList(Value[] arguments) { 16 | this.arguments = arguments; 17 | } 18 | 19 | public Value[] getArguments() { 20 | return arguments; 21 | } 22 | 23 | @Override 24 | public TokenType getType() { 25 | return TokenType.ARGUMENT_LIST; 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/redempt/crunch/Variable.java: -------------------------------------------------------------------------------- 1 | package redempt.crunch; 2 | 3 | import redempt.crunch.token.TokenType; 4 | import redempt.crunch.token.Value; 5 | 6 | public class Variable implements Value { 7 | 8 | private final int index; 9 | 10 | public Variable(int index) { 11 | this.index = index; 12 | } 13 | 14 | public int getIndex() { 15 | return index; 16 | } 17 | 18 | @Override 19 | public double getValue(double[] variableValues) { 20 | return variableValues[index]; 21 | } 22 | 23 | @Override 24 | public TokenType getType() { 25 | return TokenType.VARIABLE; 26 | } 27 | 28 | public String toString() { 29 | return "$" + (index + 1); 30 | } 31 | 32 | public Variable getClone() { 33 | return new Variable(index); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/redempt/crunch/token/LazyVariable.java: -------------------------------------------------------------------------------- 1 | package redempt.crunch.token; 2 | 3 | import java.util.function.DoubleSupplier; 4 | 5 | public class LazyVariable implements Value { 6 | 7 | private final String name; 8 | private final DoubleSupplier supplier; 9 | 10 | public LazyVariable(String name, DoubleSupplier supplier) { 11 | this.name = name; 12 | this.supplier = supplier; 13 | } 14 | 15 | @Override 16 | public TokenType getType() { 17 | return TokenType.LAZY_VARIABLE; 18 | } 19 | 20 | @Override 21 | public double getValue(double[] variableValues) { 22 | return supplier.getAsDouble(); 23 | } 24 | 25 | @Override 26 | public Value getClone() { 27 | return this; 28 | } 29 | 30 | @Override 31 | public String toString() { 32 | return name; 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/redempt/crunch/token/Constant.java: -------------------------------------------------------------------------------- 1 | package redempt.crunch.token; 2 | 3 | import java.util.Locale; 4 | 5 | /** 6 | * Represents a mathematical or boolean constant which can be used in an expression 7 | * @author Redempt 8 | */ 9 | public enum Constant implements Value { 10 | 11 | PI(Math.PI), 12 | E(Math.E), 13 | TRUE(1), 14 | FALSE(0); 15 | 16 | private final double value; 17 | 18 | Constant(double value) { 19 | this.value = value; 20 | } 21 | 22 | @Override 23 | public TokenType getType() { 24 | return TokenType.LITERAL_VALUE; 25 | } 26 | 27 | @Override 28 | public double getValue(double[] variableValues) { 29 | return value; 30 | } 31 | 32 | @Override 33 | public Value getClone() { 34 | return this; 35 | } 36 | 37 | @Override 38 | public String toString() { 39 | return super.toString().toLowerCase(Locale.ROOT); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/redempt/crunch/exceptions/ExpressionCompilationException.java: -------------------------------------------------------------------------------- 1 | package redempt.crunch.exceptions; 2 | 3 | import redempt.crunch.ExpressionParser; 4 | 5 | public class ExpressionCompilationException extends RuntimeException { 6 | 7 | private final ExpressionParser parser; 8 | 9 | public ExpressionCompilationException(ExpressionParser parser, String message) { 10 | super(generateMessage(parser, message)); 11 | this.parser = parser; 12 | } 13 | 14 | public ExpressionParser getParser() { 15 | return parser; 16 | } 17 | 18 | private static String generateMessage(ExpressionParser parser, String message) { 19 | if (parser == null) { 20 | return message; 21 | } 22 | return message + ":\n" + parser.getInput() + "\n" + repeat(' ', parser.getCursor()) + "^"; 23 | } 24 | 25 | private static String repeat(char c, int n) { 26 | StringBuilder builder = new StringBuilder(n); 27 | for (int i = 0; i < n; i++) { 28 | builder.append(c); 29 | } 30 | return builder.toString(); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/redempt/crunch/token/UnaryOperation.java: -------------------------------------------------------------------------------- 1 | package redempt.crunch.token; 2 | 3 | public class UnaryOperation implements Value { 4 | private final UnaryOperator operator; 5 | private final Value first; 6 | 7 | public UnaryOperation(UnaryOperator operator, Value value) { 8 | this.operator = operator; 9 | this.first = value; 10 | } 11 | 12 | public UnaryOperator getOperator() { 13 | return operator; 14 | } 15 | 16 | public Value getChild() { 17 | return first; 18 | } 19 | 20 | @Override 21 | public double getValue(double[] variableValues) { 22 | return operator.getOperation().applyAsDouble(first.getValue(variableValues)); 23 | } 24 | 25 | @Override 26 | public TokenType getType() { 27 | return TokenType.UNARY_OPERATOR; 28 | } 29 | 30 | public String toString() { 31 | return "(" + operator.getSymbol() + first.toString() + ")"; 32 | } 33 | 34 | public UnaryOperation getClone() { 35 | return new UnaryOperation(operator, first.getClone()); 36 | } 37 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Redempt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/redempt/crunch/token/BinaryOperation.java: -------------------------------------------------------------------------------- 1 | package redempt.crunch.token; 2 | 3 | public class BinaryOperation implements Value { 4 | 5 | private final BinaryOperator operator; 6 | private final Value first; 7 | private final Value second; 8 | 9 | public BinaryOperation(BinaryOperator operator, Value first, Value second) { 10 | this.operator = operator; 11 | this.first = first; 12 | this.second = second; 13 | } 14 | 15 | public BinaryOperator getOperator() { 16 | return operator; 17 | } 18 | 19 | public Value[] getValues() { 20 | return new Value[] {first, second}; 21 | } 22 | 23 | @Override 24 | public double getValue(double[] variableValues) { 25 | return operator.getOperation().applyAsDouble(first.getValue(variableValues), second.getValue(variableValues)); 26 | } 27 | 28 | @Override 29 | public TokenType getType() { 30 | return TokenType.BINARY_OPERATION; 31 | } 32 | 33 | public String toString() { 34 | return "(" + first.toString() + operator.getSymbol() + second.toString() + ")"; 35 | } 36 | 37 | public BinaryOperation getClone() { 38 | return new BinaryOperation(operator, first.getClone(), second.getClone()); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/redempt/crunch/ShuntingYard.java: -------------------------------------------------------------------------------- 1 | package redempt.crunch; 2 | 3 | import redempt.crunch.token.*; 4 | 5 | import java.util.ArrayDeque; 6 | import java.util.Deque; 7 | 8 | public class ShuntingYard { 9 | 10 | private final Deque operators = new ArrayDeque<>(); 11 | private final Deque stack = new ArrayDeque<>(); 12 | 13 | public void addOperator(BinaryOperator operator) { 14 | while (!operators.isEmpty() && operator.getPriority() <= operators.getLast().getPriority()) { 15 | createOperation(); 16 | } 17 | operators.add(operator); 18 | } 19 | 20 | public void addValue(Value value) { 21 | stack.add(value); 22 | } 23 | 24 | private void createOperation() { 25 | BinaryOperator op = operators.removeLast(); 26 | Value right = stack.removeLast(); 27 | Value left = stack.removeLast(); 28 | if (right.getType() == TokenType.LITERAL_VALUE && left.getType() == TokenType.LITERAL_VALUE) { 29 | stack.add(new LiteralValue(op.getOperation().applyAsDouble(left.getValue(new double[0]), right.getValue(new double[0])))); 30 | } else { 31 | stack.add(new BinaryOperation(op, left, right)); 32 | } 33 | } 34 | 35 | public Value finish() { 36 | while (stack.size() > 1) { 37 | createOperation(); 38 | } 39 | return stack.removeLast(); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/redempt/crunch/functional/FunctionCall.java: -------------------------------------------------------------------------------- 1 | package redempt.crunch.functional; 2 | 3 | import redempt.crunch.token.TokenType; 4 | import redempt.crunch.token.Value; 5 | 6 | /** 7 | * Represents a lazy function call with other lazy values as function arguments 8 | * @author Redempt 9 | */ 10 | public class FunctionCall implements Value { 11 | 12 | private final Value[] values; 13 | private final Function function; 14 | private final double[] numbers; 15 | 16 | public FunctionCall(Function function, Value[] values) { 17 | this.function = function; 18 | this.values = values; 19 | numbers = new double[function.getArgCount()]; 20 | } 21 | 22 | @Override 23 | public TokenType getType() { 24 | return TokenType.FUNCTION_CALL; 25 | } 26 | 27 | @Override 28 | public double getValue(double[] variableValues) { 29 | for (int i = 0; i < values.length; i++) { 30 | numbers[i] = values[i].getValue(variableValues); 31 | } 32 | return function.call(numbers); 33 | } 34 | 35 | @Override 36 | public Value getClone() { 37 | Value[] clone = new Value[values.length]; 38 | System.arraycopy(values, 0, clone, 0, values.length); 39 | return new FunctionCall(function, values); 40 | } 41 | 42 | public String toString() { 43 | StringBuilder builder = new StringBuilder(function.getName()).append('('); 44 | for (int i = 0; i < values.length; i++) { 45 | Value arg = values[i]; 46 | builder.append(arg.toString()); 47 | if (i != values.length - 1) { 48 | builder.append(", "); 49 | } 50 | } 51 | return builder.append(')').toString(); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/redempt/crunch/functional/Function.java: -------------------------------------------------------------------------------- 1 | package redempt.crunch.functional; 2 | 3 | import redempt.crunch.token.Token; 4 | import redempt.crunch.token.TokenType; 5 | 6 | import java.util.function.ToDoubleFunction; 7 | 8 | /** 9 | * Represents a function which can be called in expressions whose environments have it 10 | * @author Redempt 11 | */ 12 | public class Function implements Token { 13 | 14 | private final String name; 15 | private final int argCount; 16 | private final ToDoubleFunction function; 17 | 18 | /** 19 | * Create a Function 20 | * @param name The function name 21 | * @param argCount The number of arguments this Function will take 22 | * @param function A lambda to take the arguments as a double array and return a value 23 | */ 24 | public Function(String name, int argCount, ToDoubleFunction function) { 25 | this.function = function; 26 | this.name = name; 27 | this.argCount = argCount; 28 | } 29 | 30 | /** 31 | * @return The name of this function 32 | */ 33 | public String getName() { 34 | return name; 35 | } 36 | 37 | /** 38 | * @return The number of arguments this function takes 39 | */ 40 | public int getArgCount() { 41 | return argCount; 42 | } 43 | 44 | /** 45 | * Calls this function with a set of values - Warning, no validation is done on array size 46 | * @param values The input values 47 | * @return The output value 48 | */ 49 | public double call(double[] values) { 50 | return function.applyAsDouble(values); 51 | } 52 | 53 | @Override 54 | public TokenType getType() { 55 | return TokenType.FUNCTION; 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/redempt/crunch/token/UnaryOperator.java: -------------------------------------------------------------------------------- 1 | package redempt.crunch.token; 2 | 3 | import java.util.concurrent.ThreadLocalRandom; 4 | import java.util.function.DoubleUnaryOperator; 5 | 6 | public enum UnaryOperator implements Token { 7 | NEGATE("-", d -> -d), 8 | NOT("!", d -> d == 1 ? 0 : 1), 9 | SIN("sin", Math::sin), 10 | COS("cos", Math::cos), 11 | TAN("tan", Math::tan), 12 | SINH("sinh", Math::sinh), 13 | COSH("cosh", Math::cosh), 14 | TANH("tanh", Math::tanh), 15 | ASIN("asin", Math::asin), 16 | ACOS("acos", Math::acos), 17 | ATAN("atan", Math::atan), 18 | ABS("abs", Math::abs), 19 | ROUND("round", Math::round), 20 | FLOOR("floor", Math::floor), 21 | CEIL("ceil", Math::ceil), 22 | LOG("log", Math::log), 23 | SQRT("sqrt", Math::sqrt), 24 | CBRT("cbrt", Math::cbrt), 25 | RAND("rand", d -> ThreadLocalRandom.current().nextDouble() * d, false); 26 | 27 | private final String symbol; 28 | private final DoubleUnaryOperator operation; 29 | private boolean pure = true; 30 | 31 | UnaryOperator(String symbol, DoubleUnaryOperator operation) { 32 | this.symbol = symbol; 33 | this.operation = operation; 34 | } 35 | 36 | UnaryOperator(String symbol, DoubleUnaryOperator operation, boolean pure) { 37 | this(symbol, operation); 38 | this.pure = pure; 39 | } 40 | 41 | @Override 42 | public TokenType getType() { 43 | return TokenType.UNARY_OPERATOR; 44 | } 45 | 46 | public DoubleUnaryOperator getOperation() { 47 | return operation; 48 | } 49 | 50 | public int getPriority() { 51 | return 6; 52 | } 53 | 54 | public boolean isPure() { 55 | return pure; 56 | } 57 | 58 | public String getSymbol() { 59 | return symbol; 60 | } 61 | 62 | @Override 63 | public String toString() { 64 | return symbol; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/redempt/crunch/token/BinaryOperator.java: -------------------------------------------------------------------------------- 1 | package redempt.crunch.token; 2 | 3 | import java.util.function.DoubleBinaryOperator; 4 | 5 | /** 6 | * Represents an Operator which can be used in mathematical expressions 7 | * 8 | * @author Redempt 9 | */ 10 | public enum BinaryOperator implements Token { 11 | 12 | BOOLEAN_OR("|", 0, (a, b) -> (a == 1 || b == 1) ? 1d : 0d), 13 | BOOLEAN_OR_ALT("||", 0, (a, b) -> (a == 1 || b == 1) ? 1d : 0d), 14 | BOOLEAN_AND("&", 0, (a, b) -> (a == 1 && b == 1) ? 1d : 0d), 15 | BOOLEAN_AND_ALT("&&", 0, (a, b) -> (a == 1 && b == 1) ? 1d : 0d), 16 | GREATER_THAN(">", 1, (a, b) -> a > b ? 1d : 0d), 17 | LESS_THAN("<", 1, (a, b) -> a < b ? 1d : 0d), 18 | EQUAL_TO("=", 1, (a, b) -> a == b ? 1d : 0d), 19 | EQUAL_TO_ALT("==", 1, (a, b) -> a == b ? 1d : 0d), 20 | NOT_EQUAL_TO("!=", 1, (a, b) -> a != b ? 1d : 0d), 21 | GREATER_THAN_OR_EQUAL_TO(">=", 1, (a, b) -> a >= b ? 1d : 0d), 22 | LESS_THAN_OR_EQUAL_TO("<=", 1, (a, b) -> a <= b ? 1d : 0d), 23 | EXPONENT("^", 5, (a, b) -> Math.pow(a, b)), 24 | MULTIPLY("*", 4, (a, b) -> a * b), 25 | DIVIDE("/", 4, (a, b) -> a / b), 26 | MODULUS("%", 4, (a, b) -> a % b), 27 | ADD("+", 3, (a, b) -> a + b), 28 | SUBTRACT("-", 3, (a, b) -> a - b), 29 | SCIENTIFIC_NOTATION("E", 5, (a, b) -> a * Math.pow(10, b)); 30 | 31 | private final String symbol; 32 | private final DoubleBinaryOperator operation; 33 | private final int priority; 34 | 35 | BinaryOperator(String name, int priority, DoubleBinaryOperator operation) { 36 | this.symbol = name; 37 | this.operation = operation; 38 | this.priority = priority; 39 | } 40 | 41 | @Override 42 | public TokenType getType() { 43 | return TokenType.BINARY_OPERATOR; 44 | } 45 | 46 | public String toString() { 47 | return symbol; 48 | } 49 | 50 | public String getSymbol() { 51 | return symbol; 52 | } 53 | 54 | public DoubleBinaryOperator getOperation() { 55 | return operation; 56 | } 57 | 58 | public int getPriority() { 59 | return priority; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/redempt/crunch/Crunch.java: -------------------------------------------------------------------------------- 1 | package redempt.crunch; 2 | 3 | import redempt.crunch.functional.ExpressionEnv; 4 | import redempt.crunch.token.BinaryOperator; 5 | 6 | /** 7 | * Public API methods for compiling expressions 8 | * @author Redempt 9 | */ 10 | public final class Crunch { 11 | 12 | private Crunch() { 13 | // Prevent instantiation 14 | } 15 | 16 | private static final ExpressionEnv DEFAULT_EVALUATION_ENVIRONMENT = new ExpressionEnv(); 17 | 18 | /** 19 | * Compiles a mathematical expression into a CompiledExpression. Variables must be integers starting at 1 prefixed 20 | * with $. Supported operations can be found in {@link BinaryOperator}, which lists the operations and their symbols 21 | * for use in expressions. Parenthesis are also supported. 22 | * @param expression The expression to compile 23 | * @return The compiled expression 24 | */ 25 | public static CompiledExpression compileExpression(String expression) { 26 | return Crunch.compileExpression(expression, DEFAULT_EVALUATION_ENVIRONMENT); 27 | } 28 | 29 | /** 30 | * Compiles a mathematical expression into a CompiledExpression. Variables must be integers starting at 1 prefixed 31 | * with $. Supported operations can be found in {@link BinaryOperator}, which lists the operations and their symbols 32 | * for use in expressions. Parenthesis are also supported. 33 | * @param expression The expression to compile 34 | * @param env The EvaluationEnvironment providing custom functions that can be used in the expression 35 | * @return The compiled expression 36 | */ 37 | public static CompiledExpression compileExpression(String expression, ExpressionEnv env) { 38 | return new ExpressionParser(expression, env).parse(); 39 | } 40 | 41 | /** 42 | * Compiles and evaluates an expression once. This is only for if you need a one-off evaluation of an expression 43 | * which will not be evaluated again. If the expression will be evaluated multiple times, use {@link Crunch#compileExpression(String)} 44 | * @param expression The expression to evaluate 45 | * @param varValues The variable values for the expression 46 | * @return The value of the expression 47 | */ 48 | public static double evaluateExpression(String expression, double... varValues) { 49 | return Crunch.compileExpression(expression).evaluate(varValues); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /src/redempt/crunch/data/FastNumberParsing.java: -------------------------------------------------------------------------------- 1 | package redempt.crunch.data; 2 | 3 | /** 4 | * Utility class with some methods for parsing base 10 numbers (only ints and doubles for now) that are faster than the standard Java implementation 5 | */ 6 | public class FastNumberParsing { 7 | 8 | /** 9 | * Parse an integer from base 10 string input 10 | * @param input The base 10 string input 11 | * @return The parsed integer 12 | */ 13 | public static int parseInt(String input) { 14 | return parseInt(input, 0, input.length()); 15 | } 16 | 17 | /** 18 | * Parse an integer from base 10 string input 19 | * @param input The base 10 string input 20 | * @param start The starting index to parse from, inclusive 21 | * @param end The ending index to parse to, exclusive 22 | * @return The parsed integer 23 | */ 24 | public static int parseInt(String input, int start, int end) { 25 | if (start == end) { 26 | throw new NumberFormatException("Zero-length input"); 27 | } 28 | int i = start; 29 | boolean negative = false; 30 | if (input.charAt(i) == '-') { 31 | negative = true; 32 | i++; 33 | } 34 | int output = 0; 35 | for (; i < end; i++) { 36 | char c = input.charAt(i); 37 | if (c > '9' || c < '0') { 38 | throw new NumberFormatException("Non-numeric character in input '" + input.substring(start, end) + "'"); 39 | } 40 | output *= 10; 41 | output += c - '0'; 42 | } 43 | return negative ? -output: output; 44 | } 45 | 46 | /** 47 | * Parse a double from base 10 string input, only real number values are supported (no NaN or Infinity) 48 | * @param input The base 10 string input 49 | * @return The parsed double 50 | */ 51 | public static double parseDouble(String input) { 52 | return parseDouble(input, 0, input.length()); 53 | } 54 | 55 | /** 56 | * Parse a double from base 10 string input, only real number values are supported (no NaN or Infinity) 57 | * @param input The base 10 string input 58 | * @param start The starting index to parse from, inclusive 59 | * @param end The ending index to parse to, exclusive 60 | * @return The parsed double 61 | */ 62 | public static double parseDouble(String input, int start, int end) { 63 | if (start == end) { 64 | throw new NumberFormatException("Zero-length input"); 65 | } 66 | int i = start; 67 | boolean negative = false; 68 | if (input.charAt(start) == '-') { 69 | negative = true; 70 | i++; 71 | } 72 | double output = 0; 73 | double after = 0; 74 | int decimal = -1; 75 | for (; i < end; i++) { 76 | char c = input.charAt(i); 77 | if (c == '.') { 78 | if (decimal != -1) { 79 | throw new NumberFormatException("Second period in double for input '" + input + "'"); 80 | } 81 | decimal = i; 82 | continue; 83 | } 84 | if (c > '9' || c < '0') { 85 | throw new NumberFormatException("Non-numeric character in input '" + input + "'"); 86 | } 87 | if (decimal != -1) { 88 | after *= 10; 89 | after += c - '0'; 90 | } else { 91 | output *= 10; 92 | output += c - '0'; 93 | } 94 | } 95 | after /= Math.pow(10, end - decimal - 1); 96 | return negative ? (-output - after) : (output + after); 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /src/redempt/crunch/data/CharTree.java: -------------------------------------------------------------------------------- 1 | package redempt.crunch.data; 2 | 3 | import redempt.crunch.ExpressionParser; 4 | 5 | /** 6 | * A simple implementation of a prefix tree for better parsing 7 | * Only supports ASCII characters 8 | * @param The type stored in this CharTree 9 | */ 10 | public class CharTree { 11 | 12 | private final Node root = new Node(); 13 | 14 | /** 15 | * Sets a String in this CharTree 16 | * @param str The String to use as the key 17 | * @param value The value to store 18 | */ 19 | public void set(String str, T value) { 20 | Node node = root; 21 | for (char c : str.toCharArray()) { 22 | node = node.getOrCreateNode(c); 23 | } 24 | node.setValue(value); 25 | } 26 | 27 | /** 28 | * Gets a value by its key 29 | * @param str The key 30 | * @return The value mapped to the key, or null if it is not present 31 | */ 32 | public T get(String str) { 33 | Node node = root; 34 | for (char c : str.toCharArray()) { 35 | node = node.getNode(c); 36 | if (node == null) { 37 | return null; 38 | } 39 | } 40 | return (T) node.getValue(); 41 | } 42 | 43 | /** 44 | * Check if the character exists at the root level in this tree 45 | * @param c The character to check 46 | * @return Whether the character exists at the root level 47 | */ 48 | public boolean containsFirstChar(char c) { 49 | return root.getNode(c) != null; 50 | } 51 | 52 | /** 53 | * Gets a token forward from the given index in a string 54 | * @param str The string to search in 55 | * @param index The starting index to search from 56 | * @return A pair with the token or null if none was found, and the length parsed 57 | */ 58 | public Pair getFrom(String str, int index) { 59 | Node node = root; 60 | T val = null; 61 | for (int i = index; i < str.length(); i++) { 62 | node = node.getNode(str.charAt(i)); 63 | if (node == null) { 64 | return new Pair<>(val, i - index); 65 | } 66 | if (node.getValue() != null) { 67 | val = (T) node.getValue(); 68 | } 69 | } 70 | return new Pair<>(val, str.length() - index); 71 | } 72 | 73 | public T getWith(ExpressionParser parser) { 74 | Node node = root; 75 | T val = null; 76 | int lastParsed = parser.getCursor(); 77 | String input = parser.getInput(); 78 | 79 | for (int i = lastParsed; i < input.length(); i++) { 80 | node = node.getNode(input.charAt(i)); 81 | if (node == null) { 82 | parser.setCursor(val == null ? parser.getCursor() : lastParsed + 1); 83 | return val; 84 | } 85 | T nodeValue = (T) node.getValue(); 86 | if (nodeValue != null) { 87 | lastParsed = i; 88 | val = nodeValue; 89 | } 90 | } 91 | if (val != null) { 92 | parser.setCursor(lastParsed + 1); 93 | } 94 | return val; 95 | } 96 | 97 | private static class Node { 98 | 99 | private Object value; 100 | private final Node[] children = new Node[256]; 101 | 102 | public Node getNode(char c) { 103 | return children[c]; 104 | } 105 | 106 | public Node getOrCreateNode(char c) { 107 | if (children[c] == null) { 108 | children[c] = new Node(); 109 | } 110 | return children[c]; 111 | } 112 | 113 | public Object getValue() { 114 | return value; 115 | } 116 | 117 | public void setValue(Object value) { 118 | this.value = value; 119 | } 120 | 121 | } 122 | 123 | } 124 | -------------------------------------------------------------------------------- /src/redempt/crunch/CompiledExpression.java: -------------------------------------------------------------------------------- 1 | package redempt.crunch; 2 | 3 | import redempt.crunch.exceptions.ExpressionEvaluationException; 4 | import redempt.crunch.token.BinaryOperation; 5 | import redempt.crunch.token.TokenType; 6 | import redempt.crunch.token.Value; 7 | 8 | /** 9 | * An expression which has been compiled with {@link Crunch#compileExpression(String)} and can be evaluated with {@link CompiledExpression#evaluate(double...)} 10 | * @author Redempt 11 | */ 12 | public class CompiledExpression { 13 | 14 | protected double[] variableValues; 15 | private int variableCount; 16 | private Value value; 17 | 18 | protected CompiledExpression() {} 19 | 20 | public CompiledExpression(Value value, int variableCount) { 21 | initialize(value, variableCount); 22 | } 23 | 24 | protected void initialize(Value value, int variableCount) { 25 | this.value = value; 26 | this.variableCount = variableCount; 27 | } 28 | 29 | protected void setVariableValues(double[] values) { 30 | checkArgCount(values.length); 31 | variableValues = values; 32 | } 33 | 34 | /** 35 | * Gets the internal Value representation of the expression. This is essentially reflection into the expression. Proceed at your own risk. 36 | * @return The Value this CompiledExpression wraps 37 | */ 38 | public Value getValue() { 39 | return value; 40 | } 41 | 42 | /** 43 | * Gets the highest index of variables used in this expression. Any call to {@link CompiledExpression#evaluate(double...)} 44 | * must pass at least this many values. 45 | * @return The number of variables used in this expression 46 | */ 47 | public int getVariableCount() { 48 | return variableCount; 49 | } 50 | 51 | /** 52 | * Evaluates this CompiledExpression and returns its value 53 | * @param values The values for variables used in this expression, in order starting with 1 54 | * @return The resulting value 55 | */ 56 | public double evaluate(double... values) { 57 | setVariableValues(values); 58 | return value.getValue(this.variableValues); 59 | } 60 | 61 | /** 62 | * Evaluates this CompiledExpression and returns its value without modifying variable values 63 | * @return The resulting value 64 | */ 65 | public double evaluate() { 66 | checkArgCount(0); 67 | return value.getValue(this.variableValues); 68 | } 69 | 70 | /** 71 | * Evaluates this CompiledExpression and returns its value 72 | * @param first The first variable value 73 | * @return The resulting value 74 | */ 75 | public double evaluate(double first) { 76 | checkArgCount(1); 77 | if (variableValues == null) { 78 | variableValues = new double[1]; 79 | } 80 | variableValues[0] = first; 81 | return value.getValue(this.variableValues); 82 | } 83 | 84 | /** 85 | * Evaluates this CompiledExpression and returns its value 86 | * @param first The first variable value 87 | * @param second The second variable value 88 | * @return The resulting value 89 | */ 90 | public double evaluate(double first, double second) { 91 | checkArgCount(2); 92 | if (variableValues == null) { 93 | variableValues = new double[2]; 94 | } 95 | variableValues[0] = first; 96 | variableValues[1] = second; 97 | return value.getValue(this.variableValues); 98 | } 99 | 100 | private void checkArgCount(int args) { 101 | if (variableCount > args) { 102 | throw new ExpressionEvaluationException("Too few variable values - expected " + variableCount + ", got " + args); 103 | } 104 | } 105 | 106 | /** 107 | * @return A clone of this CompiledExpression 108 | */ 109 | public CompiledExpression clone() { 110 | return new CompiledExpression(value, variableCount); 111 | } 112 | 113 | /** 114 | * Converts this CompiledExpression back to a String which can be used to recreate it later 115 | * @return A String representation of this CompiledExpression 116 | */ 117 | public String toString() { 118 | return value.toString(); 119 | } 120 | 121 | } 122 | -------------------------------------------------------------------------------- /src/redempt/crunch/functional/ExpressionEnv.java: -------------------------------------------------------------------------------- 1 | package redempt.crunch.functional; 2 | 3 | import redempt.crunch.data.CharTree; 4 | import redempt.crunch.token.*; 5 | import redempt.crunch.Variable; 6 | 7 | import java.util.Locale; 8 | import java.util.function.DoubleSupplier; 9 | import java.util.function.ToDoubleFunction; 10 | 11 | /** 12 | * Represents an environment containing functions that can be called in expressions 13 | * @author Redempt 14 | */ 15 | public class ExpressionEnv { 16 | 17 | private final CharTree binaryOperators = new CharTree<>(); 18 | private final CharTree leadingOperators = new CharTree<>(); 19 | private final CharTree values = new CharTree<>(); 20 | 21 | private int varCount = 0; 22 | 23 | /** 24 | * Creates a new EvaluationEnvironment 25 | */ 26 | public ExpressionEnv() { 27 | for (BinaryOperator operator : BinaryOperator.values()) { 28 | binaryOperators.set(operator.getSymbol(), operator); 29 | } 30 | for (UnaryOperator operator : UnaryOperator.values()) { 31 | leadingOperators.set(operator.getSymbol(), operator); 32 | } 33 | for (Constant constant : Constant.values()) { 34 | values.set(constant.toString().toLowerCase(Locale.ROOT), constant); 35 | } 36 | } 37 | 38 | private void checkName(String name) { 39 | if (name == null || name.isEmpty()) { 40 | throw new IllegalArgumentException("Identifier cannot be empty or null"); 41 | } 42 | if (!Character.isAlphabetic(name.charAt(0))) { 43 | throw new IllegalArgumentException("Identifier must begin with an alphabetic character"); 44 | } 45 | } 46 | 47 | /** 48 | * Adds a Function that can be called from expressions with this environment 49 | * @param function The function 50 | */ 51 | public ExpressionEnv addFunction(Function function) { 52 | if (function == null) { 53 | throw new IllegalArgumentException("Function cannot be null"); 54 | } 55 | 56 | String name = function.getName(); 57 | this.checkName(name); 58 | this.leadingOperators.set(name, function); 59 | return this; 60 | } 61 | 62 | /** 63 | * Adds any number of Functions that can be called from expressions with this environment 64 | * @param functions The functions to add 65 | */ 66 | public ExpressionEnv addFunctions(Function... functions) { 67 | if (functions == null) { 68 | throw new IllegalArgumentException("Functions cannot be null"); 69 | } 70 | 71 | for (Function function : functions) { 72 | addFunction(function); 73 | } 74 | return this; 75 | } 76 | 77 | /** 78 | * Adds a lazily-evaluated variable that will not need to be passed with the variable values 79 | * @param name The name of the lazy variable 80 | * @param supply A function to supply the value of the variable when needed 81 | */ 82 | public ExpressionEnv addLazyVariable(String name, DoubleSupplier supply) { 83 | if (supply == null) { 84 | throw new IllegalArgumentException("Supply cannot be null"); 85 | } 86 | 87 | checkName(name); 88 | values.set(name, new LazyVariable(name, supply)); 89 | return this; 90 | } 91 | 92 | public ExpressionEnv setVariableNames(String... names) { 93 | if (names == null) { 94 | throw new IllegalArgumentException("Names cannot be null"); 95 | } 96 | 97 | varCount = names.length; 98 | for (int i = 0; i < names.length; i++) { 99 | checkName(names[i]); 100 | values.set(names[i], new Variable(i)); 101 | } 102 | return this; 103 | } 104 | 105 | /** 106 | * Adds a Function that can be called from expressions with this environment 107 | * @param name The function name 108 | * @param argCount The argument count for the function 109 | * @param func The lambda to accept the arguments as a double array and return a value 110 | */ 111 | public ExpressionEnv addFunction(String name, int argCount, ToDoubleFunction func) { 112 | addFunction(new Function(name, argCount, func)); 113 | return this; 114 | } 115 | 116 | /** 117 | * @return The prefix tree of all leading operators, including unary operators and functions 118 | */ 119 | public CharTree getLeadingOperators() { 120 | return this.leadingOperators; 121 | } 122 | 123 | /** 124 | * @return The prefix tree of all binary operators 125 | */ 126 | public CharTree getBinaryOperators() { 127 | return this.binaryOperators; 128 | } 129 | 130 | /** 131 | * @return The prefix tree of all values, including constants, variables, and lazy variables 132 | */ 133 | public CharTree getValues() { 134 | return this.values; 135 | } 136 | 137 | /** 138 | * @return The number of variables in this expression environment 139 | */ 140 | public int getVariableCount() { 141 | return this.varCount; 142 | } 143 | 144 | } 145 | -------------------------------------------------------------------------------- /test/redempt/crunch/test/CrunchTest.java: -------------------------------------------------------------------------------- 1 | package redempt.crunch.test; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import redempt.crunch.CompiledExpression; 5 | import redempt.crunch.Crunch; 6 | import redempt.crunch.exceptions.ExpressionCompilationException; 7 | import redempt.crunch.exceptions.ExpressionEvaluationException; 8 | import redempt.crunch.functional.ExpressionEnv; 9 | 10 | import static org.junit.jupiter.api.Assertions.*; 11 | 12 | class CrunchTest { 13 | 14 | private static final double DELTA = 1e-7; 15 | 16 | @Test 17 | void nullTest() { 18 | assertThrows(ExpressionCompilationException.class, () -> Crunch.compileExpression(null), "Null single argument"); 19 | assertThrows(ExpressionCompilationException.class, () -> Crunch.compileExpression(null, null), "Null multi-argument"); 20 | assertThrows(ExpressionCompilationException.class, () -> Crunch.compileExpression("1", null), "Second argument null"); 21 | } 22 | 23 | @Test 24 | void constantTest() { 25 | assertEquals(Math.PI, Crunch.evaluateExpression("pi"), DELTA, "Pi equality"); 26 | assertEquals(Math.E, Crunch.evaluateExpression("e"), DELTA, "Euler's constant equality"); 27 | assertEquals(1, Crunch.evaluateExpression("true"), DELTA, "True equal to 1"); 28 | assertEquals(0, Crunch.evaluateExpression("false"), DELTA, "False equal to 0"); 29 | assertEquals(-1, Crunch.evaluateExpression("-1"), "Negation operator"); 30 | } 31 | 32 | @Test 33 | void basicOperationTest() { 34 | assertEquals(2, Crunch.evaluateExpression("1+1"), "Simple addition"); 35 | assertEquals(2, Crunch.evaluateExpression("1 + 1"), "Simple expression with whitespace"); 36 | assertEquals(2, Crunch.evaluateExpression(" 1 + 1 "), "Lots of whitespace"); 37 | assertEquals(8, Crunch.evaluateExpression("2^3"), "Simple exponent test"); 38 | assertEquals(10, Crunch.evaluateExpression("15 - 5"), "Simple subtraction test"); 39 | assertEquals(2, Crunch.evaluateExpression("1--1"), "Subtraction and negate operator"); 40 | assertEquals(2, Crunch.evaluateExpression(" 1 -- 1"), "Somewhat confusing whitespace"); 41 | assertEquals(5, Crunch.evaluateExpression("10 / 2"), "Asymmetric operator"); 42 | } 43 | 44 | @Test 45 | void complexOperationTest() { 46 | assertEquals(9, Crunch.evaluateExpression("6/2*(1+2)"), "Order of operations"); 47 | assertEquals(5, Crunch.evaluateExpression("6/2*1+2"), "Order of operations 2"); 48 | assertEquals(1, Crunch.evaluateExpression("tan(atan(cos(acos(sin(asin(1))))))"), DELTA, "Trig functions"); 49 | assertEquals(402193.3186140596, Crunch.evaluateExpression("6.5*7.8^2.3 + (3.5^3+7/2)^3 -(5*4/(2-3))*4 + 6.5*7.8^2.3 + (3.5^3+7/2)^3 -(5*4/(2-3))*4 + 6.5*7.8^2.3 + (3.5^3+7/2)^3 -(5*4/(2-3))*4 + 6.5*7.8^2.3 + (3.5^3+7/2)^3 -(5*4/(2-3))*4"), DELTA, "Large expression"); 50 | assertEquals(-5, Crunch.evaluateExpression("1-(2)*3"), DELTA, "Weird syntax"); 51 | assertEquals(1, Crunch.evaluateExpression("--1"), "Adjacent operators"); 52 | } 53 | 54 | @Test 55 | void booleanLogicTest() { 56 | assertEquals(1, Crunch.evaluateExpression("true & true"), "Boolean and"); 57 | assertEquals(1, Crunch.evaluateExpression("true | false"), "Boolean or"); 58 | assertEquals(0, Crunch.evaluateExpression("true & (true & false | false)"), "More complex boolean expression"); 59 | assertEquals(1, Crunch.evaluateExpression("1 = 1 & 3 = 3"), "Arithmetic comparisons"); 60 | assertEquals(1, Crunch.evaluateExpression("1 != 2 & 3 != 4"), "Using !="); 61 | } 62 | 63 | @Test 64 | void syntaxTest() { 65 | assertThrows(ExpressionCompilationException.class, () -> Crunch.compileExpression("("), "Lone opening paren"); 66 | assertThrows(ExpressionCompilationException.class, () -> Crunch.compileExpression(")"), "Lone closing paren"); 67 | assertThrows(ExpressionCompilationException.class, () -> Crunch.compileExpression("1 1"), "No operator"); 68 | assertThrows(ExpressionCompilationException.class, () -> Crunch.compileExpression("+"), "Only operator"); 69 | } 70 | 71 | @Test 72 | void variableTest() { 73 | assertEquals(10, Crunch.evaluateExpression("$1", 10), "Basic variable value"); 74 | assertEquals(14, Crunch.evaluateExpression("$1 - $2", 10, -4), "Multiple variables"); 75 | assertThrows(ExpressionEvaluationException.class, () -> Crunch.evaluateExpression("$1"), "No variable value"); 76 | 77 | ExpressionEnv env = new ExpressionEnv(); 78 | env.setVariableNames("x", "y"); 79 | assertEquals(33, Crunch.compileExpression("x * y", env).evaluate(11, 3), "Multiplying named variables"); 80 | assertThrows(ExpressionEvaluationException.class, () -> Crunch.compileExpression("x * y", env).evaluate(1), "Too few values"); 81 | assertThrows(ExpressionEvaluationException.class, () -> Crunch.compileExpression("x", env).evaluate()); 82 | } 83 | 84 | @Test 85 | void functionTest() { 86 | ExpressionEnv env = new ExpressionEnv(); 87 | env.addFunction("mult", 2, d -> d[0] * d[1]); 88 | env.addFunction("four", 0, d -> 4d); 89 | assertEquals(45, Crunch.compileExpression("mult(15, 3)", env).evaluate(), "Basic function"); 90 | assertEquals(96, Crunch.compileExpression("mult(2, mult(4, mult(3, 4)))", env).evaluate(), "Nested functions"); 91 | assertEquals(4, Crunch.compileExpression("four()", env).evaluate(), "No-argument function"); 92 | assertThrows(ExpressionCompilationException.class, () -> Crunch.compileExpression("mult", env), "No argument list"); 93 | assertThrows(ExpressionCompilationException.class, () -> Crunch.compileExpression("mult(1)", env), "Not enough arguments"); 94 | assertThrows(ExpressionCompilationException.class, () -> Crunch.compileExpression("mult(1, 2, 3)", env), "Too many arguments"); 95 | } 96 | 97 | @Test 98 | void rootingTest() { 99 | assertEquals(2, Crunch.evaluateExpression("sqrt(4)"), "Square Rooting"); 100 | assertEquals(2, Crunch.evaluateExpression("cbrt(8)"), "Cube Rooting"); 101 | } 102 | 103 | @Test 104 | void lazyVariableTest() { 105 | ExpressionEnv env = new ExpressionEnv(); 106 | env.addLazyVariable("x", () -> 2); 107 | env.addLazyVariable("y", () -> 7); 108 | assertEquals(14, Crunch.compileExpression("x*y", env).evaluate()); 109 | assertEquals(3, Crunch.compileExpression("x + 1", env).evaluate()); 110 | } 111 | 112 | @Test 113 | void scientificNotationTest() { 114 | assertEquals(2E7, Crunch.evaluateExpression("2E7"), DELTA); 115 | } 116 | 117 | @Test 118 | void noInlineRandomTest() { 119 | CompiledExpression expr = Crunch.compileExpression("rand1000000"); 120 | assertNotEquals(expr.evaluate(), expr.evaluate()); 121 | } 122 | 123 | @Test 124 | void inlineTest() { 125 | assertEquals("6.0", Crunch.compileExpression("1 + 2 + 3").toString()); 126 | assertEquals("-1.0", Crunch.compileExpression("-1").toString()); 127 | assertEquals("1.0", Crunch.compileExpression("--1").toString()); 128 | } 129 | 130 | @Test 131 | void largeExpressionWithCustomFunctionTest() { 132 | ExpressionEnv env = new ExpressionEnv(); 133 | env.addFunction("max", 2, d -> Math.max(d[0], d[1])); 134 | String expr = "max( 0.0, (378044 * 100 / 100.0 - 294964) * 1.0 ) - 0.0"; 135 | CompiledExpression compiled = Crunch.compileExpression(expr, env); 136 | assertEquals(83080, compiled.evaluate()); 137 | } 138 | 139 | @Test 140 | void cloneTest() { 141 | CompiledExpression expr = Crunch.compileExpression("$1"); 142 | assertEquals(1, expr.evaluate(1)); 143 | assertEquals(2, expr.clone().evaluate(2)); 144 | } 145 | 146 | } 147 | -------------------------------------------------------------------------------- /src/redempt/crunch/ExpressionParser.java: -------------------------------------------------------------------------------- 1 | package redempt.crunch; 2 | 3 | import redempt.crunch.data.FastNumberParsing; 4 | import redempt.crunch.exceptions.ExpressionCompilationException; 5 | import redempt.crunch.functional.ArgumentList; 6 | import redempt.crunch.functional.ExpressionEnv; 7 | import redempt.crunch.functional.Function; 8 | import redempt.crunch.functional.FunctionCall; 9 | import redempt.crunch.token.BinaryOperator; 10 | import redempt.crunch.token.LiteralValue; 11 | import redempt.crunch.token.Token; 12 | import redempt.crunch.token.TokenType; 13 | import redempt.crunch.token.UnaryOperation; 14 | import redempt.crunch.token.UnaryOperator; 15 | import redempt.crunch.token.Value; 16 | 17 | public class ExpressionParser { 18 | 19 | private final String input; 20 | private final ExpressionEnv environment; 21 | private final CompiledExpression expression = new CompiledExpression(); 22 | 23 | private int maxVarIndex; 24 | private int cursor = 0; 25 | 26 | ExpressionParser(String input, ExpressionEnv env) { 27 | if (input == null) { 28 | throw new ExpressionCompilationException(null, "Expression is null"); 29 | } 30 | if (env == null) { 31 | throw new ExpressionCompilationException(null, "Environment is null"); 32 | } 33 | maxVarIndex = env.getVariableCount() - 1; 34 | this.input = input; 35 | this.environment = env; 36 | } 37 | 38 | public char peek() { 39 | return input.charAt(cursor); 40 | } 41 | 42 | public char advance() { 43 | return input.charAt(cursor++); 44 | } 45 | 46 | public void advanceCursor() { 47 | cursor++; 48 | } 49 | 50 | public boolean isAtEnd() { 51 | return cursor >= input.length(); 52 | } 53 | 54 | public int getCursor() { 55 | return cursor; 56 | } 57 | 58 | public void setCursor(int cursor) { 59 | this.cursor = cursor; 60 | } 61 | 62 | public String getInput() { 63 | return input; 64 | } 65 | 66 | public void expectChar(char c) { 67 | if (isAtEnd() || advance() != c) { 68 | throw new ExpressionCompilationException(this, "Expected '" + c + "'"); 69 | } 70 | } 71 | 72 | private void error(String msg) { 73 | throw new ExpressionCompilationException(this, msg); 74 | } 75 | 76 | private boolean whitespace() { 77 | while (!isAtEnd() && Character.isWhitespace(peek())) { 78 | cursor++; 79 | } 80 | return true; 81 | } 82 | 83 | private Value parseExpression() { 84 | if (isAtEnd()) { 85 | error("Expected expression"); 86 | } 87 | Value first = parseTerm(); 88 | if (isAtEnd() || peek() == ')' || peek() == ',') { 89 | return first; 90 | } 91 | ShuntingYard tokens = new ShuntingYard(); 92 | tokens.addValue(first); 93 | while (whitespace() && !isAtEnd() && peek() != ')' && peek() != ',') { 94 | BinaryOperator token = environment.getBinaryOperators().getWith(this); 95 | if (token == null) { 96 | error("Expected binary operator"); 97 | } 98 | tokens.addOperator(token); 99 | whitespace(); 100 | tokens.addValue(parseTerm()); 101 | } 102 | return tokens.finish(); 103 | } 104 | 105 | private Value parseNestedExpression() { 106 | expectChar('('); 107 | whitespace(); 108 | Value expression = parseExpression(); 109 | expectChar(')'); 110 | return expression; 111 | } 112 | 113 | private Value parseAnonymousVariable() { 114 | expectChar('$'); 115 | double value = parseLiteral().getValue(new double[0]); 116 | if (value % 1 != 0) { 117 | error("Decimal variable indices are not allowed"); 118 | } 119 | if (value < 1) { 120 | error("Zero and negative variable indices are not allowed"); 121 | } 122 | int index = (int) value - 1; 123 | maxVarIndex = Math.max(index, maxVarIndex); 124 | return new Variable(index); 125 | } 126 | 127 | private Value parseTerm() { 128 | switch (peek()) { 129 | case '0': 130 | case '1': 131 | case '2': 132 | case '3': 133 | case '4': 134 | case '5': 135 | case '6': 136 | case '7': 137 | case '8': 138 | case '9': 139 | case '.': 140 | return parseLiteral(); 141 | case '(': 142 | return parseNestedExpression(); 143 | case '$': 144 | return parseAnonymousVariable(); 145 | default: 146 | break; // Ignore 147 | } 148 | 149 | Token leadingOperator = environment.getLeadingOperators().getWith(this); 150 | if (leadingOperator != null) { 151 | return parseLeadingOperation(leadingOperator); 152 | } 153 | Value term = environment.getValues().getWith(this); 154 | if (term == null) { 155 | error("Expected value"); 156 | } 157 | return term; 158 | } 159 | 160 | private LiteralValue parseLiteral() { 161 | int start = cursor; 162 | char c; 163 | while (Character.isDigit(c = peek()) || c == '.') { 164 | advanceCursor(); 165 | if (isAtEnd()) { 166 | break; 167 | } 168 | } 169 | return new LiteralValue(FastNumberParsing.parseDouble(input, start, cursor)); 170 | } 171 | 172 | private Value parseLeadingOperation(Token token) { 173 | whitespace(); 174 | switch (token.getType()) { 175 | case UNARY_OPERATOR: 176 | UnaryOperator op = (UnaryOperator) token; 177 | Value term = parseTerm(); 178 | if (op.isPure() && term.getType() == TokenType.LITERAL_VALUE) { 179 | return new LiteralValue(op.getOperation().applyAsDouble(term.getValue(new double[0]))); 180 | } 181 | return new UnaryOperation(op, term); 182 | case FUNCTION: 183 | Function function = (Function) token; 184 | ArgumentList args = parseArgumentList(function.getArgCount()); 185 | return new FunctionCall(function, args.getArguments()); 186 | } 187 | error("Expected leading operation"); 188 | return null; 189 | } 190 | 191 | private ArgumentList parseArgumentList(int args) { 192 | expectChar('('); 193 | whitespace(); 194 | Value[] values = new Value[args]; 195 | if (args == 0) { 196 | expectChar(')'); 197 | return new ArgumentList(new Value[0]); 198 | } 199 | values[0] = parseExpression(); 200 | whitespace(); 201 | for (int i = 1; i < args; i++) { 202 | expectChar(','); 203 | whitespace(); 204 | values[i] = parseExpression(); 205 | whitespace(); 206 | } 207 | 208 | expectChar(')'); 209 | return new ArgumentList(values); 210 | } 211 | 212 | public CompiledExpression parse() { 213 | whitespace(); 214 | Value value = parseExpression(); 215 | whitespace(); 216 | if (!isAtEnd()) { 217 | error("Dangling term"); 218 | } 219 | expression.initialize(value, maxVarIndex + 1); 220 | return expression; 221 | } 222 | 223 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Crunch 2 | The fastest Java expression compiler/evaluator 3 | 4 | Support Discord: https://discord.gg/agu5xGy2YZ 5 | 6 | # Installation for Development 7 | 8 | Crunch can be accessed via my build server using Gradle or Maven. Read the section for whichever you use below. 9 | 10 | ## Gradle: 11 | 12 | ``` 13 | repositories { 14 | maven { url 'https://redempt.dev' } 15 | } 16 | 17 | ``` 18 | 19 | ``` 20 | dependencies { 21 | implementation 'com.github.Redempt:Crunch:Tag' 22 | } 23 | ``` 24 | 25 | Replace `Tag` with a version, like `1.0`. 26 | 27 | ## Maven: 28 | 29 | ``` 30 | 31 | redempt.dev 32 | https://redempt.dev 33 | 34 | ``` 35 | 36 | ``` 37 | 38 | com.github.Redempt 39 | Crunch 40 | Tag 41 | 42 | ``` 43 | 44 | Replace `Tag` with a version, like `1.0`. 45 | 46 | # Usage 47 | 48 | Crunch offers a better solution to evaluating complex mathematical expressions than using ScriptManager. It is simple to use, performant, and lightweight. 49 | 50 | There are only a handful of methods you will need to use in Crunch. To compile an expression, simply call `Crunch#compileExpression`. Here's an example: 51 | 52 | ```java 53 | CompiledExpression exp = Crunch.compileExpression("1 + 1"); 54 | exp.evaluate(); //This will return 2 55 | ``` 56 | 57 | You can use all the basic operations you're familiar with. If you want to see a list of all supported operations, check the [Operator](https://github.com/Redempt/Crunch/blob/master/src/redempt/crunch/Operator.java) enum, or the Operations section below. 58 | 59 | Variables can also be used with Crunch. They must be numbered, starting with 1, and preceded by a `$`. This is part of what makes Crunch so performant. If you need named variables, however, you can specify names for them with an `EvaluationEnvironment`. When calling `evaluate` on a CompiledExpression with variables, you must pass them in order of index. 60 | 61 | ```java 62 | CompiledExpression exp = Crunch.compileExpression("$1 / $2"); 63 | exp.evaluate(27, 3); //This will return 9 64 | ``` 65 | 66 | Spaces are ignored entirely, so if you don't feel the need to add them, you may remove them. 67 | 68 | You can also define your own functions fairly simply: 69 | 70 | ```java 71 | EvaluationEnvironment env = new EvaluationEnvironment(); 72 | // name # args lambda to do logic 73 | env.addFunction("mult", 2, (d) -> d[0] * d[1]); 74 | CompiledExpression exp = Crunch.compileExpression("mult(2, 3)", env); 75 | exp.evaluate(); //This will return 6 76 | ``` 77 | 78 | With an EvaluationEnvironment, you're also able to specify names for your variables: 79 | 80 | ```java 81 | EvaluationEnvironment env = new EvaluationEnvironment(); 82 | env.setVariableNames("x", "y"); 83 | CompiledExpression exp = Crunch.compileExpression("x - y", env); 84 | exp.evaluate(3, 4); //This will return -1 85 | ``` 86 | 87 | The values for the variables must be passed in the same order that you passed the variable names in. 88 | 89 | You're also able to define lazy variables, which don't need to be passed as arguments to `evaluate`: 90 | 91 | ```java 92 | EvaluationEnvironment env = new EvaluationEnvironment(); 93 | env.addLazyVariable("x", () -> 4); 94 | CompiledExpression exp = Crunch.compileExpression("x + 1", env); 95 | exp.evaluate(); //This will return 5 96 | ``` 97 | 98 | In the case that you only need to evaluate an expression once and never again, you can use `Crunch#evaluateExpression`: 99 | 100 | ```java 101 | int exampleVar = 50; 102 | Crunch.evaluateExpression("abs(3 - $1)", exampleVar); 103 | ``` 104 | 105 | However, if the expression will be used more than once, it is highly recommended to keep it as a `CompiledExpression` instead. 106 | 107 | CompiledExpressions are NOT thread-safe, and may have issues if `evaluate` is called from multiple threads at the same time. For multi-threaded purposes, please mutex your CompiledExpression or clone it with `CompiledExpression#clone` and pass it off to another thread. 108 | 109 | # Performance 110 | 111 | Performance is one of the largest benefits of using Crunch. It is designed to be extremely performant, and lives up to that expectation. For cases where you need to perform a lot of evaluations quickly from a string-compiled mathematical expression, Crunch is the best option. 112 | 113 | Here I will compare the runtimes of Crunch against two similar librararies: [EvalEx](https://github.com/uklimaschewski/EvalEx) and [exp4j](https://github.com/fasseg/exp4j). I will compare both compilation times and evaluation times. 114 | 115 | CPU: AMD Ryzen 7 5800X 116 | 117 | Benchmark source: https://github.com/Redempt/CrunchBenchmark 118 | 119 | ## Compilation 120 | Simple expression: `3*5` 121 | Complex expression: `6.5*7.8^2.3 + (3.5^3+7/2)^3 -(5*4/(2-3))*4 + 6.5*7.8^2.3 + (3.5^3+7/2)^3 -(5*4/(2-3))*4 + 6.5*7.8^2.3 + (3.5^3+7/2)^3 -(5*4/(2-3))*4 + 6.5*7.8^2.3 + (3.5^3+7/2)^3 -(5*4/(2-3))*4` 122 | ``` 123 | Benchmark Mode Score Error Units 124 | CompileBenchmark.crunchCompileComplexExpression avgt 2.974 ± 0.033 us/op 125 | CompileBenchmark.crunchCompileSimpleExpression avgt 0.050 ± 0.001 us/op 126 | CompileBenchmark.evalExCompileComplexExpression avgt 38.450 ± 0.526 us/op 127 | CompileBenchmark.evalExCompileSimpleExpression avgt 9.156 ± 0.256 us/op 128 | CompileBenchmark.exp4jCompileComplexExpression avgt 3.464 ± 0.026 us/op 129 | CompileBenchmark.exp4jCompileSimpleExpression avgt 0.276 ± 0.009 us/op 130 | ``` 131 | 132 | ## Evaluation 133 | 134 | Simple expression: `(10*x)+5/2` 135 | Constant expression: `6.5*7.8^2.3 + (3.5^3+7/2)^3 -(5*4/(2-3))*4 + 6.5*7.8^2.3 + (3.5^3+7/2)^3 -(5*4/(2-3))*4 + 6.5*7.8^2.3 + (3.5^3+7/2)^3 -(5*4/(2-3))*4 + 6.5*7.8^2.3 + (3.5^3+7/2)^3 -(5*4/(2-3))*4` 136 | 137 | ``` 138 | Benchmark Mode Score Error Units 139 | EvalBenchmark.crunchConstantEval avgt 0.823 ± 0.020 ns/op 140 | EvalBenchmark.crunchSimpleEval avgt 4.296 ± 0.058 ns/op 141 | EvalBenchmark.evalExConstantEval avgt 26156.342 ± 183.188 ns/op 142 | EvalBenchmark.evalExSimpleEval avgt 2283.572 ± 19.630 ns/op 143 | EvalBenchmark.exp4jConstantEval avgt 540.194 ± 4.434 ns/op 144 | EvalBenchmark.exp4jSimpleEval avgt 44.727 ± 0.554 ns/op 145 | ``` 146 | 147 | # Operations and Syntax 148 | 149 | `()` - Create a parenthetical expression which will be evaluated first (`3 * (4 + 1)`) 150 | 151 | `$` - Denotes a variable (`$1 / 3`) 152 | 153 | `e` - Euler's constant (`log(e)`) 154 | 155 | `pi` - pi (`sin(pi)`) 156 | 157 | `+` - Add two numbers (`1 + 1`) 158 | 159 | `-` - Subtract two numbers, or negate one (`3-2`, `-(4+2)`) 160 | 161 | `/` - Divide two numbers (`3 / 4`) 162 | 163 | `*` - Multiply two numbers (`2 * 3`) 164 | 165 | `^` - Raise one number to the power of another (`3^3`) 166 | 167 | `%` - Take the modulus, or division remainder, of one number with another (`7 % 4`) 168 | 169 | `abs` - Take the absolute value of a number (`abs$1`, `abs-1`) 170 | 171 | `round` - Rounds a number to the nearest integer (`round1.5`, `round(2.3)`) 172 | 173 | `ceil` - Rounds a number up to the nearest integer (`ceil1.05`) 174 | 175 | `floor` - Rounds a number down to the nearest integer (`floor0.95`) 176 | 177 | `rand` - Generate a random number between 0 and the specified upper bound (`rand4`) 178 | 179 | `log` - Get the natural logarithm of a number (`log(e)`) 180 | 181 | `sqrt` - Get the square root of a number (`sqrt4`) 182 | 183 | `cbrt` - Get the cube root of a number (`cbrt(8)`) 184 | 185 | `sin` - Get the sine of a number (`sin$2`) 186 | 187 | `cos` - Get the cosine of a number (`cos(2*pi)`) 188 | 189 | `tan` - Get the tangent of a number (`tanpi`) 190 | 191 | `asin` - Get the arcsine of a number (`asin$2`) 192 | 193 | `acos` - Get the arccosine of a number (`acos0.45`) 194 | 195 | `atan` - Get the arctangent of a number (`atan1`) 196 | 197 | `sinh` - Get the hyperbolic sine of a number (`sinh(4)`) 198 | 199 | `cosh` - Get the hyperbolic cosine of a number (`sinh(4)`) 200 | 201 | `true` - Boolean constant representing 1 202 | 203 | `false` - Boolean constant representing 0 204 | 205 | `=` - Compare if two numbers are equal (`1 = 1` will be `1`, `1 = 3` will be `0`), also accepts `==` 206 | 207 | `!=` - Compare if two numbers are not equal (`1 != 2` will be `1`, `1 != 1` will be `0`) 208 | 209 | `>` - Compare if one number is greater than another (`1 > 0`) 210 | 211 | `<` - Compare if one number is less than another (`0 < 1`) 212 | 213 | `>=` - Compare if one number is greater than or equal to another (`1 >= 1`) 214 | 215 | `<=` - Compare if one number is less than or equal to another (`0 <= 1`) 216 | 217 | `|` - Boolean or (`true | false`), also accepts `||` 218 | 219 | `&` - Boolean and (`true & true`), also accepts `&&` 220 | 221 | `!` - Boolean not/inverse (`!true`) 222 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Use "xargs" to parse quoted args. 209 | # 210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 211 | # 212 | # In Bash we could simply go: 213 | # 214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 215 | # set -- "${ARGS[@]}" "$@" 216 | # 217 | # but POSIX shell has neither arrays nor command substitution, so instead we 218 | # post-process each arg (as a line of input to sed) to backslash-escape any 219 | # character that might be a shell metacharacter, then use eval to reverse 220 | # that process (while maintaining the separation between arguments), and wrap 221 | # the whole thing up as a single "set" statement. 222 | # 223 | # This will of course break if any of these variables contains a newline or 224 | # an unmatched quote. 225 | # 226 | 227 | eval "set -- $( 228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 229 | xargs -n1 | 230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 231 | tr '\n' ' ' 232 | )" '"$@"' 233 | 234 | exec "$JAVACMD" "$@" 235 | --------------------------------------------------------------------------------