Allow for math expressions in GuiCraftAmount textbox (#353)

This commit is contained in:
Serenibyss
2024-01-06 20:55:22 -06:00
committed by GitHub
parent b252d1c891
commit 0b7341e4a8
3 changed files with 295 additions and 49 deletions
@@ -0,0 +1,208 @@
package appeng.client.gui;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Collections;
import java.util.Stack;
public class MathExpressionParser {
private final Stack<String> postfixStack = new Stack<>();
private final Stack<Character> opStack = new Stack<>();
private static final int[] OPERATOR_PRIORITY = new int[] { 0, 3, 2, 1, -1, 1, 0, 2 };
public static double parse(String expression) {
double result;
if (expression == null) return Double.NaN;
expression = expression.replace(" ", "");
if (expression.length() == 1 && Character.isDigit(expression.charAt(0))) {
return expression.charAt(0) - '0';
}
try {
expression = transform(expression);
result = new MathExpressionParser().calculate(expression);
} catch (Exception e) {
return Double.NaN;
}
return result;
}
/**
* replace '-' with '~'
* e.g.-2+-1*(-3E-2)-(-1) -> ~2+~1*(~3E~2)-(~1)
*/
private static String transform(String expression) {
char[] arr = expression.toCharArray();
for (int i = 0; i < arr.length; i++) {
if (arr[i] == '-') {
if (i == 0) {
arr[i] = '~';
} else {
char c = arr[i - 1];
if (c == '+' || c == '-' || c == '*' || c == '/' || c == '(' || c == 'E' || c == 'e') {
arr[i] = '~';
}
}
}
}
if (arr[0] == '~' || arr[1] == '(') {
arr[0] = '-';
return "0" + new String(arr);
} else {
return new String(arr);
}
}
public double calculate(String expression) {
Stack<String> resultStack = new Stack<>();
prepare(expression);
Collections.reverse(postfixStack);
String firstValue, secondValue, currentValue;
while (!postfixStack.isEmpty()) {
currentValue = postfixStack.pop();
if (!isOperator(currentValue.charAt(0))) {
currentValue = currentValue.replace("~", "-");
resultStack.push(currentValue);
} else {
secondValue = resultStack.pop();
firstValue = resultStack.pop();
firstValue = firstValue.replace("~", "-");
secondValue = secondValue.replace("~", "-");
String tempResult = calculate(firstValue, secondValue, currentValue.charAt(0));
resultStack.push(String.valueOf(tempResult));
}
}
return Double.parseDouble(resultStack.pop());
}
private void prepare(String expression) {
opStack.push(',');
char[] arr = expression.toCharArray();
int currentIndex = 0;
int count = 0;
char currentOp, peekOp;
for (int i = 0; i < arr.length; i++) {
currentOp = arr[i];
if (isOperator(currentOp)) {
if (count > 0) {
postfixStack.push(new String(arr, currentIndex, count));
}
peekOp = opStack.peek();
if (currentOp == ')') {
while (opStack.peek() != '(') {
postfixStack.push(String.valueOf(opStack.pop()));
}
opStack.pop();
} else {
while (currentOp != '(' && peekOp != ',' && compare(currentOp, peekOp)) {
postfixStack.push(String.valueOf(opStack.pop()));
peekOp = opStack.peek();
}
opStack.push(currentOp);
}
count = 0;
currentIndex = i + 1;
} else {
count++;
}
}
if (count > 1 || (count == 1 && !isOperator(arr[currentIndex]))) {
postfixStack.push(new String(arr, currentIndex, count));
}
while (opStack.peek() != ',') {
postfixStack.push(String.valueOf(opStack.pop()));
}
}
private static boolean isOperator(char c) {
return c == '+' || c == '-' || c == '*' || c == '/' || c == '(' || c == ')';
}
private static boolean compare(char cur, char peek) {
return OPERATOR_PRIORITY[(peek) - 40] >= OPERATOR_PRIORITY[(cur) - 40];
}
private String calculate(String firstValue, String secondValue, char currentOp) {
return switch (currentOp) {
case '+' -> add(firstValue, secondValue);
case '-' -> sub(firstValue, secondValue);
case '*' -> mul(firstValue, secondValue);
case '/' -> div(firstValue, secondValue);
default -> "";
};
}
public static String add(String v1, String v2) {
BigDecimal b1 = new BigDecimal(v1);
BigDecimal b2 = new BigDecimal(v2);
return String.valueOf(b1.add(b2));
}
/**
* subtraction
*
* @param v1 p1
* @param v2 p2
* @return sub
*/
public static String sub(String v1, String v2) {
BigDecimal b1 = new BigDecimal(v1);
BigDecimal b2 = new BigDecimal(v2);
return String.valueOf(b1.subtract(b2));
}
/**
* multiplication
*
* @param v1 p1
* @param v2 p2
* @return mul
*/
public static String mul(String v1, String v2) {
BigDecimal b1 = new BigDecimal(v1);
BigDecimal b2 = new BigDecimal(v2);
return String.valueOf(b1.multiply(b2));
}
/**
* division. e = 10^-10
*
* @param v1 p1
* @param v2 p2
* @return div
*/
public static String div(String v1, String v2) {
BigDecimal b1 = new BigDecimal(v1);
BigDecimal b2 = new BigDecimal(v2);
return String.valueOf(b1.divide(b2, 16, RoundingMode.HALF_UP));
}
/**
* rounding
*
* @param v p
* @param scale scale
* @return result
*/
public static double round(double v, int scale) {
if (scale < 0) {
throw new IllegalArgumentException("The scale must be a positive integer or zero");
}
BigDecimal b = new BigDecimal(Double.toString(v));
return b.divide(BigDecimal.ONE, scale, RoundingMode.HALF_UP).doubleValue();
}
public static String round(String v, int scale) {
if (scale < 0) {
throw new IllegalArgumentException("The scale must be a positive integer or zero");
}
BigDecimal b = new BigDecimal(v);
return String.valueOf(b.divide(BigDecimal.ONE, scale, RoundingMode.HALF_UP));
}
}
@@ -22,10 +22,9 @@ package appeng.client.gui.implementations;
import appeng.api.AEApi;
import appeng.api.definitions.IDefinitions;
import appeng.api.definitions.IParts;
import appeng.api.features.IWirelessTermHandler;
import appeng.api.storage.ITerminalHost;
import appeng.client.gui.AEBaseGui;
import appeng.client.gui.widgets.GuiNumberBox;
import appeng.client.gui.MathExpressionParser;
import appeng.client.gui.widgets.GuiTabButton;
import appeng.container.AEBaseContainer;
import appeng.container.implementations.ContainerCraftAmount;
@@ -42,6 +41,7 @@ import appeng.parts.reporting.PartExpandedProcessingPatternTerminal;
import appeng.parts.reporting.PartPatternTerminal;
import appeng.parts.reporting.PartTerminal;
import net.minecraft.client.gui.GuiButton;
import net.minecraft.client.gui.GuiTextField;
import net.minecraft.entity.player.InventoryPlayer;
import net.minecraft.item.ItemStack;
import org.lwjgl.input.Keyboard;
@@ -50,7 +50,7 @@ import java.io.IOException;
public class GuiCraftAmount extends AEBaseGui {
private GuiNumberBox amountToCraft;
private GuiTextField amountToCraft;
private GuiTabButton originalGuiBtn;
private GuiButton next;
@@ -126,7 +126,7 @@ public class GuiCraftAmount extends AEBaseGui {
this.buttonList.add(this.originalGuiBtn = new GuiTabButton(this.guiLeft + 154, this.guiTop, myIcon, myIcon.getDisplayName(), this.itemRender));
}
this.amountToCraft = new GuiNumberBox(this.fontRenderer, this.guiLeft + 62, this.guiTop + 57, 59, this.fontRenderer.FONT_HEIGHT, Integer.class);
this.amountToCraft = new GuiTextField(0, this.fontRenderer, this.guiLeft + 62, this.guiTop + 57, 59, this.fontRenderer.FONT_HEIGHT);
this.amountToCraft.setEnableBackgroundDrawing(false);
this.amountToCraft.setMaxStringLength(16);
this.amountToCraft.setTextColor(0xFFFFFF);
@@ -149,8 +149,17 @@ public class GuiCraftAmount extends AEBaseGui {
this.drawTexturedModalRect(offsetX, offsetY, 0, 0, this.xSize, this.ySize);
try {
long amt = Long.parseLong(this.amountToCraft.getText());
this.next.enabled = (!this.amountToCraft.getText().isEmpty() && amt > 0);
String out = this.amountToCraft.getText();
double resultD = MathExpressionParser.parse(out);
long amt;
if (resultD <= 0 || Double.isNaN(resultD)) {
amt = 0;
} else {
amt = (long) MathExpressionParser.round(resultD, 0);
}
this.next.enabled = amt > 0;
} catch (final NumberFormatException e) {
this.next.enabled = false;
}
@@ -164,33 +173,7 @@ public class GuiCraftAmount extends AEBaseGui {
if (key == Keyboard.KEY_RETURN || key == Keyboard.KEY_NUMPADENTER) {
this.actionPerformed(this.next);
}
if ((key == 211 || key == 205 || key == 203 || key == 14 || character == '-' || Character.isDigit(character)) && this.amountToCraft
.textboxKeyTyped(character, key)) {
try {
String out = this.amountToCraft.getText();
boolean fixed = false;
while (out.startsWith("0") && out.length() > 1) {
out = out.substring(1);
fixed = true;
}
if (fixed) {
this.amountToCraft.setText(out);
}
if (out.isEmpty()) {
out = "0";
}
final long result = Long.parseLong(out);
if (result < 0) {
this.amountToCraft.setText("1");
}
} catch (final NumberFormatException e) {
// :P
}
} else {
if (!this.amountToCraft.textboxKeyTyped(character, key)) {
super.keyTyped(character, key);
}
}
@@ -207,7 +190,15 @@ public class GuiCraftAmount extends AEBaseGui {
}
if (btn == this.next) {
NetworkHandler.instance().sendToServer(new PacketCraftRequest(Integer.parseInt(this.amountToCraft.getText()), isShiftKeyDown()));
double resultD = MathExpressionParser.parse(this.amountToCraft.getText());
int result;
if (resultD <= 0 || Double.isNaN(resultD)) {
result = 1;
} else {
result = (int) MathExpressionParser.round(resultD, 0);
}
NetworkHandler.instance().sendToServer(new PacketCraftRequest(result, isShiftKeyDown()));
}
} catch (final NumberFormatException e) {
// nope..
@@ -226,22 +217,15 @@ public class GuiCraftAmount extends AEBaseGui {
try {
String out = this.amountToCraft.getText();
boolean fixed = false;
while (out.startsWith("0") && out.length() > 1) {
out = out.substring(1);
fixed = true;
}
double resultD = MathExpressionParser.parse(out);
int result;
if (fixed) {
this.amountToCraft.setText(out);
if (resultD <= 0 || Double.isNaN(resultD)) {
result = 0;
} else {
result = (int) MathExpressionParser.round(resultD, 0);
}
if (out.isEmpty()) {
out = "0";
}
long result = Integer.parseInt(out);
if (result == 1 && i > 1) {
result = 0;
}
@@ -251,8 +235,7 @@ public class GuiCraftAmount extends AEBaseGui {
result = 1;
}
out = Long.toString(result);
Integer.parseInt(out);
out = Integer.toString(result);
this.amountToCraft.setText(out);
} catch (final NumberFormatException e) {
// :P
@@ -0,0 +1,55 @@
package appeng.client.gui;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.Locale;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class MathExpressionParserTest {
@ParameterizedTest
@CsvSource(value = {
"1 + 2|3",
"3 *4 |12",
"1 + 2 * 3 |7",
"1 - 6|-5",
"1/3|0.333333",
"23.4 + 0.6|24",
"1 - -4|5",
"1 + 4*3*2|25",
"1/0|failed",
"1/(1 - 1)|failed",
"3 + 2 * 4 - 1 /2|10.5",
"1 + (2 * (2 * (1 + 1)))|9",
"arkazkdhz|failed",
"1 + 2 3 7 - 1|237", // whitespace is trimmed
"2 + + 2|failed",
"10e6|10000000",
"-1 -1|-2",
"- (1 + 1)|-2",
"2 * -1|-2",
"2 -2|0",
"- 1|-1",
"-1|-1",
"- - - - - 5|failed", // not able to handle multiple negations, may fix in the future
"-(-(-(-2)))|failed", // not able to handle multiple negations, may fix in the future
"1 - -1|2",
"1 + -(2|failed"
}, delimiter = '|')
void testMath(String expression, String expected) {
DecimalFormat format = new DecimalFormat("#.######", DecimalFormatSymbols.getInstance(Locale.US));
format.setParseBigDecimal(true);
format.setNegativePrefix("-");
double parsed = MathExpressionParser.parse(expression);
if (Double.isNaN(parsed)) {
assertEquals(expected, "failed");
} else {
assertEquals(expected, format.format(parsed));
}
}
}