/*
* Copyright (C) 2022 NotEnoughUpdates contributors
*
* This file is part of NotEnoughUpdates.
*
* NotEnoughUpdates is free software: you can redistribute it
* and/or modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation, either
* version 3 of the License, or (at your option) any later version.
*
* NotEnoughUpdates is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with NotEnoughUpdates. If not, see .
*/
package io.github.moulberry.notenoughupdates.util;
import io.github.moulberry.notenoughupdates.NotEnoughUpdates;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.DecimalFormat;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
import java.util.Locale;
import java.util.NoSuchElementException;
import java.util.Optional;
public class Calculator {
public interface VariableProvider {
Optional provideVariable(String name) throws CalculatorException;
}
public static DecimalFormat getDecimalFormat() {
StringBuilder f = new StringBuilder("#,##0.");
for (int i = 0; i < NotEnoughUpdates.INSTANCE.config.misc.calculationPrecision; i++) {
f.append("#");
}
return new DecimalFormat(f.toString());
}
public static BigDecimal calculate(String source, VariableProvider variables) throws CalculatorException {
return evaluate(variables, shuntingYard(lex(source)));
}
public static BigDecimal calculate(String source) throws CalculatorException {
return calculate(source, (ignored) -> Optional.empty());
}
///
public enum TokenType {
NUMBER, BINOP, LPAREN, RPAREN, POSTOP, PREOP, VARIABLE
}
public static class Token {
public TokenType type;
String operatorValue;
long numericValue;
int exponent;
int tokenStart;
int tokenLength;
}
static String binops = "+-*/^x";
static String postops = "mkbts%";
static String digits = "0123456789";
static String nameCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ_";
static void readDigitsInto(Token token, String source, boolean decimals) {
int startIndex = token.tokenStart + token.tokenLength;
for (int j = 0; j + startIndex < source.length(); j++) {
char d = source.charAt(j + startIndex);
int d0 = digits.indexOf(d);
if (d0 != -1) {
if (decimals)
token.exponent--;
token.numericValue *= 10;
token.numericValue += d0;
token.tokenLength += 1;
} else {
return;
}
}
}
public static class CalculatorException extends Exception {
int offset, length;
public CalculatorException(String message, int offset, int length) {
super(message);
this.offset = offset;
this.length = length;
}
public int getLength() {
return length;
}
public int getOffset() {
return offset;
}
}
public static List lex(String source) throws CalculatorException {
List tokens = new ArrayList<>();
boolean doesNotHaveLValue = true;
for (int i = 0; i < source.length(); ) {
char c = source.charAt(i);
if (Character.isWhitespace(c)) {
i++;
continue;
}
Token token = new Token();
token.tokenStart = i;
if (doesNotHaveLValue && c == '-') {
token.tokenLength = 1;
token.type = TokenType.PREOP;
token.operatorValue = "-";
} else if (binops.indexOf(c) != -1) {
token.tokenLength = 1;
token.type = TokenType.BINOP;
token.operatorValue = String.valueOf(c);
if (c == '*' && i + 1 < source.length() && source.charAt(i + 1) == '*') {
token.tokenLength++;
token.operatorValue = "^";
}
} else if (postops.indexOf(c) != -1) {
token.tokenLength = 1;
token.type = TokenType.POSTOP;
token.operatorValue = String.valueOf(c).toLowerCase(Locale.ROOT);
} else if (c == ')') {
token.tokenLength = 1;
token.type = TokenType.RPAREN;
token.operatorValue = ")";
} else if (c == '(') {
token.tokenLength = 1;
token.type = TokenType.LPAREN;
token.operatorValue = "(";
} else if ('.' == c || ',' == c) {
token.tokenLength = 1;
token.type = TokenType.NUMBER;
readDigitsInto(token, source, true);
if (token.tokenLength == 1) {
throw new CalculatorException("Invalid number literal", i, 1);
}
} else if ('$' == c) {
token.tokenLength = 1;
token.type = TokenType.VARIABLE;
token.operatorValue = "";
boolean inParenthesis = false;
if (i + 1 < source.length() && source.charAt(i + 1) == '{') {
token.tokenLength++;
inParenthesis = true;
}
for (int j = token.tokenStart + token.tokenLength; j < source.length(); j++) {
char d = source.charAt(j);
if (inParenthesis) {
if (d == '}') {
token.tokenLength++;
inParenthesis = false;
break;
}
} else if (nameCharacters.indexOf(d) == -1) break;
token.operatorValue += d;
token.tokenLength++;
}
if (token.operatorValue.length() == 0 || inParenthesis) {
throw new CalculatorException("Unterminated variable literal", token.tokenStart, token.tokenLength);
}
} else if (digits.indexOf(c) != -1) {
token.type = TokenType.NUMBER;
readDigitsInto(token, source, false);
if (i + token.tokenLength < source.length()) {
char p = source.charAt(i + token.tokenLength);
if ('.' == p || ',' == p) {
token.tokenLength++;
readDigitsInto(token, source, true);
}
}
} else {
throw new CalculatorException("Unknown thing " + c, i, 1);
}
doesNotHaveLValue =
token.type == TokenType.LPAREN || token.type == TokenType.PREOP || token.type == TokenType.BINOP;
tokens.add(token);
i += token.tokenLength;
}
return tokens;
}
///
///
static int getPrecedence(Token token) throws CalculatorException {
switch (token.operatorValue.intern()) {
case "+":
case "-":
return 0;
case "*":
case "/":
case "x":
return 1;
case "^":
return 2;
}
throw new CalculatorException("Unknown operator " + token.operatorValue, token.tokenStart, token.tokenLength);
}
public static List shuntingYard(List toShunt) throws CalculatorException {
// IT'S SHUNTING TIME
// This is an implementation of the shunting yard algorithm
Deque op = new ArrayDeque<>();
List out = new ArrayList<>();
for (Token currentlyShunting : toShunt) {
switch (currentlyShunting.type) {
case NUMBER:
case VARIABLE:
out.add(currentlyShunting);
break;
case BINOP:
int p = getPrecedence(currentlyShunting);
while (!op.isEmpty()) {
Token l = op.peek();
if (l.type == TokenType.LPAREN)
break;
assert (l.type == TokenType.BINOP || l.type == TokenType.PREOP);
int pl = getPrecedence(l);
if (pl >= p) { // Association order
out.add(op.pop());
} else {
break;
}
}
op.push(currentlyShunting);
break;
case PREOP:
op.push(currentlyShunting);
break;
case LPAREN:
op.push(currentlyShunting);
break;
case RPAREN:
while (1 > 0) {
if (op.isEmpty())
throw new CalculatorException(
"Unbalanced right parenthesis",
currentlyShunting.tokenStart,
currentlyShunting.tokenLength
);
Token l = op.pop();
if (l.type == TokenType.LPAREN) {
break;
}
out.add(l);
}
break;
case POSTOP:
out.add(currentlyShunting);
break;
}
}
while (!op.isEmpty()) {
Token l = op.pop();
if (l.type == TokenType.LPAREN)
throw new CalculatorException("Unbalanced left parenthesis", l.tokenStart, l.tokenLength);
out.add(l);
}
return out;
}
///
///
public static BigDecimal evaluate(VariableProvider provider, List rpnTokens) throws CalculatorException {
Deque values = new ArrayDeque<>();
int precision = NotEnoughUpdates.INSTANCE != null ? NotEnoughUpdates.INSTANCE.config.misc.calculationPrecision : 5;
try {
for (Token command : rpnTokens) {
switch (command.type) {
case VARIABLE:
values.push(provider.provideVariable(command.operatorValue)
.orElseThrow(() -> new CalculatorException(
"Unknown variable " + command.operatorValue,
command.tokenStart,
command.tokenLength
)));
break;
case PREOP:
values.push(values.pop().negate());
break;
case NUMBER:
values.push(new BigDecimal(command.numericValue).scaleByPowerOfTen(command.exponent));
break;
case BINOP:
BigDecimal right = values.pop().setScale(precision, RoundingMode.HALF_UP);
BigDecimal left = values.pop().setScale(precision, RoundingMode.HALF_UP);
switch (command.operatorValue.intern()) {
case "^":
if (right.compareTo(new BigDecimal(1000)) >= 0) {
Token rightToken = rpnTokens.get(rpnTokens.indexOf(command) - 1);
throw new CalculatorException(
right + " is too large, pick a power less than 1000",
rightToken.tokenStart,
rightToken.tokenLength
);
}
if (right.doubleValue() != right.intValue()) {
Token rightToken = rpnTokens.get(rpnTokens.indexOf(command) - 1);
throw new CalculatorException(
right + " has a decimal, pick a power that is non-decimal",
rightToken.tokenStart,
rightToken.tokenLength
);
}
if (right.doubleValue() < 0) {
Token rightToken = rpnTokens.get(rpnTokens.indexOf(command) - 1);
throw new CalculatorException(
right + " is a negative number, pick a power that is positive",
rightToken.tokenStart,
rightToken.tokenLength
);
}
values.push(left.pow(right.intValue()).setScale(precision, RoundingMode.HALF_UP));
break;
case "x":
case "*":
values.push(left.multiply(right).setScale(precision, RoundingMode.HALF_UP));
break;
case "/":
try {
values.push(left.divide(right, RoundingMode.HALF_UP).setScale(precision, RoundingMode.HALF_UP));
} catch (ArithmeticException e) {
throw new CalculatorException("Encountered division by 0", command.tokenStart, command.tokenLength);
}
break;
case "+":
values.push(left.add(right).setScale(precision, RoundingMode.HALF_UP));
break;
case "-":
values.push(left.subtract(right).setScale(precision, RoundingMode.HALF_UP));
break;
default:
throw new CalculatorException(
"Unknown operation " + command.operatorValue,
command.tokenStart,
command.tokenLength
);
}
break;
case LPAREN:
case RPAREN:
throw new CalculatorException(
"Did not expect unshunted token in RPN",
command.tokenStart,
command.tokenLength
);
case POSTOP:
BigDecimal p = values.pop();
switch (command.operatorValue.intern()) {
case "s":
values.push(p.multiply(new BigDecimal(64)).setScale(precision, RoundingMode.HALF_UP));
break;
case "k":
values.push(p.multiply(new BigDecimal(1_000)).setScale(precision, RoundingMode.HALF_UP));
break;
case "m":
values.push(p.multiply(new BigDecimal(1_000_000)).setScale(precision, RoundingMode.HALF_UP));
break;
case "b":
values.push(p.multiply(new BigDecimal(1_000_000_000)).setScale(precision, RoundingMode.HALF_UP));
break;
case "t":
values.push(p.multiply(new BigDecimal("1000000000000")).setScale(precision, RoundingMode.HALF_UP));
break;
case "%":
values.push(p
.setScale(precision + 1, RoundingMode.HALF_UP)
.divide(new BigDecimal(100), RoundingMode.HALF_UP)
.setScale(precision, RoundingMode.HALF_UP));
break;
default:
throw new CalculatorException(
"Unknown operation " + command.operatorValue,
command.tokenStart,
command.tokenLength
);
}
break;
}
}
BigDecimal peek = values.pop();
return peek.stripTrailingZeros();
} catch (NoSuchElementException e) {
throw new CalculatorException("Unfinished expression", 0, 0);
}
}
///
}