From 57682dc188b833b66c6fd894a5742a78f7fe0f45 Mon Sep 17 00:00:00 2001 From: Abdiel Kavash <19243993+AbdielKavash@users.noreply.github.com> Date: Wed, 7 Feb 2024 05:36:32 -0600 Subject: [PATCH 1/9] Expression parsing optimization and improvements. --- .../modularui/api/math/MathExpression.java | 741 ++++++++++++------ .../modularui/test/TestTile.java | 11 +- .../api/math/MathExpressionTest.java | 86 +- 3 files changed, 574 insertions(+), 264 deletions(-) diff --git a/src/main/java/com/gtnewhorizons/modularui/api/math/MathExpression.java b/src/main/java/com/gtnewhorizons/modularui/api/math/MathExpression.java index de93846..1ffbd63 100644 --- a/src/main/java/com/gtnewhorizons/modularui/api/math/MathExpression.java +++ b/src/main/java/com/gtnewhorizons/modularui/api/math/MathExpression.java @@ -1,310 +1,475 @@ package com.gtnewhorizons.modularui.api.math; -import java.text.ParseException; +import java.text.NumberFormat; +import java.text.ParsePosition; import java.util.ArrayList; -import java.util.Collections; import java.util.List; +import java.util.function.BiFunction; +import java.util.regex.Pattern; -import com.gtnewhorizons.modularui.common.widget.textfield.TextFieldWidget; +import org.jetbrains.annotations.NotNull; public class MathExpression { - private static final List
+ *
+ * All evaluation is done with double
precision. Standard rules of operator priority are followed.
+ *
+ * To further tune details of parsing, pass an instance of {@link Context}. See documentation of this class for + * details of options. + *
+ *
+ * After parsing finishes, ctx.success
indicates whether parsing was successful or not. In case parsing
+ * fails, ctx.errorMessage
will try to give a description of what went wrong. Note that this only
+ * handles syntax errors; arithmetic errors (such as division by zero) are not checked and will return a value
+ * according to Java specification of the double
type.
+ *
+ * Default: 0 + */ + public Context setEmptyValue(double emptyValue) { + this.emptyValue = emptyValue; + return this; + } + + private double errorValue = 0; + + /** + * Value to return if the expression contains an error. Note that this only catches syntax errors, not + * evaluation errors like overflow or division by zero. + *
+ * Default: 0 + */ + public Context setErrorValue(double errorValue) { + this.errorValue = errorValue; + return this; + } + + /** + * Default value to return when the expression is empty or has an error. + *
+ * Equivalent to ctx.setEmptyValue(defaultValue).setErrorValue(defaultValue)
.
+ */
+ public Context setDefaultValue(double defaultValue) {
+ this.emptyValue = defaultValue;
+ this.errorValue = defaultValue;
+ return this;
+ }
+
+ private double hundredPercent = 100;
+
+ /**
+ * Value to be considered 100% for expressions which contain percentages. For example, if this is 500, then
+ * "20%" evaluates to 100.
+ *
+ * Default: 100 + */ + public Context setHundredPercent(double hundredPercent) { + this.hundredPercent = hundredPercent; + return this; + } + + private NumberFormat numberFormat = NumberFormat.getNumberInstance(); + + /** + * Format in which to expect the input expression to be. The main purpose of specifying this is properly + * handling thousands separators and decimal point. + *
+ * Defaults to the user's system locale NumberFormat
.
+ */
+ public Context setNumberFormat(NumberFormat numberFormat) {
+ this.numberFormat = numberFormat;
+ return this;
+ }
+
+ private boolean plainOnly = false;
+
+ /**
+ * If this is true, no expression parsing is performed, and the input is expected to be just a plain number. The
+ * parsing still handles localization, error handling, etc.
+ *
+ * Default: false
+ */
+ public Context setPlainOnly(boolean plainOnly) {
+ this.plainOnly = plainOnly;
+ return this;
+ }
+
+ private boolean success = false;
+
+ /**
+ * Call this after parsing has finished.
+ *
+ * @return true if the parsing was successful.
+ */
+ public boolean getSuccess() {
+ return success;
+ }
+
+ private String errorMessage = "";
+
+ /**
+ * Call this after parsing has finished.
+ *
+ * @return If the parsing has failed with an error, this will try to explain what went wrong.
+ */
+ public String getErrorMessage() {
+ return errorMessage;
+ }
+
+ }
}
diff --git a/src/main/java/com/gtnewhorizons/modularui/test/TestTile.java b/src/main/java/com/gtnewhorizons/modularui/test/TestTile.java
index 26e89bf..184fb38 100644
--- a/src/main/java/com/gtnewhorizons/modularui/test/TestTile.java
+++ b/src/main/java/com/gtnewhorizons/modularui/test/TestTile.java
@@ -178,10 +178,13 @@ private Widget createPage1() {
changeableWidget.notifyChangeServer();
}).setShiftClickPriority(0).setPos(10, 30))
.addChild(
- new TextFieldWidget().setGetter(() -> String.valueOf(longValue))
- .setSetter(val -> longValue = (long) MathExpression.parseMathExpression(val))
- .setNumbersLong(val -> val).setTextColor(Color.WHITE.dark(1))
- .setTextAlignment(Alignment.CenterLeft).setScrollBar()
+ new TextFieldWidget().setPattern(MathExpression.EXPRESSION_PATTERN)
+ .setGetter(() -> String.valueOf(longValue))
+ .setSetter(
+ val -> longValue = (long) MathExpression.parseMathExpression(
+ val,
+ new MathExpression.Context().setHundredPercent(1000)))
+ .setTextColor(Color.WHITE.dark(1)).setTextAlignment(Alignment.CenterLeft).setScrollBar()
.setBackground(DISPLAY.withOffset(-2, -2, 4, 4)).setSize(92, 20).setPos(10, 50))
.addChild(
SlotWidget.phantom(phantomInventory, 1).setShiftClickPriority(1).setIgnoreStackSizeLimit(true)
diff --git a/src/test/java/com/gtnewhorizons/modularui/api/math/MathExpressionTest.java b/src/test/java/com/gtnewhorizons/modularui/api/math/MathExpressionTest.java
index 137389d..cd293d0 100644
--- a/src/test/java/com/gtnewhorizons/modularui/api/math/MathExpressionTest.java
+++ b/src/test/java/com/gtnewhorizons/modularui/api/math/MathExpressionTest.java
@@ -2,20 +2,34 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
+import java.text.NumberFormat;
+import java.util.Locale;
+
import org.junit.jupiter.api.Test;
class MathExpressionTest {
+ MathExpression.Context ctxEN = new MathExpression.Context()
+ .setNumberFormat(NumberFormat.getNumberInstance(Locale.US));
+ MathExpression.Context ctxFR = new MathExpression.Context()
+ .setNumberFormat(NumberFormat.getNumberInstance(Locale.FRENCH));
+ MathExpression.Context ctxES = new MathExpression.Context()
+ .setNumberFormat(NumberFormat.getNumberInstance(Locale.forLanguageTag("ES")));
+
@Test
void NumbersBasic_Test() {
- assertEquals(42, MathExpression.parseMathExpression("42"));
+ assertEquals(41, MathExpression.parseMathExpression("41"));
assertEquals(42, MathExpression.parseMathExpression(" 42 "));
- assertEquals(123456, MathExpression.parseMathExpression("123,456"));
- assertEquals(123456, MathExpression.parseMathExpression("123 456"));
- assertEquals(123456, MathExpression.parseMathExpression("123_456"));
+ assertEquals(123456.789, MathExpression.parseMathExpression("123456.789", ctxEN));
+ assertEquals(234567.891, MathExpression.parseMathExpression("234,567.891", ctxEN));
+
+ assertEquals(345678.912, MathExpression.parseMathExpression("345 678,912", ctxFR));
- assertEquals(123.456, MathExpression.parseMathExpression("123.456"));
+ String s = NumberFormat.getNumberInstance(Locale.FRENCH).format(456789.123);
+ assertEquals(456789.123, MathExpression.parseMathExpression(s, ctxFR));
+
+ assertEquals(567891.234, MathExpression.parseMathExpression("567.891,234", ctxES));
}
@Test
@@ -24,10 +38,20 @@ void ArithmeticBasic_Test() {
assertEquals(-1, MathExpression.parseMathExpression("2-3"));
assertEquals(6, MathExpression.parseMathExpression("2*3"));
assertEquals(2, MathExpression.parseMathExpression("6/3"));
- assertEquals(1, MathExpression.parseMathExpression("7%3"));
assertEquals(8, MathExpression.parseMathExpression("2^3"));
+ }
+
+ @Test
+ void UnaryMinus_Test() {
+ assertEquals(-5, MathExpression.parseMathExpression("-5"));
+ assertEquals(-3, MathExpression.parseMathExpression("-5+2"));
+ assertEquals(-7, MathExpression.parseMathExpression("-5-2"));
+ assertEquals(-15, MathExpression.parseMathExpression("-5*3"));
+ assertEquals(-2.5, MathExpression.parseMathExpression("-5/2"));
+ assertEquals(-25, MathExpression.parseMathExpression("-5^2")); // ! this is -(5^2), not (-5)^2.
- assertEquals(5, MathExpression.parseMathExpression("2 + 3"));
+ assertEquals(2, MathExpression.parseMathExpression("4+-2"));
+ assertEquals(6, MathExpression.parseMathExpression("4--2"));
}
@Test
@@ -43,24 +67,26 @@ void ArithmeticPriority_Test() {
}
@Test
- void UnaryZero_Test() {
- assertEquals(-5, MathExpression.parseMathExpression("-5"));
- assertEquals(-3, MathExpression.parseMathExpression("-5+2"));
- assertEquals(-7, MathExpression.parseMathExpression("-5-2"));
- assertEquals(-10, MathExpression.parseMathExpression("-5*2"));
- assertEquals(-2.5, MathExpression.parseMathExpression("-5/2"));
- assertEquals(-1, MathExpression.parseMathExpression("-5%2"));
- assertEquals(25, MathExpression.parseMathExpression("-5^2")); // ! this is (-5)^2, not -(5^2).
+ void Brackets_Test() {
+ assertEquals(5, MathExpression.parseMathExpression("(2+3)"));
+ assertEquals(20, MathExpression.parseMathExpression("(2+3)*4"));
+ assertEquals(14, MathExpression.parseMathExpression("2+(3*4)"));
+ assertEquals(42, MathExpression.parseMathExpression("(((42)))"));
+
+ assertEquals(14, MathExpression.parseMathExpression("2(3+4)"));
}
@Test
void ScientificBasic_Test() {
assertEquals(2000, MathExpression.parseMathExpression("2e3"));
assertEquals(3000, MathExpression.parseMathExpression("3E3"));
- assertEquals(4000, MathExpression.parseMathExpression("4 e 3"));
- assertEquals(5600, MathExpression.parseMathExpression("5.6e3"));
- assertEquals(70_000, MathExpression.parseMathExpression("700e2"));
- assertEquals(8, MathExpression.parseMathExpression("8e0"));
+ assertEquals(0.04, MathExpression.parseMathExpression("4e-2"));
+ assertEquals(0.05, MathExpression.parseMathExpression("5E-2"));
+
+ assertEquals(6000, MathExpression.parseMathExpression("6 e 3"));
+ assertEquals(7800, MathExpression.parseMathExpression("7.8e3"));
+ assertEquals(90_000, MathExpression.parseMathExpression("900e2"));
+ assertEquals(1, MathExpression.parseMathExpression("1e0"));
}
@Test
@@ -87,8 +113,8 @@ void SuffixesBasic_Test() {
assertEquals(10_000_000_000_000D, MathExpression.parseMathExpression("10t"));
assertEquals(11_000_000_000_000D, MathExpression.parseMathExpression("11T"));
- assertEquals(2050, MathExpression.parseMathExpression("2.05k"));
- assertEquals(50, MathExpression.parseMathExpression("0.05k"));
+ assertEquals(2050, MathExpression.parseMathExpression("2.05k", ctxEN));
+ assertEquals(50, MathExpression.parseMathExpression("0.05k", ctxEN));
assertEquals(3000, MathExpression.parseMathExpression("3 k"));
}
@@ -104,10 +130,22 @@ void SuffixesArithmetic_Test() {
assertEquals(4_000_000_000D, MathExpression.parseMathExpression("4kkk"));
// Not supported, but shouldn't fail.
- assertEquals(6_000_000_000D, MathExpression.parseMathExpression("6km"));
- assertEquals(500_000, MathExpression.parseMathExpression("0.5ke3"));
+ assertEquals(6_000_000_000d, MathExpression.parseMathExpression("6km"));
+ assertEquals(500_000, MathExpression.parseMathExpression("0.5ke3", ctxEN));
// Please don't do this.
- assertEquals(20_000_000_000D, MathExpression.parseMathExpression("2e0.01k"));
+ assertEquals(20_000_000_000D, MathExpression.parseMathExpression("2e0.01k", ctxEN));
+ }
+
+ @Test
+ void Percent_Test() {
+ ctxEN.setHundredPercent(1000);
+
+ assertEquals(100, MathExpression.parseMathExpression("10%", ctxEN));
+ assertEquals(2000, MathExpression.parseMathExpression("200%", ctxEN));
+ assertEquals(-300, MathExpression.parseMathExpression("-30%", ctxEN));
+
+ assertEquals(450, MathExpression.parseMathExpression("40% + 50", ctxEN));
+ assertEquals(500, MathExpression.parseMathExpression("(20+30)%", ctxEN));
}
}
From 5e2429d38c4a618e0a5d46d5082ab512e42f52e5 Mon Sep 17 00:00:00 2001
From: Abdiel Kavash <19243993+AbdielKavash@users.noreply.github.com>
Date: Wed, 7 Feb 2024 06:25:15 -0600
Subject: [PATCH 2/9] Re-add accidentally removed public method.
---
.../gtnewhorizons/modularui/api/math/MathExpression.java | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/src/main/java/com/gtnewhorizons/modularui/api/math/MathExpression.java b/src/main/java/com/gtnewhorizons/modularui/api/math/MathExpression.java
index 1ffbd63..34e1895 100644
--- a/src/main/java/com/gtnewhorizons/modularui/api/math/MathExpression.java
+++ b/src/main/java/com/gtnewhorizons/modularui/api/math/MathExpression.java
@@ -38,6 +38,15 @@ public static double parseMathExpression(String expr, double onFailReturn) {
return parseMathExpression(expr, new Context().setDefaultValue(onFailReturn));
}
+ /**
+ * @deprecated Call as parseMathExpression(num, new MathExpression.Context()
+ * .setPlainOnly(true).setDefaultValue(onFailReturn))
+ */
+ @Deprecated
+ public static double parse(String num, double onFailReturn) {
+ return parseMathExpression(num, new Context().setPlainOnly(true).setDefaultValue(onFailReturn));
+ }
+
/**
* Parses a mathematical expression in a string and returns the result value.
*
From 96c298717b4d0203bb2e599ce78f88e7b5be21ed Mon Sep 17 00:00:00 2001 From: Abdiel Kavash <19243993+AbdielKavash@users.noreply.github.com> Date: Wed, 7 Feb 2024 06:25:37 -0600 Subject: [PATCH 3/9] Update BS + deps. --- dependencies.gradle | 8 ++++---- settings.gradle | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/dependencies.gradle b/dependencies.gradle index 4c3cd90..4a61a94 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -3,14 +3,14 @@ dependencies { api("org.jetbrains:annotations:23.0.0") - api("com.github.GTNewHorizons:NotEnoughItems:2.5.4-GTNH:dev") + api("com.github.GTNewHorizons:NotEnoughItems:2.5.17-GTNH:dev") - compileOnly("com.github.GTNewHorizons:Hodgepodge:2.4.12:dev") { transitive = false } - compileOnly("com.github.GTNewHorizons:GT5-Unofficial:5.09.45.47:dev") { + compileOnly("com.github.GTNewHorizons:Hodgepodge:2.4.20:dev") { transitive = false } + compileOnly("com.github.GTNewHorizons:GT5-Unofficial:5.09.45.61:dev") { transitive = false exclude group:"com.github.GTNewHorizons", module:"ModularUI" } - compileOnly("com.github.GTNewHorizons:Applied-Energistics-2-Unofficial:rv3-beta-312-GTNH:dev") { transitive = false } + compileOnly("com.github.GTNewHorizons:Applied-Energistics-2-Unofficial:rv3-beta-315-GTNH:dev") { transitive = false } testImplementation(platform('org.junit:junit-bom:5.9.2')) testImplementation('org.junit.jupiter:junit-jupiter') diff --git a/settings.gradle b/settings.gradle index e8946ad..3b6c62d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -17,7 +17,7 @@ pluginManagement { } plugins { - id 'com.gtnewhorizons.gtnhsettingsconvention' version '1.0.8' + id 'com.gtnewhorizons.gtnhsettingsconvention' version '1.0.9' } From 5ef0cb2fad07abeb201b422baee6c6e1fa9b3013 Mon Sep 17 00:00:00 2001 From: Abdiel Kavash <19243993+AbdielKavash@users.noreply.github.com> Date: Wed, 7 Feb 2024 07:09:33 -0600 Subject: [PATCH 4/9] Minor fixes. --- .../com/gtnewhorizons/modularui/api/math/MathExpression.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/gtnewhorizons/modularui/api/math/MathExpression.java b/src/main/java/com/gtnewhorizons/modularui/api/math/MathExpression.java index 34e1895..04a5704 100644 --- a/src/main/java/com/gtnewhorizons/modularui/api/math/MathExpression.java +++ b/src/main/java/com/gtnewhorizons/modularui/api/math/MathExpression.java @@ -57,10 +57,10 @@ public static double parse(String num, double onFailReturn) { *
@@ -472,7 +472,7 @@ public double evaluate(double left, double right) {
}
}
- public enum Suffix {
+ private enum Suffix {
THOUSAND(1_000d),
MILLION(1_000_000d),
From 9c8741ac788844b7040c93356136d5ec20a901ff Mon Sep 17 00:00:00 2001
From: Abdiel Kavash <19243993+AbdielKavash@users.noreply.github.com>
Date: Sat, 17 Feb 2024 14:44:34 -0600
Subject: [PATCH 5/9] Minor fixes and refactors.
---
.../modularui/api/math/MathExpression.java | 36 +++++++++++--------
.../api/math/MathExpressionTest.java | 3 ++
2 files changed, 24 insertions(+), 15 deletions(-)
diff --git a/src/main/java/com/gtnewhorizons/modularui/api/math/MathExpression.java b/src/main/java/com/gtnewhorizons/modularui/api/math/MathExpression.java
index 04a5704..e56aba3 100644
--- a/src/main/java/com/gtnewhorizons/modularui/api/math/MathExpression.java
+++ b/src/main/java/com/gtnewhorizons/modularui/api/math/MathExpression.java
@@ -17,7 +17,7 @@ public class MathExpression {
* explanation of the syntax.
*/
public static final Pattern EXPRESSION_PATTERN = Pattern.compile("[0-9., +\\-*/^()eEkKmMgGbBtT%]*");
- // Character to support French locale thousands separator.
+ // Character ' ' (non-breaking space) to support French locale thousands separator.
// TODO:
// There should be a config option to default the number format to either the user's computer locale, or to a
@@ -84,38 +84,43 @@ public static double parse(String num, double onFailReturn) {
public static double parseMathExpression(String expr, Context ctx) {
if (expr == null) {
ctx.success = true;
+ ctx.errorMessage = "Success";
return ctx.emptyValue;
}
- // Strip all spaces from the input string.
+ // Strip all spaces and underscores from the input string.
+ // This allows using them for readability and as thousands separators (using java convention 1_000_000).
// This also correctly interprets numbers in the French locale typed by user using spaces as thousands
// separators.
// See: https://bugs.java.com/bugdatabase/view_bug?bug_id=4510618
- expr = expr.replace(" ", "");
+ expr = expr.replace(" ", "").replace("_", "");
if (expr.isEmpty()) {
ctx.success = true;
+ ctx.errorMessage = "Success";
return ctx.emptyValue;
}
// Read the first numeric value, skip any further parsing if the string contains *only* one number.
- ParsePosition pp = new ParsePosition(0);
- Number value = ctx.numberFormat.parse(expr, pp);
+ ParsePosition parsePos = new ParsePosition(0);
+ Number value = ctx.numberFormat.parse(expr, parsePos);
if (ctx.plainOnly) {
// Skip any further parsing, only return what was found.
- if (value == null || pp.getIndex() == 0) {
+ if (value == null || parsePos.getIndex() == 0) {
ctx.success = false;
ctx.errorMessage = "Error: No number found";
return ctx.errorValue;
} else {
ctx.success = true;
+ ctx.errorMessage = "Success";
return value.doubleValue();
}
}
- if (value != null && pp.getIndex() == expr.length()) {
+ if (value != null && parsePos.getIndex() == expr.length()) {
// The entire expr is just a single number. Skip the rest of parsing completely.
ctx.success = true;
+ ctx.errorMessage = "Success";
return value.doubleValue();
}
@@ -138,7 +143,7 @@ public static double parseMathExpression(String expr, Context ctx) {
}
}
- for (int i = pp.getIndex(); i < expr.length(); ++i) {
+ for (int i = parsePos.getIndex(); i < expr.length(); ++i) {
char c = expr.charAt(i);
switch (c) {
@@ -197,15 +202,15 @@ public static double parseMathExpression(String expr, Context ctx) {
// Otherwise, read the next number.
default:
- pp.setIndex(i);
- value = ctx.numberFormat.parse(expr, pp);
- if (value == null || pp.getIndex() == i) {
+ parsePos.setIndex(i);
+ value = ctx.numberFormat.parse(expr, parsePos);
+ if (value == null || parsePos.getIndex() == i) {
ctx.success = false;
ctx.errorMessage = "Error: Number expected";
return ctx.errorValue;
} else {
handleNumber(stack, value.doubleValue(), ctx);
- i = pp.getIndex() - 1;
+ i = parsePos.getIndex() - 1;
}
}
@@ -220,6 +225,7 @@ public static double parseMathExpression(String expr, Context ctx) {
return ctx.errorValue;
}
+ ctx.errorMessage = "Success";
return stack.get(0).value;
}
@@ -406,7 +412,7 @@ private static boolean handleExpressionEnd(@NotNull List
- * Defaults to the user's system locale
+ * Proper locale-aware code needs to communicate only the numeric value between server and all clients, and let
+ * every client both parse and format it on their own.
*/
public Context setNumberFormat(NumberFormat numberFormat) {
this.numberFormat = numberFormat;
From c84bd5949fc30af987f52f7624a76d56fc6f9209 Mon Sep 17 00:00:00 2001
From: Abdiel Kavash <19243993+AbdielKavash@users.noreply.github.com>
Date: Sat, 17 Feb 2024 15:31:25 -0600
Subject: [PATCH 9/9] A word.
---
.../com/gtnewhorizons/modularui/api/math/MathExpression.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/main/java/com/gtnewhorizons/modularui/api/math/MathExpression.java b/src/main/java/com/gtnewhorizons/modularui/api/math/MathExpression.java
index 666daee..83c6521 100644
--- a/src/main/java/com/gtnewhorizons/modularui/api/math/MathExpression.java
+++ b/src/main/java/com/gtnewhorizons/modularui/api/math/MathExpression.java
@@ -297,7 +297,7 @@ private static boolean handleMinus(@NotNull ListNumberFormat
.
+ * This defaults to the EN_US locale. Care should be taken when changing this in a multiplayer setting. Code
+ * that blindly trusts the player's system locale will run into issues. One player could input a value, which
+ * will be formatted for that player's locale and potentially stored as a string. Then another player with a
+ * different locale might open the same UI, and see what to their client looks like a malformed string.
+ *