Skip to content

Instantly share code, notes, and snippets.

@dodgex
Created July 2, 2018 06:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dodgex/ed83688b29669ce82a95b492d1c3eefa to your computer and use it in GitHub Desktop.
Save dodgex/ed83688b29669ce82a95b492d1c3eefa to your computer and use it in GitHub Desktop.
import java.math.BigDecimal;
import java.math.RoundingMode;
/**
* Based on: https://stackoverflow.com/a/26227947/1659588
*/
public class Parser {
private static final BigDecimal NEGATIVE_ONE = new BigDecimal("-1");
private final String str;
private final ArgumentRetriver argRetriver;
private final int scale = 5;
private int pos = -1;
private int ch;
public Parser(String str) {
this(str, null);
}
public Parser(String str, ArgumentRetriver argRetriver) {
this.str = str;
this.argRetriver = argRetriver;
}
public BigDecimal parse() {
nextChar();
Expression x = parseExpression();
if (pos < str.length()) throw new RuntimeException("Unexpected: " + (char) ch);
return x.eval();
}
private void nextChar() {
ch = (++pos < str.length()) ? str.charAt(pos) : -1;
}
private boolean eat(int charToEat) {
while (isWhitespace()) nextChar();
if (ch == charToEat) {
nextChar();
return true;
}
return false;
}
private boolean isWhitespace() {
return ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n';
}
private Expression parseExpression() {
Expression x = parseTerm();
for (; ; ) {
if (eat('+')) { // addition
Expression left = x, right = parseTerm();
x = () -> left.eval().add(right.eval());
} else if (eat('-')) { // subtraction
Expression left = x, right = parseTerm();
x = () -> left.eval().subtract(right.eval());
} else {
return x;
}
}
}
private Expression parseTerm() {
Expression x = parseFactor();
for (; ; ) {
if (eat('*')) {
Expression left = x, right = parseTerm();
x = () -> left.eval().multiply(right.eval());
} else if (eat('/')) {
Expression left = x, right = parseTerm();
x = () -> left.eval().divide(right.eval(), scale, RoundingMode.HALF_EVEN)
.stripTrailingZeros()
;
} else {
return x;
}
}
}
private Expression parseArgument() {
if (argRetriver == null) {
throw new IllegalStateException("provide argument retriver");
}
if (!eat('(')) {
throw new IllegalStateException("'(' expected");
}
int startPos = this.pos;
while (ch != ')') nextChar();
String argument = str.substring(startPos, this.pos);
if (!argument.contains(":")) {
throw new IllegalStateException("expected ':' missing in argument: " + argument);
}
String[] split = argument.split(":", 2);
BigDecimal value = argRetriver.getArgumentValue(split[0], split[1]);
if (value == null) {
throw new IllegalStateException("no value available for argument: " + argument);
}
BigDecimal result = scaledValue(value, scale);
eat(')');
return () -> result;
}
private Expression parseFactor() {
if (eat('+')) {
return parseFactor(); // unary plus
}
if (eat('-')) {
return parseFactor().negate(); // unary minus
}
Expression x;
int startPos = this.pos;
if (eat('(')) { // parentheses
x = parseExpression();
eat(')');
} else if ((ch >= '0' && ch <= '9') || ch == '.') { // numbers
while ((ch >= '0' && ch <= '9') || ch == '.') nextChar();
x = fromString(str.substring(startPos, this.pos), scale);
} else if (ch >= 'a' && ch <= 'z') { // functions
while (ch >= 'a' && ch <= 'z') nextChar();
String func = str.substring(startPos, this.pos);
// x = parseArgument();
if (func.equals("cell")) x = parseArgument();
// else if (func.equals("sin")) x = Math.sin(Math.toRadians(x));
// else if (func.equals("cos")) x = Math.cos(Math.toRadians(x));
// else if (func.equals("tan")) x = Math.tan(Math.toRadians(x));
else throw new RuntimeException("Unknown function: " + func);
} else {
throw new RuntimeException("Unexpected: " + (char) ch);
}
return x;
}
private Expression fromString(String val, int minScale) {
BigDecimal result = scaledValue(val, minScale);
return () -> result;
}
private BigDecimal scaledValue(String val, int minScale) {
BigDecimal result = new BigDecimal(val);
return scaledValue(result, minScale);
}
private BigDecimal scaledValue(BigDecimal value, int minScale) {
if (value.scale() < minScale) {
value = value.setScale(minScale, RoundingMode.HALF_EVEN);
}
return value.stripTrailingZeros();
}
@FunctionalInterface
public interface ArgumentRetriver {
BigDecimal getArgumentValue(String row, String col);
}
@FunctionalInterface
private interface Expression {
BigDecimal eval();
default Expression negate() {
return () -> eval().multiply(NEGATIVE_ONE);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment