diff --git a/addon.gradle b/addon.gradle index cc0f5d5..4e8d2eb 100644 --- a/addon.gradle +++ b/addon.gradle @@ -1,3 +1,9 @@ compileJava { options.encoding = "UTF-8" } +test { + useJUnitPlatform() + testLogging { + events "passed", "skipped", "failed" + } +} diff --git a/build.gradle b/build.gradle index 3eb19a2..6274cb2 100644 --- a/build.gradle +++ b/build.gradle @@ -1,4 +1,4 @@ -//version: 1704751096 +//version: 1705357285 /* DO NOT CHANGE THIS FILE! Also, you may replace this file at any time if there is an update available. @@ -122,6 +122,7 @@ propertyDefaultIfUnset("modrinthProjectId", "") propertyDefaultIfUnset("modrinthRelations", "") propertyDefaultIfUnset("curseForgeProjectId", "") propertyDefaultIfUnset("curseForgeRelations", "") +propertyDefaultIfUnset("versionPattern", "") propertyDefaultIfUnset("minimizeShadowedDependencies", true) propertyDefaultIfUnset("relocateShadowedDependencies", true) // Deprecated properties (kept for backwards compat) @@ -370,6 +371,7 @@ catch (Exception ignored) { // Pulls version first from the VERSION env and then git tag String identifiedVersion String versionOverride = System.getenv("VERSION") ?: null +boolean checkVersion = false try { // Produce a version based on the tag, or for branches something like 0.2.2-configurable-maven-and-extras.38+43090270b6-dirty if (versionOverride == null) { @@ -388,6 +390,8 @@ try { } } else if (isDirty) { identifiedVersion += "-${branchName}+${gitDetails.gitHash}-dirty" + } else { + checkVersion = true } } else { identifiedVersion = versionOverride @@ -409,6 +413,8 @@ ext { if (identifiedVersion == versionOverride) { out.style(Style.Failure).text('Override version to ').style(Style.Identifier).text(modVersion).style(Style.Failure).println('!\7') +} else if (checkVersion && versionPattern && !(identifiedVersion ==~ versionPattern)) { + throw new GradleException("Invalid version: '$identifiedVersion' does not match version pattern '$versionPattern'") } group = "com.github.GTNewHorizons" @@ -428,18 +434,31 @@ minecraft { for (f in replaceGradleTokenInFile.split(',')) { tagReplacementFiles.add f } + out.style(Style.Info).text('replaceGradleTokenInFile is deprecated! Consider using generateGradleTokenClass.').println() } if (gradleTokenModId) { - injectedTags.put gradleTokenModId, modId + if (replaceGradleTokenInFile) { + injectedTags.put gradleTokenModId, modId + } else { + out.style(Style.Failure).text('gradleTokenModId is deprecated! The field will no longer be generated.').println() + } } if (gradleTokenModName) { - injectedTags.put gradleTokenModName, modName + if (replaceGradleTokenInFile) { + injectedTags.put gradleTokenModName, modName + } else { + out.style(Style.Failure).text('gradleTokenModName is deprecated! The field will no longer be generated.').println() + } } if (gradleTokenVersion) { injectedTags.put gradleTokenVersion, modVersion } if (gradleTokenGroupName) { - injectedTags.put gradleTokenGroupName, modGroup + if (replaceGradleTokenInFile) { + injectedTags.put gradleTokenGroupName, modGroup + } else { + out.style(Style.Failure).text('gradleTokenGroupName is deprecated! The field will no longer be generated.').println() + } } if (enableGenericInjection.toBoolean()) { injectMissingGenerics.set(true) diff --git a/dependencies.gradle b/dependencies.gradle index b11df4f..4c3cd90 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -5,10 +5,13 @@ dependencies { api("com.github.GTNewHorizons:NotEnoughItems:2.5.4-GTNH:dev") - compileOnly("com.github.GTNewHorizons:Hodgepodge:2.4.8:dev") { transitive = false } - compileOnly("com.github.GTNewHorizons:GT5-Unofficial:5.09.45.25:dev") { + compileOnly("com.github.GTNewHorizons:Hodgepodge:2.4.12:dev") { transitive = false } + compileOnly("com.github.GTNewHorizons:GT5-Unofficial:5.09.45.47:dev") { transitive = false exclude group:"com.github.GTNewHorizons", module:"ModularUI" } - compileOnly("com.github.GTNewHorizons:Applied-Energistics-2-Unofficial:rv3-beta-310-GTNH:dev") { transitive = false } + compileOnly("com.github.GTNewHorizons:Applied-Energistics-2-Unofficial:rv3-beta-312-GTNH:dev") { transitive = false } + + testImplementation(platform('org.junit:junit-bom:5.9.2')) + testImplementation('org.junit.jupiter:junit-jupiter') } 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 97e5bca..de93846 100644 --- a/src/main/java/com/gtnewhorizons/modularui/api/math/MathExpression.java +++ b/src/main/java/com/gtnewhorizons/modularui/api/math/MathExpression.java @@ -24,101 +24,108 @@ public static double parseMathExpression(String expr, double onFailReturn) { Object value = parsed.get(0); return value instanceof Double ? (double) value : onFailReturn; } - Double lastNum = null; - for (int i = 0; i < parsed.size(); i++) { + + if (Operator.MINUS == parsed.get(0)) { + parsed.remove(0); + parsed.set(0, -(Double) parsed.get(0)); + } + + for (int i = 1; i < parsed.size(); i++) { Object obj = parsed.get(i); - if (lastNum == null && obj instanceof Double) { - lastNum = (Double) obj; - continue; + if (obj instanceof Suffix) { + Double left = (Double) parsed.get(i - 1); + Double result = left * ((Suffix) obj).multiplier; + parsed.remove(i - 1); + parsed.remove(i - 1); + parsed.add(i - 1, result); + i--; } - if (obj == Operator.POWER) { - Double newNum = Math.pow(lastNum, (Double) parsed.get(i + 1)); + } + + for (int i = 1; i < parsed.size() - 1; i++) { + Object obj = parsed.get(i); + if (obj == Operator.SCIENTIFIC) { + Double left = (Double) parsed.get(i - 1); + Double right = (Double) parsed.get(i + 1); + Double result = left * Math.pow(10, right); parsed.remove(i - 1); parsed.remove(i - 1); parsed.remove(i - 1); - parsed.add(i - 1, newNum); - lastNum = newNum; + parsed.add(i - 1, result); i--; - continue; } - lastNum = null; } - if (lastNum != null) { - lastNum = null; - } - if (parsed.size() > 1) { - for (int i = 0; i < parsed.size(); i++) { - Object obj = parsed.get(i); - if (lastNum == null && obj instanceof Double) { - lastNum = (Double) obj; - continue; - } - if (obj == Operator.MULTIPLY) { - Double newNum = lastNum * (Double) parsed.get(i + 1); - parsed.remove(i - 1); - parsed.remove(i - 1); - parsed.remove(i - 1); - parsed.add(i - 1, newNum); - lastNum = newNum; - i--; - continue; - } - if (obj == Operator.DIVIDE) { - Double newNum = lastNum / (Double) parsed.get(i + 1); - parsed.remove(i - 1); - parsed.remove(i - 1); - parsed.remove(i - 1); - parsed.add(i - 1, newNum); - lastNum = newNum; - i--; - continue; - } - if (obj == Operator.MOD) { - Double newNum = lastNum % (Double) parsed.get(i + 1); - parsed.remove(i - 1); - parsed.remove(i - 1); - parsed.remove(i - 1); - parsed.add(i - 1, newNum); - lastNum = newNum; - i--; - continue; - } - lastNum = null; + + // ^ is right-associative: a^b^c = a^(b^c) + for (int i = parsed.size() - 2; i > 0; i--) { + Object obj = parsed.get(i); + if (obj == Operator.POWER) { + Double left = (Double) parsed.get(i - 1); + Double right = (Double) parsed.get(i + 1); + Double result = Math.pow(left, right); + parsed.remove(i - 1); + parsed.remove(i - 1); + parsed.remove(i - 1); + parsed.add(i - 1, result); + i--; } - if (lastNum != null) { - lastNum = null; + } + + for (int i = 1; i < parsed.size() - 1; i++) { + Object obj = parsed.get(i); + if (obj == Operator.MULTIPLY) { + Double left = (Double) parsed.get(i - 1); + Double right = (Double) parsed.get(i + 1); + Double result = left * right; + parsed.remove(i - 1); + parsed.remove(i - 1); + parsed.remove(i - 1); + parsed.add(i - 1, result); + i--; + } else if (obj == Operator.DIVIDE) { + Double left = (Double) parsed.get(i - 1); + Double right = (Double) parsed.get(i + 1); + Double result = left / right; + parsed.remove(i - 1); + parsed.remove(i - 1); + parsed.remove(i - 1); + parsed.add(i - 1, result); + i--; + } else if (obj == Operator.MOD) { + Double left = (Double) parsed.get(i - 1); + Double right = (Double) parsed.get(i + 1); + Double result = left % right; + parsed.remove(i - 1); + parsed.remove(i - 1); + parsed.remove(i - 1); + parsed.add(i - 1, result); + i--; } } - if (parsed.size() > 1) { - for (int i = 0; i < parsed.size(); i++) { - Object obj = parsed.get(i); - if (lastNum == null && obj instanceof Double) { - lastNum = (Double) obj; - continue; - } - if (obj == Operator.PLUS) { - Double newNum = lastNum + (Double) parsed.get(i + 1); - parsed.remove(i - 1); - parsed.remove(i - 1); - parsed.remove(i - 1); - parsed.add(i - 1, newNum); - lastNum = newNum; - i--; - continue; - } - if (obj == Operator.MINUS) { - Double newNum = lastNum - (Double) parsed.get(i + 1); - parsed.remove(i - 1); - parsed.remove(i - 1); - parsed.remove(i - 1); - parsed.add(i - 1, newNum); - lastNum = newNum; - i--; - continue; - } - lastNum = null; + + for (int i = 1; i < parsed.size() - 1; i++) { + Object obj = parsed.get(i); + if (obj == Operator.PLUS) { + Double left = (Double) parsed.get(i - 1); + Double right = (Double) parsed.get(i + 1); + Double result = left + right; + parsed.remove(i - 1); + parsed.remove(i - 1); + parsed.remove(i - 1); + parsed.add(i - 1, result); + i--; + } else if (obj == Operator.MINUS) { + Double left = (Double) parsed.get(i - 1); + Double right = (Double) parsed.get(i + 1); + Double result = left - right; + parsed.remove(i - 1); + parsed.remove(i - 1); + parsed.remove(i - 1); + parsed.add(i - 1, result); + i--; } } + if (parsed.size() != 1) { throw new IllegalStateException("Calculated expr has more than 1 result. " + parsed); } @@ -133,6 +140,11 @@ public static List buildParsedList(String expr, double onFailReturn) { for (int i = 0; i < expr.length(); i++) { char c = expr.charAt(i); switch (c) { + case ' ': + case ',': + case '_': + break; + case '+': { if (builder.length() > 0) { parsed.add(parse(builder.toString(), onFailReturn)); @@ -181,6 +193,54 @@ public static List buildParsedList(String expr, double onFailReturn) { parsed.add(Operator.POWER); break; } + case 'e': + case 'E': { + if (builder.length() > 0) { + parsed.add(parse(builder.toString(), onFailReturn)); + builder.delete(0, builder.length()); + } + parsed.add(Operator.SCIENTIFIC); + break; + } + case 'k': + case 'K': { + if (builder.length() > 0) { + parsed.add(parse(builder.toString(), onFailReturn)); + builder.delete(0, builder.length()); + } + parsed.add(Suffix.THOUSAND); + break; + } + case 'm': + case 'M': { + if (builder.length() > 0) { + parsed.add(parse(builder.toString(), onFailReturn)); + builder.delete(0, builder.length()); + } + parsed.add(Suffix.MILLION); + break; + } + case 'b': + case 'B': + case 'g': + case 'G': { + if (builder.length() > 0) { + parsed.add(parse(builder.toString(), onFailReturn)); + builder.delete(0, builder.length()); + } + parsed.add(Suffix.BILLION); + break; + } + case 't': + case 'T': { + if (builder.length() > 0) { + parsed.add(parse(builder.toString(), onFailReturn)); + builder.delete(0, builder.length()); + } + parsed.add(Suffix.TRILLION); + break; + } + default: builder.append(c); } @@ -188,26 +248,23 @@ public static List buildParsedList(String expr, double onFailReturn) { if (builder.length() > 0) { parsed.add(parse(builder.toString(), onFailReturn)); } - if (parsed.size() >= 2 && parsed.get(0) == Operator.MINUS && parsed.get(1) instanceof Double) { - parsed.add(0, 0.0); - } - boolean shouldBeOperator = false; - for (Object object : parsed) { - if (shouldBeOperator) { - if (!(object instanceof Operator)) { - return DEFAULT; - } - shouldBeOperator = false; - } else { - if (!(object instanceof Double)) { - return DEFAULT; - } - shouldBeOperator = true; - } - } - while (parsed.get(parsed.size() - 1) instanceof Operator) { - parsed.remove(parsed.size() - 1); + + if (parsed.isEmpty()) return DEFAULT; + + Object prevToken = null; + Object thisToken = null; + for (int i = 0; i < parsed.size(); ++i) { + prevToken = thisToken; + thisToken = parsed.get(i); + + if (prevToken == null && (thisToken instanceof Double || Operator.MINUS == thisToken)) continue; + if (prevToken instanceof Double && (thisToken instanceof Operator || thisToken instanceof Suffix)) continue; + if (prevToken instanceof Operator && thisToken instanceof Double) continue; + if (prevToken instanceof Suffix && (thisToken instanceof Operator || thisToken instanceof Suffix)) continue; + return DEFAULT; } + if (thisToken instanceof Operator) return DEFAULT; + return parsed; } @@ -227,7 +284,8 @@ public enum Operator { MULTIPLY("*"), DIVIDE("/"), MOD("%"), - POWER("^"); + POWER("^"), + SCIENTIFIC("e"); public final String sign; @@ -240,4 +298,18 @@ public String toString() { return sign; } } + + public enum Suffix { + + THOUSAND(1_000D), + MILLION(1_000_000D), + BILLION(1_000_000_000D), + TRILLION(1_000_000_000_000D); + + public final double multiplier; + + Suffix(double multiplier) { + this.multiplier = multiplier; + } + } } diff --git a/src/main/java/com/gtnewhorizons/modularui/common/widget/textfield/BaseTextFieldWidget.java b/src/main/java/com/gtnewhorizons/modularui/common/widget/textfield/BaseTextFieldWidget.java index 4f52fd6..2d9c667 100644 --- a/src/main/java/com/gtnewhorizons/modularui/common/widget/textfield/BaseTextFieldWidget.java +++ b/src/main/java/com/gtnewhorizons/modularui/common/widget/textfield/BaseTextFieldWidget.java @@ -60,6 +60,20 @@ public class BaseTextFieldWidget extends Widget implements IWidgetParent, Intera public static final Pattern WHOLE_NUMS = Pattern.compile("-?[0-9]*([+\\-*/%^][0-9]*)*"); public static final Pattern DECIMALS = Pattern.compile("[0-9]*(\\.[0-9]*)?([+\\-*/%^][0-9]*(\\.[0-9]*)?)*"); + + /** + * A mathematical expression. Supported: + *
    + *
  • digits: 0..9
  • + *
  • decimal point: .
  • + *
  • thousands separators: , _ [space]
  • + *
  • arithmetic operations: + - * / % ^
  • + *
  • decimal exponent: e E
  • + *
  • decimal suffixes: k K m M b B g G t T
  • + *
+ */ + public static final Pattern MATH_EXPRESSION = Pattern.compile("[0-9.,_ +\\-*/%^eEkKmMgGbBtT]*"); + /** * alphabets */ diff --git a/src/main/java/com/gtnewhorizons/modularui/common/widget/textfield/TextFieldWidget.java b/src/main/java/com/gtnewhorizons/modularui/common/widget/textfield/TextFieldWidget.java index bdc6e29..efb3845 100644 --- a/src/main/java/com/gtnewhorizons/modularui/common/widget/textfield/TextFieldWidget.java +++ b/src/main/java/com/gtnewhorizons/modularui/common/widget/textfield/TextFieldWidget.java @@ -262,7 +262,7 @@ public TextFieldWidget setValidator(Function validator) { } public TextFieldWidget setNumbersLong(Function validator) { - setPattern(WHOLE_NUMS); + setPattern(MATH_EXPRESSION); setValidator(val -> { long num; if (val.isEmpty()) { @@ -276,7 +276,7 @@ public TextFieldWidget setNumbersLong(Function validator) { } public TextFieldWidget setNumbers(Function validator) { - setPattern(WHOLE_NUMS); + setPattern(MATH_EXPRESSION); return setValidator(val -> { int num; if (val.isEmpty()) { @@ -289,7 +289,7 @@ public TextFieldWidget setNumbers(Function validator) { } public TextFieldWidget setNumbersDouble(Function validator) { - setPattern(DECIMALS); + setPattern(MATH_EXPRESSION); return setValidator(val -> { double num; if (val.isEmpty()) { diff --git a/src/main/java/com/gtnewhorizons/modularui/test/TestTile.java b/src/main/java/com/gtnewhorizons/modularui/test/TestTile.java index acd6b28..26e89bf 100644 --- a/src/main/java/com/gtnewhorizons/modularui/test/TestTile.java +++ b/src/main/java/com/gtnewhorizons/modularui/test/TestTile.java @@ -31,6 +31,7 @@ import com.gtnewhorizons.modularui.api.math.Color; import com.gtnewhorizons.modularui.api.math.CrossAxisAlignment; import com.gtnewhorizons.modularui.api.math.MainAxisAlignment; +import com.gtnewhorizons.modularui.api.math.MathExpression; import com.gtnewhorizons.modularui.api.math.Pos2d; import com.gtnewhorizons.modularui.api.math.Size; import com.gtnewhorizons.modularui.api.screen.ITileWithModularUI; @@ -78,6 +79,7 @@ public int getSlotLimit(int slot) { private int progress = 0; private int ticks = 0; private float sliderValue = 0; + private long longValue = 0; private int serverCounter = 0; private static final AdaptableUITexture DISPLAY = AdaptableUITexture .of("modularui:gui/background/display", 143, 75, 2); @@ -175,6 +177,12 @@ private Widget createPage1() { serverCounter = 0; 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() + .setBackground(DISPLAY.withOffset(-2, -2, 4, 4)).setSize(92, 20).setPos(10, 50)) .addChild( SlotWidget.phantom(phantomInventory, 1).setShiftClickPriority(1).setIgnoreStackSizeLimit(true) .setControlsAmount(true).setPos(28, 30)) diff --git a/src/test/java/com/gtnewhorizons/modularui/api/math/MathExpressionTest.java b/src/test/java/com/gtnewhorizons/modularui/api/math/MathExpressionTest.java new file mode 100644 index 0000000..137389d --- /dev/null +++ b/src/test/java/com/gtnewhorizons/modularui/api/math/MathExpressionTest.java @@ -0,0 +1,113 @@ +package com.gtnewhorizons.modularui.api.math; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +class MathExpressionTest { + + @Test + void NumbersBasic_Test() { + assertEquals(42, MathExpression.parseMathExpression("42")); + assertEquals(42, MathExpression.parseMathExpression(" 42 ")); + + assertEquals(123456, MathExpression.parseMathExpression("123,456")); + assertEquals(123456, MathExpression.parseMathExpression("123 456")); + assertEquals(123456, MathExpression.parseMathExpression("123_456")); + + assertEquals(123.456, MathExpression.parseMathExpression("123.456")); + } + + @Test + void ArithmeticBasic_Test() { + assertEquals(5, MathExpression.parseMathExpression("2+3")); + 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")); + + assertEquals(5, MathExpression.parseMathExpression("2 + 3")); + } + + @Test + void ArithmeticPriority_Test() { + assertEquals(4, MathExpression.parseMathExpression("2+3-1")); + assertEquals(14, MathExpression.parseMathExpression("2+3*4")); + assertEquals(10, MathExpression.parseMathExpression("2*3+4")); + assertEquals(7, MathExpression.parseMathExpression("2^3-1")); + assertEquals(13, MathExpression.parseMathExpression("1+2^3+4")); + + // a^b^c = a^(b^c) + assertEquals(262_144, MathExpression.parseMathExpression("4^3^2")); + } + + @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). + } + + @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")); + } + + @Test + void ScientificArithmetic_Test() { + assertEquals(4000, MathExpression.parseMathExpression("2*2e3")); + assertEquals(6000, MathExpression.parseMathExpression("2e3 * 3")); + assertEquals(-200, MathExpression.parseMathExpression("-2e2")); + assertEquals(1024, MathExpression.parseMathExpression("2^1e1")); + + // Not supported, but shouldn't fail. (2e2)e2 = 200e2 = 20_000. + assertEquals(20_000, MathExpression.parseMathExpression("2e2e2")); + } + + @Test + void SuffixesBasic_Test() { + assertEquals(2000, MathExpression.parseMathExpression("2k")); + assertEquals(3000, MathExpression.parseMathExpression("3K")); + assertEquals(4_000_000, MathExpression.parseMathExpression("4m")); + assertEquals(5_000_000, MathExpression.parseMathExpression("5M")); + assertEquals(6_000_000_000D, MathExpression.parseMathExpression("6b")); + assertEquals(7_000_000_000D, MathExpression.parseMathExpression("7B")); + assertEquals(8_000_000_000D, MathExpression.parseMathExpression("8g")); + assertEquals(9_000_000_000D, MathExpression.parseMathExpression("9G")); + 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(3000, MathExpression.parseMathExpression("3 k")); + } + + @Test + void SuffixesArithmetic_Test() { + assertEquals(2005, MathExpression.parseMathExpression("2k+5")); + assertEquals(2005, MathExpression.parseMathExpression("5+2k")); + assertEquals(4000, MathExpression.parseMathExpression("2k*2")); + assertEquals(4000, MathExpression.parseMathExpression("2*2k")); + assertEquals(-2000, MathExpression.parseMathExpression("-2k")); + + assertEquals(3_000_000, MathExpression.parseMathExpression("3kk")); + 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")); + + // Please don't do this. + assertEquals(20_000_000_000D, MathExpression.parseMathExpression("2e0.01k")); + } +}