HTTPS
https://git.nageyqma.icu/nageyqma/blade-addons-next.git
SSH
git@git.nageyqma.icu:nageyqma/blade-addons-next.git
HTTPS 链接可直接 git clone;推送代码请使用 SSH。
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308
package blade.addon.utils;
import java.util.ArrayList;
import java.util.Stack;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* This is purely made for the searchbar.
* The parser is based on Shunting Yard and Reverse Polish Notation.
* Yes this is commented more than other files, but this was more fun,
* so I actually felt like documenting it.
*/
public class MathParser {
private static final Pattern NUMBER_PATTERN = Pattern.compile("^(?<num>-?\\d+)(?<decimal>\\.\\d+)?(?<unit>[bBmMkK])?");
private static final Pattern TOKEN_PATTERN = Pattern.compile("[()+\\-*/^]");
/**
* Parses an expression and returns the value.
* @param string The expression to parse
* @return The value if it's a valid expression, otherwise NaN
*/
public static double parseExpression(String string) {
if (string == null) return Double.NaN;
ArrayList<String> tokens;
try {
tokens = toTokens(string);
} catch (IllegalArgumentException e) {
return Double.NaN;
}
if (tokens.isEmpty()) return Double.NaN;
if (tokens.size() == 1) {
try {
//incase it's just a number
return tokenToNum(tokens.getFirst());
} catch (NumberFormatException ignored) {
return Double.NaN;
}
}
ArrayList<String> rpnTokens = toRPN(tokens);
if (rpnTokens == null) {
return Double.NaN;
}
try {
return parseRPN(rpnTokens);
} catch (IllegalArgumentException e) {
return Double.NaN;
}
}
/**
* Converts a String to a number.
* Supports orders of magnitudes like b, m and k.
* It also supports negative and decimal numbers.
* If it can't convert the number,
* {@code NumberFormatException} will be thrown.
*
* @param token The token to convert to a number
* @return The converted number
*/
private static double tokenToNum(String token) {
Matcher matcher = NUMBER_PATTERN.matcher(token);
if (!matcher.find()) {
throw new NumberFormatException();
}
String num = matcher.group("num");
String decimal = matcher.group("decimal");
String unit = matcher.group("unit");
double value;
if (decimal != null) {
value = Double.parseDouble(num + decimal);
} else {
value = Double.parseDouble(num);
}
if (unit != null) {
switch (unit) {
case "b", "B":
value *= 1_000_000_000;
break;
case "m", "M":
value *= 1000_000;
break;
case "k", "K":
value *= 1000;
break;
}
}
return value;
}
/**
* Parses an RPN token, Will throw an {@code IllegalArgumentException} when handling an invalid token.
* @param stack the stack to keep track of previously handled tokens
* @param token The token to parse
*/
private static void handleTokenRPN(Stack<Double> stack, String token) {
try {
double value = tokenToNum(token);
stack.push(value);
return;
} catch (NumberFormatException ignored) {
}
if (stack.size() < 2) {
if (stack.size() == 1) {
if (token.equals("-")) {
//edge case to make for example -(2+3) negative
stack.push(stack.pop() * -1);
return;
}
}
throw new IllegalArgumentException("Invalid token");
}
double right = stack.pop();
double left = stack.pop();
switch (token) {
case "+":
stack.push(left + right);
break;
case "-":
stack.push(left - right);
break;
case "*":
stack.push(left * right);
break;
case "/":
stack.push(left / right);
break;
case "^":
stack.push(Math.pow(left, right));
break;
default:
throw new IllegalArgumentException("Invalid operator");
}
}
/**
* Parses an expression of tokens in RPN.
* Reference: <a href="https://inspirnathan.com/posts/150-reverse-polish-notation-evaluator-in-javascript/">reverse-polish-notation-evaluator-in-javascript</a>
* @param tokens The tokens to parse
* @return The parsed value, {@code Double.NaN} if its invalid
*/
private static double parseRPN(ArrayList<String> tokens) {
Stack<Double> stack = new Stack<>();
for (String token : tokens) {
handleTokenRPN(stack, token);
}
if (stack.isEmpty()) return Double.NaN;
return stack.pop();
}
private static int getPrecedence(String token) {
return switch (token) {
case "^" -> 4;
case "*", "/" -> 3;
case "+", "-" -> 2;
default -> -1;
};
}
private static String getAssociative(String token) {
return switch (token) {
case "^" -> "right";
case "*", "/", "+", "-" -> "left";
default -> "";
};
}
private static boolean isANumber(String token) {
Matcher matcher = NUMBER_PATTERN.matcher(token);
return matcher.find();
}
private static boolean addToken(ArrayList<String> output, Stack<String> stack, String token) {
if (isANumber(token)) {
output.add(token);
return true;
} else if (getPrecedence(token) != -1) {
if (stack.isEmpty()) {
stack.push(token);
return true;
}
String top = stack.peek();
while (!top.equals("(") && (getPrecedence(top) > getPrecedence(token)
|| (getPrecedence(top) == getPrecedence(token) && getAssociative(token).equals("left")))
) {
output.add(stack.pop());
if (stack.isEmpty()) break;
top = stack.peek();
}
stack.push(token);
return true;
} else if (token.equals("(")) {
stack.push(token);
return true;
} else if (token.equals(")")) {
String top = stack.peek();
while (!top.equals("(")) {
if (stack.isEmpty()) return false;
output.add(stack.pop());
if (stack.isEmpty()) return false;
top = stack.peek();
}
if (stack.isEmpty() || !stack.peek().equals("(")) return false;
stack.pop();
return true;
} else {
return false;
}
}
/**
* Converts a list of tokens to a new in RPN
* Refrence: <a href="https://inspirnathan.com/posts/151-shunting-yard-algorithm-in-javascript/">151-shunting-yard-algorithm-in-javascript</a>
* @param tokens The tokens to convert
* @return The list of tokens in RPN
*/
private static ArrayList<String> toRPN(ArrayList<String> tokens) {
ArrayList<String> output = new ArrayList<>();
Stack<String> stack = new Stack<>();
for (String token : tokens) {
if (!addToken(output, stack, token)) {
return null;
}
}
while (!stack.isEmpty()) {
String token = stack.pop();
if (token.equals("(")) return null;
output.add(token);
}
return output;
}
/**
* Split an expression string to an ArrayList of tokens.
* Throws an {@code IllegalArgumentException} when an invalid token is read.
*
* @param string The string to split into token
* @return The list of tokens
*/
private static ArrayList<String> toTokens(String string) {
ArrayList<String> output = new ArrayList<>();
String copiedString = string.replaceAll(" ", "");
//to handle binary subtraction not getting confused
//with unary subtraction
boolean wasPrevNum = false;
while (!copiedString.isEmpty()) {
Matcher matcher = NUMBER_PATTERN.matcher(copiedString);
if (!wasPrevNum && matcher.find()) {
String token = matcher.group();
int index = copiedString.indexOf(token);
if (index == 0) {
copiedString = copiedString.substring(token.length());
output.add(token);
wasPrevNum = true;
continue;
}
}
matcher = TOKEN_PATTERN.matcher(copiedString);
if (matcher.find()) {
String token = matcher.group();
int index = copiedString.indexOf(token);
if (index == 0) {
copiedString = copiedString.substring(token.length());
if (token.equals("x")) token = "*";
output.add(token);
wasPrevNum = false;
continue;
}
}
throw new IllegalArgumentException("String: " + string + " has invalid token, ended at: " + copiedString);
}
return output;
}
}