diff --git a/src/main/java/appeng/client/gui/MathExpressionParser.java b/src/main/java/appeng/client/gui/MathExpressionParser.java new file mode 100644 index 000000000..dc1696fc0 --- /dev/null +++ b/src/main/java/appeng/client/gui/MathExpressionParser.java @@ -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 postfixStack = new Stack<>(); + private final Stack 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 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)); + } +} diff --git a/src/main/java/appeng/client/gui/implementations/GuiCraftAmount.java b/src/main/java/appeng/client/gui/implementations/GuiCraftAmount.java index 02e07e2a8..8a2935b94 100644 --- a/src/main/java/appeng/client/gui/implementations/GuiCraftAmount.java +++ b/src/main/java/appeng/client/gui/implementations/GuiCraftAmount.java @@ -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 diff --git a/src/test/java/appeng/client/gui/MathExpressionParserTest.java b/src/test/java/appeng/client/gui/MathExpressionParserTest.java new file mode 100644 index 000000000..eef0776ea --- /dev/null +++ b/src/test/java/appeng/client/gui/MathExpressionParserTest.java @@ -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)); + } + } +}