diff options
author | Aaron <51387595+AzureAaron@users.noreply.github.com> | 2024-05-14 17:04:03 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-05-14 17:04:03 -0400 |
commit | 992ee43a9e5d78b9613f597923e20f0be4a49f63 (patch) | |
tree | 7768069aacbbfb255836fd131ca02235d6c56dfe | |
parent | cfff7e13191e8c70c8535b831a13b40ce2888ba6 (diff) | |
parent | 7edaa418067580434ef189a361a6802faabb3b1e (diff) | |
download | Skyblocker-992ee43a9e5d78b9613f597923e20f0be4a49f63.tar.gz Skyblocker-992ee43a9e5d78b9613f597923e20f0be4a49f63.tar.bz2 Skyblocker-992ee43a9e5d78b9613f597923e20f0be4a49f63.zip |
Merge pull request #686 from olim88/sign-calculator
Sign calculator
11 files changed, 460 insertions, 3 deletions
diff --git a/src/main/java/de/hysky/skyblocker/SkyblockerMod.java b/src/main/java/de/hysky/skyblocker/SkyblockerMod.java index 3c6f33a2..77b1ec2a 100644 --- a/src/main/java/de/hysky/skyblocker/SkyblockerMod.java +++ b/src/main/java/de/hysky/skyblocker/SkyblockerMod.java @@ -8,6 +8,7 @@ import de.hysky.skyblocker.config.ImageRepoLoader; import de.hysky.skyblocker.config.SkyblockerConfigManager; import de.hysky.skyblocker.debug.Debug; import de.hysky.skyblocker.skyblock.*; +import de.hysky.skyblocker.skyblock.calculators.CalculatorCommand; import de.hysky.skyblocker.skyblock.chat.ChatRuleAnnouncementScreen; import de.hysky.skyblocker.skyblock.chat.ChatRulesHandler; import de.hysky.skyblocker.skyblock.crimson.kuudra.Kuudra; @@ -132,6 +133,7 @@ public class SkyblockerMod implements ClientModInitializer { Shortcuts.init(); ChatRulesHandler.init(); ChatRuleAnnouncementScreen.init(); + CalculatorCommand.init(); DiscordRPCManager.init(); LividColor.init(); FishingHelper.init(); diff --git a/src/main/java/de/hysky/skyblocker/config/categories/UIAndVisualsCategory.java b/src/main/java/de/hysky/skyblocker/config/categories/UIAndVisualsCategory.java index c6936335..e6cd3c54 100644 --- a/src/main/java/de/hysky/skyblocker/config/categories/UIAndVisualsCategory.java +++ b/src/main/java/de/hysky/skyblocker/config/categories/UIAndVisualsCategory.java @@ -333,6 +333,28 @@ public class UIAndVisualsCategory { .build()) .build()) + //Input Calculator + .group(OptionGroup.createBuilder() + .name(Text.translatable("skyblocker.config.uiAndVisuals.inputCalculator")) + .collapsed(true) + .option(Option.<Boolean>createBuilder() + .name(Text.translatable("skyblocker.config.uiAndVisuals.inputCalculator.enabled")) + .description(OptionDescription.of(Text.translatable("skyblocker.config.uiAndVisuals.inputCalculator.enabled.@Tooltip"))) + .binding(defaults.uiAndVisuals.inputCalculator.enabled, + () -> config.uiAndVisuals.inputCalculator.enabled, + newValue -> config.uiAndVisuals.inputCalculator.enabled = newValue) + .controller(ConfigUtils::createBooleanController) + .build()) + .option(Option.<Boolean>createBuilder() + .name(Text.translatable("skyblocker.config.uiAndVisuals.inputCalculator.requiresEquals")) + .description(OptionDescription.of(Text.translatable("skyblocker.config.uiAndVisuals.inputCalculator.requiresEquals.@Tooltip"))) + .binding(defaults.uiAndVisuals.inputCalculator.requiresEquals, + () -> config.uiAndVisuals.inputCalculator.requiresEquals, + newValue -> config.uiAndVisuals.inputCalculator.requiresEquals = newValue) + .controller(ConfigUtils::createBooleanController) + .build()) + .build()) + //Flame Overlay .group(OptionGroup.createBuilder() .name(Text.translatable("skyblocker.config.uiAndVisuals.flameOverlay")) diff --git a/src/main/java/de/hysky/skyblocker/config/configs/UIAndVisualsConfig.java b/src/main/java/de/hysky/skyblocker/config/configs/UIAndVisualsConfig.java index 267dde14..03d300f4 100644 --- a/src/main/java/de/hysky/skyblocker/config/configs/UIAndVisualsConfig.java +++ b/src/main/java/de/hysky/skyblocker/config/configs/UIAndVisualsConfig.java @@ -54,6 +54,9 @@ public class UIAndVisualsConfig { public SearchOverlay searchOverlay = new SearchOverlay(); @SerialEntry + public InputCalculator inputCalculator = new InputCalculator(); + + @SerialEntry public FlameOverlay flameOverlay = new FlameOverlay(); public static class ChestValue { @@ -239,6 +242,14 @@ public class UIAndVisualsConfig { public List<String> auctionHistory = new ArrayList<>(); } + public static class InputCalculator { + @SerialEntry + public boolean enabled = true; + + @SerialEntry + public boolean requiresEquals = false; + } + public static class FlameOverlay { @SerialEntry public int flameHeight = 100; diff --git a/src/main/java/de/hysky/skyblocker/mixins/SignEditScreenMixin.java b/src/main/java/de/hysky/skyblocker/mixins/SignEditScreenMixin.java new file mode 100644 index 00000000..6706db58 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/mixins/SignEditScreenMixin.java @@ -0,0 +1,44 @@ +package de.hysky.skyblocker.mixins; + + +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.skyblock.calculators.SignCalculator; +import de.hysky.skyblocker.utils.Utils; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.ingame.AbstractSignEditScreen; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.Objects; + +@Mixin(AbstractSignEditScreen.class) +public abstract class SignEditScreenMixin { + @Shadow + @Final + private String[] messages; + + @Inject(method = "render", at = @At("HEAD")) + private void skyblocker$render(DrawContext context, int mouseX, int mouseY, float delta, CallbackInfo ci) { + //if the sign is being used to enter number send it to the sign calculator + if (Utils.isOnSkyblock() && SkyblockerConfigManager.get().uiAndVisuals.inputCalculator.enabled && Objects.equals(messages[1], "^^^^^^^^^^^^^^^")) { + SignCalculator.renderCalculator(context, messages[0], context.getScaledWindowWidth() / 2, 55); + } + } + + @Inject(method = "finishEditing", at = @At("HEAD")) + private void skyblocker$finishEditing(CallbackInfo ci) { + //if the sign is being used to enter number get number from calculator for if maths has been done + if (Utils.isOnSkyblock() && SkyblockerConfigManager.get().uiAndVisuals.inputCalculator.enabled && Objects.equals(messages[1], "^^^^^^^^^^^^^^^")) { + boolean isPrice = messages[2].contains("price"); + String value = SignCalculator.getNewValue(isPrice); + if (value.length() >= 15) { + value = value.substring(0, 15); + } + messages[0] = value; + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/auction/EditBidPopup.java b/src/main/java/de/hysky/skyblocker/skyblock/auction/EditBidPopup.java index 9d460803..f96e3231 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/auction/EditBidPopup.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/auction/EditBidPopup.java @@ -1,5 +1,7 @@ package de.hysky.skyblocker.skyblock.auction; +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.skyblock.calculators.SignCalculator; import de.hysky.skyblocker.utils.render.gui.AbstractPopupScreen; import net.minecraft.block.entity.SignBlockEntity; import net.minecraft.client.MinecraftClient; @@ -9,7 +11,6 @@ import net.minecraft.network.packet.c2s.play.UpdateSignC2SPacket; import net.minecraft.text.Style; import net.minecraft.text.Text; import org.jetbrains.annotations.NotNull; -import org.lwjgl.glfw.GLFW; public class EditBidPopup extends AbstractPopupScreen { private DirectionalLayoutWidget layout = DirectionalLayoutWidget.vertical(); @@ -55,6 +56,9 @@ public class EditBidPopup extends AbstractPopupScreen { public void renderBackground(DrawContext context, int mouseX, int mouseY, float delta) { super.renderBackground(context, mouseX, mouseY, delta); drawPopupBackground(context, layout.getX(), layout.getY(), layout.getWidth(), layout.getHeight()); + if (SkyblockerConfigManager.get().uiAndVisuals.inputCalculator.enabled) { + SignCalculator.renderCalculator(context, textFieldWidget.getText(), context.getScaledWindowWidth() / 2, textFieldWidget.getY() - 8); + } } private boolean isStringGood(String s) { @@ -69,8 +73,13 @@ public class EditBidPopup extends AbstractPopupScreen { } private void done(ButtonWidget widget) { - if (!isStringGood(textFieldWidget.getText().trim())) return; - sendPacket(textFieldWidget.getText().trim()); + if (SkyblockerConfigManager.get().uiAndVisuals.inputCalculator.enabled) { + if (!isStringGood(SignCalculator.getNewValue(false))) return; + sendPacket(SignCalculator.getNewValue(false)); + } else { + if (!isStringGood(textFieldWidget.getText().trim())) return; + sendPacket(textFieldWidget.getText().trim()); + } this.close(); } diff --git a/src/main/java/de/hysky/skyblocker/skyblock/calculators/CalculatorCommand.java b/src/main/java/de/hysky/skyblocker/skyblock/calculators/CalculatorCommand.java new file mode 100644 index 00000000..d103bcdd --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/calculators/CalculatorCommand.java @@ -0,0 +1,56 @@ +package de.hysky.skyblocker.skyblock.calculators; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.StringArgumentType; +import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.utils.Calculator; +import de.hysky.skyblocker.utils.Constants; +import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; +import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; +import net.minecraft.client.MinecraftClient; +import net.minecraft.command.CommandRegistryAccess; +import net.minecraft.text.MutableText; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; + +import java.text.NumberFormat; + +import static com.mojang.brigadier.arguments.StringArgumentType.getString; +import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.argument; +import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.literal; + +public class CalculatorCommand { + private static final MinecraftClient CLIENT = MinecraftClient.getInstance(); + private static final NumberFormat FORMATTER = NumberFormat.getInstance(); + + public static void init() { + ClientCommandRegistrationCallback.EVENT.register(CalculatorCommand::calculate); + } + + private static void calculate(CommandDispatcher<FabricClientCommandSource> dispatcher, CommandRegistryAccess registryAccess) { + dispatcher.register(literal(SkyblockerMod.NAMESPACE) + .then(literal("calculate") + .then(argument("equation", StringArgumentType.greedyString()) + .executes(context -> doCalculation(getString(context, "equation"))) + ) + ) + ); + } + + private static int doCalculation(String calculation) { + MutableText text = Constants.PREFIX.get(); + try { + text.append(Text.literal(FORMATTER.format(Calculator.calculate(calculation))).formatted(Formatting.GREEN)); + } catch (UnsupportedOperationException e) { + text.append(Text.translatable("skyblocker.config.uiAndVisuals.inputCalculator.invalidEquation").formatted(Formatting.RED)); + } + + if (CLIENT == null || CLIENT.player == null) { + return 0; + } + + CLIENT.player.sendMessage(text, false); + return Command.SINGLE_SUCCESS; + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/calculators/SignCalculator.java b/src/main/java/de/hysky/skyblocker/skyblock/calculators/SignCalculator.java new file mode 100644 index 00000000..dc51e48c --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/calculators/SignCalculator.java @@ -0,0 +1,66 @@ +package de.hysky.skyblocker.skyblock.calculators; + +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.utils.Calculator; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; + +import java.text.NumberFormat; + +public class SignCalculator { + private static final MinecraftClient CLIENT = MinecraftClient.getInstance(); + private static final NumberFormat FORMATTER = NumberFormat.getInstance(); + + private static String lastInput; + private static double output; + + public static void renderCalculator(DrawContext context, String message, int renderX, int renderY) { + if (SkyblockerConfigManager.get().uiAndVisuals.inputCalculator.requiresEquals && !message.startsWith("=")) { + output = -1; + lastInput = message; + return; + } + if (message.startsWith("=")) { + message = message.substring(1); + } + //only update output if new input + if (!message.equals(lastInput)) { // + try { + output = Calculator.calculate(message); + } catch (Exception e) { + output = -1; + } + } + + render(context, message, renderX, renderY); + + lastInput = message; + } + + public static String getNewValue(Boolean isPrice) { + if (output == -1) { + //if mode is not activated or just invalid equation return what the user typed in + return lastInput; + } + + //price can except decimals and exponents + if (isPrice) { + return FORMATTER.format(output); + } + //amounts want an integer number so round + return Long.toString(Math.round(output)); + } + + private static void render(DrawContext context, String input, int renderX, int renderY) { + Text text; + if (output == -1) { + text = Text.translatable("skyblocker.config.uiAndVisuals.inputCalculator.invalidEquation").formatted(Formatting.RED); + } else { + text = Text.literal(input + " = " + FORMATTER.format(output)).formatted(Formatting.GREEN); + } + + context.drawCenteredTextWithShadow(CLIENT.textRenderer, text, renderX, renderY, 0xFFFFFFFF); + } +} diff --git a/src/main/java/de/hysky/skyblocker/utils/Calculator.java b/src/main/java/de/hysky/skyblocker/utils/Calculator.java new file mode 100644 index 00000000..7b0baaf6 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/Calculator.java @@ -0,0 +1,208 @@ +package de.hysky.skyblocker.utils; + +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class Calculator { + public enum TokenType { + NUMBER, OPERATOR, L_PARENTHESIS, R_PARENTHESIS + } + + public static class Token { + public TokenType type; + String value; + int tokenLength; + } + + private static final Pattern NUMBER_PATTERN = Pattern.compile("(\\d+\\.?\\d*)([sekmbt]?)"); + private static final Map<String, Long> MAGNITUDE_VALUES = Map.of( + "s", 64L, + "e", 160L, + "k", 1_000L, + "m", 1_000_000L, + "b", 1_000_000_000L, + "t", 1_000_000_000_000L + ); + + private static List<Token> lex(String input) { + List<Token> tokens = new ArrayList<>(); + input = input.replace(" ", "").toLowerCase().replace("x", "*"); + int i = 0; + while (i < input.length()) { + Token token = new Token(); + switch (input.charAt(i)) { + case '+', '-', '*', '/' -> { + token.type = TokenType.OPERATOR; + token.value = String.valueOf(input.charAt(i)); + token.tokenLength = 1; + } + + case '(' -> { + token.type = TokenType.L_PARENTHESIS; + token.value = String.valueOf(input.charAt(i)); + token.tokenLength = 1; + //add implicit multiplication when there is a number before brackets + if (!tokens.isEmpty()) { + TokenType lastType = tokens.getLast().type; + if (lastType == TokenType.R_PARENTHESIS || lastType == TokenType.NUMBER) { + Token mutliplyToken = new Token(); + mutliplyToken.type = TokenType.OPERATOR; + mutliplyToken.value = "*"; + tokens.add(mutliplyToken); + } + } + } + + case ')' -> { + token.type = TokenType.R_PARENTHESIS; + token.value = String.valueOf(input.charAt(i)); + token.tokenLength = 1; + } + + default -> { + token.type = TokenType.NUMBER; + Matcher numberMatcher = NUMBER_PATTERN.matcher(input.substring(i)); + if (!numberMatcher.find()) {//invalid value to lex + throw new UnsupportedOperationException("invalid character"); + } + int end = numberMatcher.end(); + token.value = input.substring(i, i + end); + token.tokenLength = end; + } + } + tokens.add(token); + + i += token.tokenLength; + } + + return tokens; + } + + /** + * This is an implementation of the shunting yard algorithm to convert the equation to reverse polish notation + * + * @param tokens equation in infix notation order + * @return equation in RPN order + */ + private static List<Token> shunt(List<Token> tokens) { + Deque<Token> operatorStack = new ArrayDeque<>(); + List<Token> outputQueue = new ArrayList<>(); + + for (Token shuntingToken : tokens) { + switch (shuntingToken.type) { + case NUMBER -> outputQueue.add(shuntingToken); + case OPERATOR -> { + int precedence = getPrecedence(shuntingToken.value); + while (!operatorStack.isEmpty()) { + Token leftToken = operatorStack.peek(); + if (leftToken.type == TokenType.L_PARENTHESIS) { + break; + } + assert (leftToken.type == TokenType.OPERATOR); + int leftPrecedence = getPrecedence(leftToken.value); + if (leftPrecedence >= precedence) { + outputQueue.add(operatorStack.pop()); + continue; + } + break; + } + operatorStack.push(shuntingToken); + } + case L_PARENTHESIS -> operatorStack.push(shuntingToken); + case R_PARENTHESIS -> { + while (true) { + if (operatorStack.isEmpty()) { + throw new UnsupportedOperationException("Unbalanced left parenthesis"); + } + Token leftToken = operatorStack.pop(); + if (leftToken.type == TokenType.L_PARENTHESIS) { + break; + } + outputQueue.add(leftToken); + } + } + } + } + //empty the operator stack + while (!operatorStack.isEmpty()) { + Token leftToken = operatorStack.pop(); + if (leftToken.type == TokenType.L_PARENTHESIS) { + //technically unbalanced left parenthesis error but just assume they are close after the equation and ignore them from here + continue; + } + outputQueue.add(leftToken); + } + + return outputQueue.stream().toList(); + } + + private static int getPrecedence(String operator) { + switch (operator) { + case "+", "-" -> { + return 0; + } + case "*", "/" -> { + return 1; + } + default -> throw new UnsupportedOperationException("Invalid operator"); + } + } + + /** + * @param tokens list of Tokens in reverse polish notation + * @return answer to equation + */ + private static double evaluate(List<Token> tokens) { + Deque<Double> values = new ArrayDeque<>(); + for (Token token : tokens) { + switch (token.type) { + case NUMBER -> values.push(calculateValue(token.value)); + case OPERATOR -> { + double right = values.pop(); + double left = values.pop(); + switch (token.value) { + case "+" -> values.push(left + right); + case "-" -> values.push(left - right); + case "/" -> { + if (right == 0) { + throw new UnsupportedOperationException("Can not divide by 0"); + } + values.push(left / right); + } + case "*" -> values.push(left * right); + } + } + case L_PARENTHESIS, R_PARENTHESIS -> throw new UnsupportedOperationException("Equation is not in RPN"); + } + } + if (values.isEmpty()) { + throw new UnsupportedOperationException("Equation is empty"); + } + return values.pop(); + } + + private static double calculateValue(String value) { + Matcher numberMatcher = NUMBER_PATTERN.matcher(value.toLowerCase()); + if (!numberMatcher.matches()) { + throw new UnsupportedOperationException("Invalid number"); + } + double number = Double.parseDouble(numberMatcher.group(1)); + String magnitude = numberMatcher.group(2); + + if (!magnitude.isEmpty()) { + if (!MAGNITUDE_VALUES.containsKey(magnitude)) {//its invalid if its another letter + throw new UnsupportedOperationException("Invalid magnitude"); + } + number *= MAGNITUDE_VALUES.get(magnitude); + } + + return number; + } + + public static double calculate(String equation) { + //custom bit for replacing purse with its value + equation = equation.toLowerCase().replaceAll("p(urse)?", String.valueOf(Utils.getPurse())); + return evaluate(shunt(lex(equation))); + } +} diff --git a/src/main/resources/assets/skyblocker/lang/en_us.json b/src/main/resources/assets/skyblocker/lang/en_us.json index 6d2433c0..2549f80e 100644 --- a/src/main/resources/assets/skyblocker/lang/en_us.json +++ b/src/main/resources/assets/skyblocker/lang/en_us.json @@ -536,6 +536,13 @@ "skyblocker.config.uiAndVisuals.flameOverlay.flameOpacity": "Flame Opacity", "skyblocker.config.uiAndVisuals.flameOverlay.flameOpacity.@Tooltip": "100% default opacity\n0% off", + "skyblocker.config.uiAndVisuals.inputCalculator": "Input Calculator", + "skyblocker.config.uiAndVisuals.inputCalculator.enabled": "Enable Sign Calculator", + "skyblocker.config.uiAndVisuals.inputCalculator.enabled.@Tooltip": "Enables the ability for you to do calculations when inputting values such as price for the ah.\n Key:\n S = 64\n E = 160\n K = 1,000\n M = 1,000,000\n B = 1,000,000,000\n\n purse/P = current purse value", + "skyblocker.config.uiAndVisuals.inputCalculator.requiresEquals": "Only show with \"=\".", + "skyblocker.config.uiAndVisuals.inputCalculator.requiresEquals.@Tooltip": "Only show the calculator when the message start with \"=\".", + "skyblocker.config.uiAndVisuals.inputCalculator.invalidEquation": "Invalid Equation", + "skyblocker.config.uiAndVisuals.itemCooldown": "Item Cooldown", "skyblocker.config.uiAndVisuals.itemCooldown.enableItemCooldowns": "Enable Item Cooldown", diff --git a/src/main/resources/skyblocker.mixins.json b/src/main/resources/skyblocker.mixins.json index 0032a557..828cc206 100644 --- a/src/main/resources/skyblocker.mixins.json +++ b/src/main/resources/skyblocker.mixins.json @@ -32,6 +32,7 @@ "PlayerSkinTextureMixin", "RenderFishMixin", "ScoreboardMixin", + "SignEditScreenMixin", "SocialInteractionsPlayerListWidgetMixin", "WindowMixin", "WorldRendererMixin", diff --git a/src/test/java/de/hysky/skyblocker/utils/CalculatorTest.java b/src/test/java/de/hysky/skyblocker/utils/CalculatorTest.java new file mode 100644 index 00000000..c29efdf2 --- /dev/null +++ b/src/test/java/de/hysky/skyblocker/utils/CalculatorTest.java @@ -0,0 +1,31 @@ +package de.hysky.skyblocker.utils; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class CalculatorTest { + @Test + void testShorthands() { + Assertions.assertEquals(Calculator.calculate("1k"), 1000); + Assertions.assertEquals(Calculator.calculate("0.12k"), 120); + Assertions.assertEquals(Calculator.calculate("1k + 0.12k"), 1120); + Assertions.assertEquals(Calculator.calculate("1 + 1s + 1k + 1m + 1b"), 1001001065); + } + + @Test + void testPrecedence() { + Assertions.assertEquals(Calculator.calculate("5 + 2 * 2"), 9); + Assertions.assertEquals(Calculator.calculate("5 - 2 / 2"), 4); + Assertions.assertEquals(Calculator.calculate("5 * (1 + 2)"), 15); + } + + @Test + void testImplicitMultiplication() { + Assertions.assertEquals(Calculator.calculate("5(2 + 2)"), 20); + } + + @Test + void testImplicitClosingParenthesis() { + Assertions.assertEquals(Calculator.calculate("5(2 + 2"), 20); + } +} |