From 8911efddc3fad44a04cf1f2de11838ace9375fb1 Mon Sep 17 00:00:00 2001 From: RTTV Date: Sun, 4 Jun 2023 18:44:25 -0400 Subject: [PATCH] added a completely new math engine (nibble), which is extremely fast --- gradle.properties | 2 +- src/main/java/ca/rttv/chatcalc/ChatCalc.java | 38 +- src/main/java/ca/rttv/chatcalc/Config.java | 35 +- .../ca/rttv/chatcalc/FunctionParameter.java | 4 +- .../java/ca/rttv/chatcalc/MathEngine.java | 391 +---------------- .../ca/rttv/chatcalc/NibbleMathEngine.java | 178 ++++++++ .../ca/rttv/chatcalc/TokenizedMathEngine.java | 395 ++++++++++++++++++ .../rttv/chatcalc/tokens/FunctionToken.java | 5 +- .../rttv/chatcalc/tokens/OperatorToken.java | 13 +- .../resources/assets/chatcalc/lang/en_us.json | 1 + 10 files changed, 633 insertions(+), 429 deletions(-) create mode 100644 src/main/java/ca/rttv/chatcalc/NibbleMathEngine.java create mode 100644 src/main/java/ca/rttv/chatcalc/TokenizedMathEngine.java diff --git a/gradle.properties b/gradle.properties index d9ab7ad..60d60e9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,7 +8,7 @@ org.gradle.jvmargs=-Xmx1G loader_version=0.14.9 # Mod Properties - mod_version = 3.0.14 + mod_version = 3.0.15 maven_group = ca.rttv archives_base_name = chatcalc diff --git a/src/main/java/ca/rttv/chatcalc/ChatCalc.java b/src/main/java/ca/rttv/chatcalc/ChatCalc.java index f6ca7bb..8bf92da 100644 --- a/src/main/java/ca/rttv/chatcalc/ChatCalc.java +++ b/src/main/java/ca/rttv/chatcalc/ChatCalc.java @@ -2,6 +2,7 @@ import ca.rttv.chatcalc.tokens.Token; import com.mojang.logging.LogUtils; +import net.fabricmc.loader.api.FabricLoader; import net.minecraft.client.MinecraftClient; import net.minecraft.client.gui.widget.TextFieldWidget; import net.minecraft.text.Text; @@ -18,17 +19,14 @@ public class ChatCalc { public static final Logger LOGGER = LogUtils.getLogger(); public static final Pattern NUMBER = Pattern.compile("[-+]?\\d+(\\.\\d+)?"); public static final String SEPARATOR = ";"; + public static final char SEPARATOR_CHAR = ';'; @Contract(value = "_->_", mutates = "param1") public static boolean tryParse(TextFieldWidget field) { final MinecraftClient client = MinecraftClient.getInstance(); String originalText = field.getText(); int cursor = field.getCursor(); - String text = originalText.substring(0, cursor); - if ((text.equals("config?") || text.equals("cfg?") || text.equals("?")) && client.player != null) { - client.player.sendMessage(Text.translatable("chatcalc.config.description")); - return false; - } + String text = ChatHelper.getWord(originalText, cursor); String[] split = text.split("="); if (split.length == 2) { if (Config.JSON.has(split[0])) { @@ -36,7 +34,7 @@ public static boolean tryParse(TextFieldWidget field) { Config.refreshJson(); return ChatHelper.replaceWord(field, ""); } else { - Optional, String[]>> parsedFunction = Config.parseFunction(text); + Optional> parsedFunction = Config.parseFunction(text); if (parsedFunction.isPresent()) { Config.FUNCTIONS.put(parsedFunction.get().getA(), new Pair<>(parsedFunction.get().getB(), parsedFunction.get().getC())); Config.refreshJson(); @@ -49,24 +47,36 @@ public static boolean tryParse(TextFieldWidget field) { client.player.sendMessage(Text.translatable("chatcalc." + split[0].substring(0, split[0].length() - 1) + ".description")); return false; } - - String word = ChatHelper.getWord(originalText, cursor); - if (NUMBER.matcher(word).matches()) { + + if ((text.equals("config?") || text.equals("cfg?") || text.equals("?")) && client.player != null) { + client.player.sendMessage(Text.translatable("chatcalc.config.description")); + return false; + } else if (NUMBER.matcher(text).matches()) { return false; } else { boolean add = false; - if (word.endsWith("=")) { - word = word.substring(0, word.length() - 1); + if (text.endsWith("=")) { + text = text.substring(0, text.length() - 1); add = true; } try { - String solution = Config.getDecimalFormat().format(MathEngine.eval(word)); + long start = System.nanoTime(); + double result = Config.makeEngine().eval(text, Optional.empty()); + double us = (System.nanoTime() - start) / 1_000.0; + if (FabricLoader.getInstance().isDevelopmentEnvironment()) { + MinecraftClient.getInstance().player.sendMessage(Text.literal("Took " + us + "µs to parse equation"), true); + MinecraftClient.getInstance().player.sendMessage(Text.literal("Took " + us + "µs to parse equation"), false); + } + String solution = Config.getDecimalFormat().format(result); // so fast that creating a new one everytime doesn't matter, also lets me use fields + if (solution.equals("-0")) { + solution = "0"; + } Config.saveToChatHud(originalText); Config.saveToClipboard(originalText); return add ? ChatHelper.addWordAfterIndex(field, solution) : ChatHelper.replaceWord(field, solution); - } catch (Exception e) { + } catch (Throwable t) { if (Config.logExceptions()) { - LOGGER.error("ChatCalc Parse Error: ", e); + LOGGER.error("ChatCalc Parse Error: ", t); } return false; } diff --git a/src/main/java/ca/rttv/chatcalc/Config.java b/src/main/java/ca/rttv/chatcalc/Config.java index 324b357..7462c8b 100644 --- a/src/main/java/ca/rttv/chatcalc/Config.java +++ b/src/main/java/ca/rttv/chatcalc/Config.java @@ -1,7 +1,5 @@ package ca.rttv.chatcalc; -import ca.rttv.chatcalc.tokens.NumberToken; -import ca.rttv.chatcalc.tokens.Token; import com.google.common.collect.ImmutableMap; import com.google.gson.*; import net.minecraft.client.MinecraftClient; @@ -10,14 +8,16 @@ import java.io.*; import java.text.DecimalFormat; -import java.util.*; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; import java.util.stream.Collectors; public class Config { public static final JsonObject JSON; public static final Gson GSON; public static final File CONFIG_FILE; - public static final Map, String[]>> FUNCTIONS; + public static final Map> FUNCTIONS; public static final ImmutableMap DEFAULTS; static { @@ -28,6 +28,7 @@ public class Config { .put("log_exceptions", "false") .put("copy_type", "none") .put("calculate_last", "true") + .put("engine", "nibble") .build(); CONFIG_FILE = new File(".", "config/chatcalc.json"); GSON = new GsonBuilder().setPrettyPrinting().create(); @@ -72,7 +73,7 @@ public static boolean logExceptions() { public static void refreshJson() { try { FileWriter writer = new FileWriter(CONFIG_FILE); - JSON.add("functions", FUNCTIONS.entrySet().stream().map(x -> x.getKey() + "(" + String.join(ChatCalc.SEPARATOR, x.getValue().getRight()) + ")=" + x.getValue().getLeft().stream().map(Object::toString).collect(Collectors.joining())).collect(JsonArray::new, JsonArray::add, JsonArray::addAll)); + JSON.add("functions", FUNCTIONS.entrySet().stream().map(x -> x.getKey() + "(" + String.join(ChatCalc.SEPARATOR, x.getValue().getRight()) + ")=" + x.getValue().getLeft()).collect(JsonArray::new, JsonArray::add, JsonArray::addAll)); writer.write(GSON.toJson(JSON)); JSON.remove("functions"); writer.close(); @@ -99,7 +100,7 @@ public static void readJson() { } catch (Exception ignored) { } } - public static Optional, String[]>> parseFunction(String function) { + public static Optional> parseFunction(String function) { int functionNameEnd = function.indexOf('('); if (functionNameEnd > 0) { String functionName = function.substring(0, functionNameEnd); @@ -112,11 +113,7 @@ public static Optional, String[]>> parseFunction(Str } } String rest = function.substring(paramsEnd + 2); - try { - List tokens = MathEngine.tokenize(rest); - return Optional.of(new Triplet<>(functionName, tokens, params)); -// System.out.printf("fn: %s, params: %s, rest: %s%n", functionName, params, rest); - } catch (Exception ignored) { } + return Optional.of(new Triplet<>(functionName, rest, params)); } } return Optional.empty(); @@ -138,16 +135,12 @@ public static double func(String name, double... values) { if (values.length != FUNCTIONS.get(name).getRight().length) { throw new IllegalArgumentException(); } - List tokens = new ArrayList<>(FUNCTIONS.get(name).getLeft()); + String input = FUNCTIONS.get(name).getLeft(); FunctionParameter[] parameters = new FunctionParameter[values.length]; for (int i = 0; i < parameters.length; i++) { parameters[i] = new FunctionParameter(FUNCTIONS.get(name).getRight()[i], values[i]); } - MathEngine.simplify(tokens, false, Optional.of(parameters)); - if (tokens.get(0) instanceof NumberToken numberToken) { - return numberToken.val; - } - throw new IllegalArgumentException(); + return Config.makeEngine().eval(input, Optional.of(parameters)); } else { if (values.length == 0) { throw new IllegalArgumentException(); @@ -162,4 +155,12 @@ public static void saveToClipboard(String input) { client.keyboard.setClipboard(input); } } + + public static MathEngine makeEngine() { + if (JSON.get("engine").getAsString().equals("token")) { + return new TokenizedMathEngine(); + } else { + return new NibbleMathEngine(); + } + } } diff --git a/src/main/java/ca/rttv/chatcalc/FunctionParameter.java b/src/main/java/ca/rttv/chatcalc/FunctionParameter.java index 2399097..27140bc 100644 --- a/src/main/java/ca/rttv/chatcalc/FunctionParameter.java +++ b/src/main/java/ca/rttv/chatcalc/FunctionParameter.java @@ -1,5 +1,3 @@ package ca.rttv.chatcalc; -public record FunctionParameter(String name, double value) { - -} +public record FunctionParameter(String name, double value) {} diff --git a/src/main/java/ca/rttv/chatcalc/MathEngine.java b/src/main/java/ca/rttv/chatcalc/MathEngine.java index 3f88db2..9ea678a 100644 --- a/src/main/java/ca/rttv/chatcalc/MathEngine.java +++ b/src/main/java/ca/rttv/chatcalc/MathEngine.java @@ -1,394 +1,7 @@ package ca.rttv.chatcalc; -import ca.rttv.chatcalc.tokens.*; -import com.mojang.logging.LogUtils; -import net.minecraft.client.MinecraftClient; -import net.minecraft.text.LiteralTextContent; -import net.minecraft.text.MutableText; -import net.minecraft.text.Text; -import org.jetbrains.annotations.Contract; -import org.slf4j.Logger; - -import java.util.ArrayList; -import java.util.List; import java.util.Optional; -import java.util.stream.Collectors; - -public class MathEngine { - public static final Logger LOGGER = LogUtils.getLogger(); - - @Contract(value = "!null->_", pure = true) - public static double eval(String input) { - MinecraftClient client = MinecraftClient.getInstance(); - List tokens = tokenize(input); - { - if (Config.debugTokens() && client.player != null) { - LOGGER.info(tokens.stream().map(Object::toString).collect(Collectors.joining())); - - MutableText text = MutableText.of(new LiteralTextContent("§r")); - text.getSiblings().addAll(tokens.stream().map(Token::toText).toList()); - text.getSiblings().add(Text.literal("§r")); - client.player.sendMessage(text); - } - } // print to console for debug - - simplify(tokens, false, Optional.empty()); - if (tokens.size() > 0 && tokens.get(0) instanceof NumberToken numberToken) { - return numberToken.val; - } - throw new IllegalArgumentException(); - } - - @Contract(value = "!null->!null", pure = true) - public static List tokenize(String input) { - // right bracketing isn't required I just need to do left-bracketing - input = input.toLowerCase() - .replace("**", "^") - .replaceAll(",", ""); - - int depth = 0; - for (int i = 0; i < input.length(); i++) { - char c = input.charAt(i); - if (c == ')') { - depth--; - } else if (c == '(') { - depth++; - } - } - if (depth < 0) { - input = "(".repeat(-depth).concat(input); - } else if (depth > 0) { - input = input.concat(")".repeat(depth)); - } - - List tokens = new ArrayList<>(input.length() >> 1); // just a guess - { - Optional> currentType = Optional.empty(); - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < input.length(); i++) { - char c = input.charAt(i); - - if (c == ';') { // separator (for function calls) - if (sb.length() > 0) { - makeToken(currentType, sb.toString()).ifPresent(tokens::add); - } - sb = new StringBuilder().append(c); - currentType = Optional.of(SeparatorToken.class); - makeToken(currentType, sb.toString()).ifPresent(tokens::add); - sb = new StringBuilder(); - continue; - } - - if (isNumber(c) && (currentType.isEmpty() || currentType.get() != FunctionToken.class)) { // this exclusion is so log_ custom bases work - sb.append(c); - currentType = Optional.of(NumberToken.class); - continue; - } - - if (isOperator(c)) { - if (sb.length() > 0) { // this segment on every one of these is to finish a number token - makeToken(currentType, sb.toString()).ifPresent(tokens::add); - } - sb = new StringBuilder().append(c); // this resets it after the number token has been done - currentType = Optional.of(OperatorToken.class); - makeToken(currentType, sb.toString()).ifPresent(tokens::add); - sb = new StringBuilder(); - continue; - } - - if (isBracket(c)) { - if (sb.length() > 0) { // this segment on every one of these is to finish a number token - makeToken(currentType, sb.toString()).ifPresent(tokens::add); - } - sb = new StringBuilder().append(c); // this resets it after the number token has been done - currentType = Optional.of(BracketToken.class); - makeToken(currentType, sb.toString()).ifPresent(tokens::add); - sb = new StringBuilder(); - continue; - } - - if (isAbs(c)) { - if (sb.length() > 0) { // this segment on every one of these is to finish a number token - makeToken(currentType, sb.toString()).ifPresent(tokens::add); - } - sb = new StringBuilder().append(c); // this resets it after the number token has been done - currentType = Optional.of(AbsToken.class); - makeToken(currentType, sb.toString()).ifPresent(tokens::add); - sb = new StringBuilder(); - continue; - } - - if (sb.length() > 0 && !currentType.equals(Optional.of(FunctionToken.class))) { - makeToken(currentType, sb.toString()).ifPresent(tokens::add); - sb = new StringBuilder(); - } - sb.append(c); // if something is not a member of another type, it just assumes it's an unknown function - currentType = Optional.of(FunctionToken.class); - switch (sb.toString()) { - case "e" -> { - tokens.add(new NumberToken(Math.E)); - currentType = Optional.empty(); - sb = new StringBuilder(); - } - case "pi" -> { - tokens.add(new NumberToken(Math.PI)); - currentType = Optional.empty(); - sb = new StringBuilder(); - } - case "tau" -> { - tokens.add(new NumberToken(6.28318530717958647692528676655900577)); // overkill but idc - currentType = Optional.empty(); - sb = new StringBuilder(); - } - } - } - - if (sb.length() > 0) { // this segment on every one of these is to finish a number token - makeToken(currentType, sb.toString()).ifPresent(tokens::add); - } - } // parse to tokens - { - boolean isOpeningAbsToken = true; // inverts between for abs tokens, represents if its opening or closing - for (int i = 0; i < tokens.size() - 1; i++) { - Token token = tokens.get(i); - Token number = tokens.get(i + 1); - if (token instanceof OperatorToken operatorToken && (operatorToken.val == 43 || operatorToken.val == 45) && number instanceof NumberToken numberToken) { - double signed = operatorToken.val == 45 ? -numberToken.val : numberToken.val; - if (i == 0) { // if im an operator, and the next is a number, of course we should modify that - tokens.set(i + 1, new NumberToken(signed)); - tokens.remove(i); // yes, we should skip the next one - continue; - } - - Token previous = tokens.get(i - 1); - if (previous instanceof OperatorToken) { - tokens.set(i + 1, new NumberToken(signed)); - //noinspection SuspiciousListRemoveInLoop -- checked, yes, we should skip the next one - tokens.remove(i); - } else if (previous instanceof BracketToken bracketToken && bracketToken.isOpen) { - tokens.set(i + 1, new NumberToken(signed)); - //noinspection SuspiciousListRemoveInLoop -- checked, yes, we should skip the next one - tokens.remove(i); - } else if (previous instanceof AbsToken) { - if (isOpeningAbsToken) { - tokens.set(i + 1, new NumberToken(signed)); - //noinspection SuspiciousListRemoveInLoop -- checked, yes, we should skip the next one - tokens.remove(i); - } - isOpeningAbsToken = !isOpeningAbsToken; - } - } - } - } // convert - + signs on numbers when appropriate - return tokens; - } - - @Contract(value = "_,_,_->_", mutates = "param1") - public static void simplify(List tokens, boolean abs, Optional params) { - final MinecraftClient client = MinecraftClient.getInstance(); - - for (int i = 0; i < tokens.size(); i++) { - Token token = tokens.get(i); - if (token instanceof FunctionToken functionToken) { - if (params.isPresent()) { // since 'a' will be considered a function, I have to detect if it is, then convert it into its constant - boolean any = true; - a: - while (any) { - any = false; - for (FunctionParameter param : params.get()) { - if (((FunctionToken) tokens.get(i)).func.startsWith(param.name())) { - any = true; - String name = ((FunctionToken) tokens.get(i)).func.substring(param.name().length()); - tokens.set(i, new NumberToken(param.value())); // yes this does reverse it, but at the end of the day its multiplication, so it doesn't matter. - if (name.length() > 0) { - tokens.add(i, new FunctionToken(name)); - } else { - break a; - } - break; - } - } - } - } - if (functionToken.func.length() == 1 && isPos(functionToken.func.charAt(0)) && client.player != null) { - tokens.set(i, new NumberToken(switch (functionToken.func.charAt(0)) { - case 'x' -> client.player.getX(); - case 'y' -> client.player.getY(); - case 'z' -> client.player.getZ(); - default -> throw new IllegalArgumentException(); - })); - } - } - } - - for (int i = 0; i < tokens.size(); i++) { - Token token = tokens.get(i); - if (token instanceof BracketToken bracketToken) { - //noinspection SuspiciousListRemoveInLoop -- checked - tokens.remove(i); - if (bracketToken.isOpen) { - simplify(tokens.subList(i, tokens.size()), false, params); - } else { - simplify(tokens.subList(0, i), false, params); - return; - } - } else if (token instanceof AbsToken absToken) { // absolute value - if (abs) { - Token first = tokens.get(0); - if (first instanceof NumberToken numberToken) { - tokens.set(i, new NumberToken(absToken.apply(numberToken.val))); - return; - } - return; - } else { - simplify(tokens.subList(i, tokens.size()), true, params); - } - } else if (token instanceof FunctionToken functionToken) { // function - //noinspection SuspiciousListRemoveInLoop -- checked - tokens.remove(i); // thyself - double exponent = 1.0; - if (tokens.get(i) instanceof OperatorToken operatorToken && operatorToken.val == 94) { - //noinspection SuspiciousListRemoveInLoop -- checked - tokens.remove(i); // exponent operator - if (tokens.get(i) instanceof NumberToken numberToken) { - exponent = numberToken.val; - } else if (tokens.get(i) instanceof BracketToken bracketToken) { - if (!bracketToken.isOpen) { - throw new IllegalArgumentException(); - } else { - tokens.remove(i); // opening bracket for exponent - simplify(tokens.subList(i, tokens.size()), false, params); - if (!(tokens.get(i) instanceof NumberToken numberToken)) { - throw new IllegalArgumentException(); - } - exponent = numberToken.val; - } - } else { - throw new IllegalArgumentException(); - } - } - //noinspection SuspiciousListRemoveInLoop -- checked - tokens.remove(i); // number - simplify(tokens.subList(i, tokens.size()), false, params); - int end = tokens.size(); // tricky code, automatically the end is tokens.size() if there is no separator, somehow, extra caution I guess. - boolean lastWasSeparator = true; - for (int j = i; j < end; j++) { - if (tokens.get(j) instanceof SeparatorToken) { - if (lastWasSeparator) { - throw new IllegalArgumentException(); - } else { - lastWasSeparator = true; - } - } else if (tokens.get(j) instanceof NumberToken) { - if (lastWasSeparator) { - lastWasSeparator = false; - } else { - end = j; - break; - } - } else { - if (lastWasSeparator) { - throw new IllegalArgumentException(); - } else { - end = j; - break; - } - } - } - if (lastWasSeparator) { - throw new IllegalArgumentException(); - } - double[] values = new double[(end - i + 1) / 2]; - for (int k = 0, j = i; j < end; ) { - values[k] = ((NumberToken) tokens.get(j)).val; // safe cast because of checks above - k++; - j += 2; - } - double output = Math.pow(functionToken.apply(values), exponent); - tokens.set(i, new NumberToken(output)); - } - } // brackets - - for (int i = 0; i < tokens.size(); i++) { - Token token = tokens.get(i); - if (token instanceof OperatorToken operatorToken && operatorToken.val == 94) { - i = operatorToken.eval(tokens, i, params); - } - } // exponent - - for (int i = 0; i < tokens.size(); i++) { - Token token = tokens.get(i); - if (token instanceof OperatorToken operatorToken && (operatorToken.val == 47 || operatorToken.val == 42)) { - i = operatorToken.eval(tokens, i, params); - continue; - } - if (token instanceof NumberToken first && i + 1 < tokens.size() && tokens.get(i + 1) instanceof NumberToken second) { // (4)5 multiplication style, should be done next to the normal multiplication - tokens.remove(i); - tokens.set(i--, new NumberToken(first.val * second.val)); - } - } // division & multiplication - - for (int i = 0; i < tokens.size(); i++) { - Token token = tokens.get(i); - if (token instanceof OperatorToken operatorToken && operatorToken.val == 37) { - i = operatorToken.eval(tokens, i, params); - } - } // modulo - - for (int i = 0; i < tokens.size(); i++) { - Token token = tokens.get(i); - if (token instanceof OperatorToken operatorToken && (operatorToken.val == 43 || operatorToken.val == 45)) { - i = operatorToken.eval(tokens, i, params); - } - } // addition & subtraction - } - - /** - * I don't care that intellij is screaming at me, I want to always know in my code, if a value is something or - * nothing, no, nullable doesn't work, I want to have to explicitly specify what to do if there's no value, - * because everyone has a habit of ignoring those. - *

- * thank you for listening to my ted-talk - *

- * this post was made by a rust user - */ - @Contract(value = "_,!null->!null", pure = true) - @SuppressWarnings("OptionalUsedAsFieldOrParameterType") - private static Optional makeToken(Optional> clazz, String param) { - System.out.println(param); - return clazz.map(value -> switch (value.getName()) { - case "ca.rttv.chatcalc.tokens.NumberToken" -> new NumberToken(Double.parseDouble(param)); - case "ca.rttv.chatcalc.tokens.OperatorToken" -> new OperatorToken(param.charAt(0)); - case "ca.rttv.chatcalc.tokens.BracketToken" -> new BracketToken(param.charAt(0)); - case "ca.rttv.chatcalc.tokens.FunctionToken" -> new FunctionToken(param); - case "ca.rttv.chatcalc.tokens.AbsToken" -> new AbsToken(); - case "ca.rttv.chatcalc.tokens.SeparatorToken" -> new SeparatorToken(); - default -> null; - }); - } - - @Contract(value = "_->_", pure = true) - private static boolean isAbs(char c) { - return c == 124; - } - - @Contract(value = "_->_", pure = true) - private static boolean isBracket(char c) { - return c == 40 || c == 41; - } - - @Contract(value = "_->_", pure = true) - private static boolean isOperator(char c) { - return c == 42 || c == 43 || c == 45 || c == 47 || c == 94 || c == 37; - } - - @Contract(value = "_->_", pure = true) - private static boolean isNumber(char c) { - return c >= 48 && c <= 57 || c == 46; - } - @Contract(value = "_->_", pure = true) - private static boolean isPos(char c) { - return c >= 120 && c <= 122; - } +public interface MathEngine { + double eval(String input, Optional paramaters); } diff --git a/src/main/java/ca/rttv/chatcalc/NibbleMathEngine.java b/src/main/java/ca/rttv/chatcalc/NibbleMathEngine.java new file mode 100644 index 0000000..c3e1a76 --- /dev/null +++ b/src/main/java/ca/rttv/chatcalc/NibbleMathEngine.java @@ -0,0 +1,178 @@ +package ca.rttv.chatcalc; + +import ca.rttv.chatcalc.tokens.FunctionToken; +import net.minecraft.client.MinecraftClient; +import net.minecraft.util.math.Vec3d; +import org.jetbrains.annotations.Nullable; + +import java.nio.charset.StandardCharsets; +import java.util.Optional; + +public class NibbleMathEngine implements MathEngine { + byte[] bytes; + int idx; + @Nullable FunctionParameter[] params; + double x, y, z; + + @Override + public double eval(String input, Optional paramaters) { + bytes = input.concat("\0").getBytes(StandardCharsets.US_ASCII); // we shouldn't encounter unicode in our math + idx = 0; + params = paramaters.orElse(null); + //noinspection DataFlowIssue -- player != null when the chat != null + Vec3d pos = MinecraftClient.getInstance().player.getPos(); + x = pos.x; + y = pos.y; + z = pos.z; + return expression(false); + } + + private boolean bite(char bite) { // pun intended + if (bytes[idx] == bite) { + idx++; + return true; + } else { + return false; + } + } + + private double expression(boolean abs) { + double x = modulo(abs); + while (true) { // prolly unnecessarily + if (bite('+')) x += modulo(abs); + else if (bite('-')) x -= modulo(abs); + else return x; + } + } + + private double modulo(boolean abs) { + double x = term(abs); + while (true) { // prolly unnecessarily + if (bite('%')) x %= term(abs); + else return x; + } + } + + private double term(boolean abs) { + double x = part(abs); + while (true) { + if (bite('*')) x *= part(abs); + else if (bite('/')) x /= part(abs); + else if (bytes[idx] == '(') x *= expression(abs); + else if (bytes[idx] <= '9' && bytes[idx] >= '0') x *= expression(abs); + else if (!abs && bytes[idx] == '|') x *= Math.abs(expression(false)); // simplify to false + else return x; + } + } + + private double part(boolean abs) { + long sign = bite('-') ? 0x8000_0000_0000_0000L : 0L; + if (sign == 0) bite('+'); + + double x = 1.0; + + a: + { + if (bite('(')) { + x = expression(false); + if (!bite(')')) throw new IllegalArgumentException("Expected closing parenthesis"); + } else if (!abs && bite('|')) { + x = Math.abs(expression(true)); + if (!bite('|')) throw new IllegalArgumentException("Expected closing absolute value character"); + } else if ((bytes[idx] <= '9' & bytes[idx] >= '0') | bytes[idx] == '.') { + int start = idx; + while ((bytes[idx] <= '9' & bytes[idx] >= '0') | bytes[idx] == '.') idx++; + x = Double.parseDouble(new String(bytes, start, idx - start, StandardCharsets.US_ASCII)); + } else if (bytes[idx] <= 'z' & bytes[idx] >= 'a') { + int start = idx; + while (bytes[idx] <= 'z' & bytes[idx] >= 'a' | bytes[idx] == '_') idx++; + String func = new String(bytes, start, idx - start, StandardCharsets.US_ASCII); + double u = 1.0; + b: while (true) { + if (func.startsWith("pi")) { + x *= u; + u = Math.PI; + func = func.substring(2); + continue; + } + if (func.startsWith("e")) { + x *= u; + u = Math.E; + func = func.substring(1); + continue; + } + if (func.startsWith("x")) { + x *= u; + u = this.x; + func = func.substring(1); + continue; + } + if (func.startsWith("y")) { + x *= u; + u = this.y; + func = func.substring(1); + continue; + } + if (func.startsWith("z")) { + x *= u; + u = this.z; + func = func.substring(1); + continue; + } + if (params != null) { + for (FunctionParameter param : params) { + if (param != null && func.startsWith(param.name())) { + x *= u; + u = param.value(); + func = func.substring(param.name().length()); + continue b; + } + } + } + if (func.length() == 0) { + if (bite('^')) u = Math.pow(u, part(false)); + x *= u; + break a; + } + break; + } + if (func.equals("log_")) { + double base = part(false); + if (!bite('(')) throw new IllegalArgumentException("Expected parenthesis for function"); + double value = expression(false); + if (!bite(')')) throw new IllegalArgumentException("Expected closing parenthesis for function"); + x *= FunctionToken.log(base, value); + break a; + } + int param_count = 1; + double exponent = 1.0; + if (bite('^')) exponent = part(false); + if (!bite('(')) throw new IllegalArgumentException("Expected parenthesis for function"); + int depth = 0; + int before = idx; + while (bytes[idx] != '\0') { + if (bytes[idx] == ChatCalc.SEPARATOR_CHAR & depth == 0) param_count++; + else if (bytes[idx] == ')') { + if (depth-- == 0) break; + } else if (bytes[idx] == '(') depth++; + idx++; + } + idx = before; + double[] values = new double[param_count]; + int value_count = 0; + while (true) { + if (bite('\0')) throw new IllegalArgumentException("Expected closing parenthesis for function"); + values[value_count++] = expression(false); + if (bite(')')) break; + if (!bite(';')) throw new AssertionError("Expected that a semicolon exists between the parameters"); + } + x *= Math.pow(new FunctionToken(func).apply(values), exponent); + } else + throw new IllegalArgumentException("Expected a valid character for equation, not " + (char) bytes[idx]); + } + + if (bite('^')) x = Math.pow(x, part(false)); + + return Double.longBitsToDouble(Double.doubleToLongBits(x) | sign); + } +} diff --git a/src/main/java/ca/rttv/chatcalc/TokenizedMathEngine.java b/src/main/java/ca/rttv/chatcalc/TokenizedMathEngine.java new file mode 100644 index 0000000..882999d --- /dev/null +++ b/src/main/java/ca/rttv/chatcalc/TokenizedMathEngine.java @@ -0,0 +1,395 @@ +package ca.rttv.chatcalc; + +import ca.rttv.chatcalc.tokens.*; +import com.mojang.logging.LogUtils; +import net.minecraft.client.MinecraftClient; +import net.minecraft.text.LiteralTextContent; +import net.minecraft.text.MutableText; +import net.minecraft.text.Text; +import org.jetbrains.annotations.Contract; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +public class TokenizedMathEngine implements MathEngine { + public static final Logger LOGGER = LogUtils.getLogger(); + + @Override + @Contract(value = "!null,!null->_", pure = true) + public double eval(String input, Optional paramaters) { + MinecraftClient client = MinecraftClient.getInstance(); + List tokens = tokenize(input); + { + if (Config.debugTokens() && client.player != null) { + LOGGER.info(tokens.stream().map(Object::toString).collect(Collectors.joining())); + + MutableText text = MutableText.of(new LiteralTextContent("§r")); + text.getSiblings().addAll(tokens.stream().map(Token::toText).toList()); + text.getSiblings().add(Text.literal("§r")); + client.player.sendMessage(text); + } + } // print to console for debug + + simplify(tokens, false, paramaters); + if (tokens.size() > 0 && tokens.get(0) instanceof NumberToken numberToken) { + return numberToken.val; + } + throw new IllegalArgumentException(); + } + + @Contract(value = "!null->!null", pure = true) + public List tokenize(String input) { + // right bracketing isn't required I just need to do left-bracketing + input = input.toLowerCase() + .replace("**", "^") + .replaceAll(",", ""); + + int depth = 0; + for (int i = 0; i < input.length(); i++) { + char c = input.charAt(i); + if (c == ')') { + depth--; + } else if (c == '(') { + depth++; + } + } + if (depth < 0) { + input = "(".repeat(-depth).concat(input); + } else if (depth > 0) { + input = input.concat(")".repeat(depth)); + } + + List tokens = new ArrayList<>(input.length() >> 1); // just a guess + { + Optional> currentType = Optional.empty(); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < input.length(); i++) { + char c = input.charAt(i); + + if (c == ';') { // separator (for function calls) + if (sb.length() > 0) { + makeToken(currentType, sb.toString()).ifPresent(tokens::add); + } + sb = new StringBuilder().append(c); + currentType = Optional.of(SeparatorToken.class); + makeToken(currentType, sb.toString()).ifPresent(tokens::add); + sb = new StringBuilder(); + continue; + } + + if (isNumber(c) && (currentType.isEmpty() || currentType.get() != FunctionToken.class)) { // this exclusion is so log_ custom bases work + sb.append(c); + currentType = Optional.of(NumberToken.class); + continue; + } + + if (isOperator(c)) { + if (sb.length() > 0) { // this segment on every one of these is to finish a number token + makeToken(currentType, sb.toString()).ifPresent(tokens::add); + } + sb = new StringBuilder().append(c); // this resets it after the number token has been done + currentType = Optional.of(OperatorToken.class); + makeToken(currentType, sb.toString()).ifPresent(tokens::add); + sb = new StringBuilder(); + continue; + } + + if (isBracket(c)) { + if (sb.length() > 0) { // this segment on every one of these is to finish a number token + makeToken(currentType, sb.toString()).ifPresent(tokens::add); + } + sb = new StringBuilder().append(c); // this resets it after the number token has been done + currentType = Optional.of(BracketToken.class); + makeToken(currentType, sb.toString()).ifPresent(tokens::add); + sb = new StringBuilder(); + continue; + } + + if (isAbs(c)) { + if (sb.length() > 0) { // this segment on every one of these is to finish a number token + makeToken(currentType, sb.toString()).ifPresent(tokens::add); + } + sb = new StringBuilder().append(c); // this resets it after the number token has been done + currentType = Optional.of(AbsToken.class); + makeToken(currentType, sb.toString()).ifPresent(tokens::add); + sb = new StringBuilder(); + continue; + } + + if (sb.length() > 0 && !currentType.equals(Optional.of(FunctionToken.class))) { + makeToken(currentType, sb.toString()).ifPresent(tokens::add); + sb = new StringBuilder(); + } + sb.append(c); // if something is not a member of another type, it just assumes it's an unknown function + currentType = Optional.of(FunctionToken.class); + switch (sb.toString()) { + case "e" -> { + tokens.add(new NumberToken(Math.E)); + currentType = Optional.empty(); + sb = new StringBuilder(); + } + case "pi" -> { + tokens.add(new NumberToken(Math.PI)); + currentType = Optional.empty(); + sb = new StringBuilder(); + } + case "tau" -> { + tokens.add(new NumberToken(6.28318530717958647692528676655900577)); // overkill but idc + currentType = Optional.empty(); + sb = new StringBuilder(); + } + } + } + + if (sb.length() > 0) { // this segment on every one of these is to finish a number token + makeToken(currentType, sb.toString()).ifPresent(tokens::add); + } + } // parse to tokens + { + boolean isOpeningAbsToken = true; // inverts between for abs tokens, represents if its opening or closing + for (int i = 0; i < tokens.size() - 1; i++) { + Token token = tokens.get(i); + Token number = tokens.get(i + 1); + if (token instanceof OperatorToken operatorToken && (operatorToken.val == 43 || operatorToken.val == 45) && number instanceof NumberToken numberToken) { + double signed = operatorToken.val == 45 ? -numberToken.val : numberToken.val; + if (i == 0) { // if im an operator, and the next is a number, of course we should modify that + tokens.set(i + 1, new NumberToken(signed)); + tokens.remove(i); // yes, we should skip the next one + continue; + } + + Token previous = tokens.get(i - 1); + if (previous instanceof OperatorToken || previous instanceof SeparatorToken) { + tokens.set(i + 1, new NumberToken(signed)); + //noinspection SuspiciousListRemoveInLoop -- checked, yes, we should skip the next one + tokens.remove(i); + } else if (previous instanceof BracketToken bracketToken && bracketToken.isOpen) { + tokens.set(i + 1, new NumberToken(signed)); + //noinspection SuspiciousListRemoveInLoop -- checked, yes, we should skip the next one + tokens.remove(i); + } else if (previous instanceof AbsToken) { + if (isOpeningAbsToken) { + tokens.set(i + 1, new NumberToken(signed)); + //noinspection SuspiciousListRemoveInLoop -- checked, yes, we should skip the next one + tokens.remove(i); + } + isOpeningAbsToken = !isOpeningAbsToken; + } + } + } + } // convert - + signs on numbers when appropriate + return tokens; + } + + @Contract(value = "_,_,_->_", mutates = "param1") + public static void simplify(List tokens, boolean abs, Optional params) { + final MinecraftClient client = MinecraftClient.getInstance(); + + for (int i = 0; i < tokens.size(); i++) { + Token token = tokens.get(i); + if (token instanceof FunctionToken functionToken) { + if (params.isPresent()) { // since 'a' will be considered a function, I have to detect if it is, then convert it into its constant + boolean any = true; + a: + while (any) { + any = false; + for (FunctionParameter param : params.get()) { + if (((FunctionToken) tokens.get(i)).func.startsWith(param.name())) { + any = true; + String name = ((FunctionToken) tokens.get(i)).func.substring(param.name().length()); + tokens.set(i, new NumberToken(param.value())); // yes this does reverse it, but at the end of the day its multiplication, so it doesn't matter. + if (name.length() > 0) { + tokens.add(i, new FunctionToken(name)); + } else { + break a; + } + break; + } + } + } + } + if (functionToken.func.length() == 1 && isPos(functionToken.func.charAt(0)) && client.player != null) { + tokens.set(i, new NumberToken(switch (functionToken.func.charAt(0)) { + case 'x' -> client.player.getX(); + case 'y' -> client.player.getY(); + case 'z' -> client.player.getZ(); + default -> throw new IllegalArgumentException(); + })); + } + } + } + + for (int i = 0; i < tokens.size(); i++) { + Token token = tokens.get(i); + if (token instanceof BracketToken bracketToken) { + //noinspection SuspiciousListRemoveInLoop -- checked + tokens.remove(i); + if (bracketToken.isOpen) { + simplify(tokens.subList(i, tokens.size()), false, params); + } else { + simplify(tokens.subList(0, i), false, params); + return; + } + } else if (token instanceof AbsToken absToken) { // absolute value + if (abs) { + Token first = tokens.get(0); + if (first instanceof NumberToken numberToken) { + tokens.set(i, new NumberToken(absToken.apply(numberToken.val))); + return; + } + return; + } else { + simplify(tokens.subList(i, tokens.size()), true, params); + } + } else if (token instanceof FunctionToken functionToken) { // function + //noinspection SuspiciousListRemoveInLoop -- checked + tokens.remove(i); // thyself + double exponent = 1.0; + if (tokens.get(i) instanceof OperatorToken operatorToken && operatorToken.val == 94) { + //noinspection SuspiciousListRemoveInLoop -- checked + tokens.remove(i); // exponent operator + if (tokens.get(i) instanceof NumberToken numberToken) { + exponent = numberToken.val; + } else if (tokens.get(i) instanceof BracketToken bracketToken) { + if (!bracketToken.isOpen) { + throw new IllegalArgumentException(); + } else { + tokens.remove(i); // opening bracket for exponent + simplify(tokens.subList(i, tokens.size()), false, params); + if (!(tokens.get(i) instanceof NumberToken numberToken)) { + throw new IllegalArgumentException(); + } + exponent = numberToken.val; + } + } else { + throw new IllegalArgumentException(); + } + } + //noinspection SuspiciousListRemoveInLoop -- checked + tokens.remove(i); // number + simplify(tokens.subList(i, tokens.size()), false, params); + int end = tokens.size(); // tricky code, automatically the end is tokens.size() if there is no separator, somehow, extra caution I guess. + boolean lastWasSeparator = true; + for (int j = i; j < end; j++) { + if (tokens.get(j) instanceof SeparatorToken) { + if (lastWasSeparator) { + throw new IllegalArgumentException(); + } else { + lastWasSeparator = true; + } + } else if (tokens.get(j) instanceof NumberToken) { + if (lastWasSeparator) { + lastWasSeparator = false; + } else { + end = j; + break; + } + } else { + if (lastWasSeparator) { + throw new IllegalArgumentException(); + } else { + end = j; + break; + } + } + } + if (lastWasSeparator) { + throw new IllegalArgumentException(); + } + double[] values = new double[(end - i + 1) / 2]; + for (int k = 0, j = i; j < end; ) { + values[k] = ((NumberToken) tokens.get(j)).val; // safe cast because of checks above + k++; + j += 2; + } + double output = Math.pow(functionToken.apply(values), exponent); + tokens.set(i, new NumberToken(output)); + } + } // brackets + + for (int i = 0; i < tokens.size(); i++) { + Token token = tokens.get(i); + if (token instanceof OperatorToken operatorToken && operatorToken.val == 94) { + i = operatorToken.eval(tokens, i, params); + } + } // exponent + + for (int i = 0; i < tokens.size(); i++) { + Token token = tokens.get(i); + if (token instanceof OperatorToken operatorToken && (operatorToken.val == 47 || operatorToken.val == 42)) { + i = operatorToken.eval(tokens, i, params); + continue; + } + if (token instanceof NumberToken first && i + 1 < tokens.size() && tokens.get(i + 1) instanceof NumberToken second) { // (4)5 multiplication style, should be done next to the normal multiplication + tokens.remove(i); + tokens.set(i--, new NumberToken(first.val * second.val)); + } + } // division & multiplication + + for (int i = 0; i < tokens.size(); i++) { + Token token = tokens.get(i); + if (token instanceof OperatorToken operatorToken && operatorToken.val == 37) { + i = operatorToken.eval(tokens, i, params); + } + } // modulo + + for (int i = 0; i < tokens.size(); i++) { + Token token = tokens.get(i); + if (token instanceof OperatorToken operatorToken && (operatorToken.val == 43 || operatorToken.val == 45)) { + i = operatorToken.eval(tokens, i, params); + } + } // addition & subtraction + } + + /** + * I don't care that intellij is screaming at me, I want to always know in my code, if a value is something or + * nothing, no, nullable doesn't work, I want to have to explicitly specify what to do if there's no value, + * because everyone has a habit of ignoring those. + *

+ * thank you for listening to my ted-talk + *

+ * this post was made by a rust user + */ + @Contract(value = "_,!null->!null", pure = true) + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + private static Optional makeToken(Optional> clazz, String param) { + System.out.println(param); + return clazz.map(value -> switch (value.getName()) { + case "ca.rttv.chatcalc.tokens.NumberToken" -> new NumberToken(Double.parseDouble(param)); + case "ca.rttv.chatcalc.tokens.OperatorToken" -> new OperatorToken(param.charAt(0)); + case "ca.rttv.chatcalc.tokens.BracketToken" -> new BracketToken(param.charAt(0)); + case "ca.rttv.chatcalc.tokens.FunctionToken" -> new FunctionToken(param); + case "ca.rttv.chatcalc.tokens.AbsToken" -> new AbsToken(); + case "ca.rttv.chatcalc.tokens.SeparatorToken" -> new SeparatorToken(); + default -> null; + }); + } + + @Contract(value = "_->_", pure = true) + private static boolean isAbs(char c) { + return c == 124; + } + + @Contract(value = "_->_", pure = true) + private static boolean isBracket(char c) { + return c == 40 || c == 41; + } + + @Contract(value = "_->_", pure = true) + private static boolean isOperator(char c) { + return c == 42 || c == 43 || c == 45 || c == 47 || c == 94 || c == 37; + } + + @Contract(value = "_->_", pure = true) + private static boolean isNumber(char c) { + return c >= 48 && c <= 57 || c == 46; + } + + @Contract(value = "_->_", pure = true) + private static boolean isPos(char c) { + return c >= 120 && c <= 122; + } +} diff --git a/src/main/java/ca/rttv/chatcalc/tokens/FunctionToken.java b/src/main/java/ca/rttv/chatcalc/tokens/FunctionToken.java index d15aab2..4d2e375 100644 --- a/src/main/java/ca/rttv/chatcalc/tokens/FunctionToken.java +++ b/src/main/java/ca/rttv/chatcalc/tokens/FunctionToken.java @@ -1,13 +1,14 @@ package ca.rttv.chatcalc.tokens; import ca.rttv.chatcalc.Config; -import ca.rttv.chatcalc.MathEngine; +import ca.rttv.chatcalc.TokenizedMathEngine; import net.minecraft.text.LiteralTextContent; import net.minecraft.text.MutableText; import net.minecraft.text.Text; import java.util.HashMap; import java.util.Map; +import java.util.Optional; import java.util.function.DoubleUnaryOperator; public final class FunctionToken implements Token { @@ -53,7 +54,7 @@ public double apply(double... values) { if (values.length != 1) { throw new IllegalArgumentException(); } - return log(MathEngine.eval(func.substring(4)), values[0]); + return log(Config.makeEngine().eval(func.substring(4), Optional.empty()), values[0]); } if (functions.containsKey(func)) { if (values.length != 1) { diff --git a/src/main/java/ca/rttv/chatcalc/tokens/OperatorToken.java b/src/main/java/ca/rttv/chatcalc/tokens/OperatorToken.java index 77e9c13..6b7ea6b 100644 --- a/src/main/java/ca/rttv/chatcalc/tokens/OperatorToken.java +++ b/src/main/java/ca/rttv/chatcalc/tokens/OperatorToken.java @@ -1,7 +1,7 @@ package ca.rttv.chatcalc.tokens; import ca.rttv.chatcalc.FunctionParameter; -import ca.rttv.chatcalc.MathEngine; +import ca.rttv.chatcalc.TokenizedMathEngine; import net.minecraft.text.LiteralTextContent; import net.minecraft.text.MutableText; import net.minecraft.text.Text; @@ -48,7 +48,7 @@ public int eval(List tokens, int i, Optional params) if (tokens.get(i - 1) instanceof BracketToken bracketToken) { tokens.remove(--i); int start = bracketToken.getStart(tokens, i); - MathEngine.simplify(tokens.subList(start, i), false, params); + TokenizedMathEngine.simplify(tokens.subList(start, i), false, params); // safe because the only engine to use tokens is the token engin } if (tokens.get(i - 1) instanceof NumberToken numberToken) { @@ -60,7 +60,14 @@ public int eval(List tokens, int i, Optional params) } if (tokens.get(i + 1) instanceof BracketToken) { tokens.remove(--i); - MathEngine.simplify(tokens.subList(i, tokens.size()), false, params); + TokenizedMathEngine.simplify(tokens.subList(i, tokens.size()), false, params); // safe because the only engine to use tokens is the token engin + } + if (tokens.get(i + 1) instanceof OperatorToken op && (op.val == '+' || op.val == '-')) { + tokens.remove(i + 1); + TokenizedMathEngine.simplify(tokens.subList(i + 1, tokens.size()), false, params); // safe because the only engine to use tokens is the token engin + if (tokens.get(i + 1) instanceof NumberToken right && op.val == '-') { + tokens.set(i + 1, new NumberToken(-right.val)); + } } if (!(tokens.get(i + 1) instanceof NumberToken right)) { throw new IllegalArgumentException(); diff --git a/src/main/resources/assets/chatcalc/lang/en_us.json b/src/main/resources/assets/chatcalc/lang/en_us.json index 28afc39..34e98e5 100644 --- a/src/main/resources/assets/chatcalc/lang/en_us.json +++ b/src/main/resources/assets/chatcalc/lang/en_us.json @@ -5,5 +5,6 @@ "chatcalc.log_exceptions.description": "Logs exceptions to your minecraft logs. The reason this is a toggle is because when pressing tab for non chatcalc purposes, it throws an error", "chatcalc.copy_type.description": "An option either copy to your clipboard (value: clipboard) or add it to your chat history (value: chat_history) (the thing when pressing the up arrow and down arrow), or to not save to anywhere (none)", "chatcalc.calculate_last.description": "When tabbing in usernames or subcommands, chatcalc could take precedence over those actions. Which is particularly annoying for usernames that start with x, y, z, etc. (default: true)", + "chatcalc.engine.description": "Different Math Engines, currently nibble is the default however is also extremely new, you can swap back to the older 'token' engine (which is much slower). (A math engine is the code which calculates your math) (default: nibble)", "chatcalc.config.description": "Configuration options are:\n - Decimal Format (decimal_format?)\n - Radians (radians?)\n - Debug Tokens (debug_tokens?)\n - Log Exceptions (log_exceptions?)\n - Copy Type (copy_type?)\n - Calculate Last (calculate_last?)" } \ No newline at end of file