修复Calculator.conversion(String expression)方法计算包含科学计数法表达式的值时逻辑有误,结果不符合预期(pr#4172@Github)

This commit is contained in:
Looly 2025-12-01 22:37:30 +08:00
parent 7609948ec7
commit 695bc0ae4d
3 changed files with 110 additions and 19 deletions

View File

@ -176,35 +176,90 @@ public class Calculator {
} }
/** /**
* 将表达式中负数的符号更改 * 将表达式中的一元负号转换为内部标记~便于后续解析
* * 规则说明
* @param expression 例如-2+-1*(-3E-2)-(-1) 被转为 ~2+~1*(~3E~2)-(~1) * - 科学计数法整体识别为数字e/E 后的 + - 属于指数符号不参与一元符号折叠
* @return 转换后的字符串 * - 一元 + / - 仅在表达式开头或运算符左括号之后生效可折叠连续符号 --3+-3 -> ~3
* 示例
* - 输入-2+-1*(-3E-2)-(-1)
* - 输出~2+~1*(~3E~2)-(~1)
*/ */
private static String transform(String expression) { private static String transform(String expression) {
expression = StrUtil.cleanBlank(expression); expression = StrUtil.cleanBlank(expression);
expression = StrUtil.removeSuffix(expression, "="); expression = StrUtil.removeSuffix(expression, "=");
final char[] arr = expression.toCharArray(); final char[] arr = expression.toCharArray();
final StringBuilder out = new StringBuilder(arr.length);
for (int i = 0; i < arr.length; i++) { for (int i = 0; i < arr.length; i++) {
if (arr[i] == '-') { final char c = arr[i];
if (i == 0) {
arr[i] = '~'; // 把x或X当作 *
if (CharUtil.equals(c, 'x', true)) {
out.append('*');
continue;
}
// 若是'+''-'需要判断是指数符号二元运算符还是一元运算符序列
if (c == '+' || c == '-') {
// 如果前一个已写入的字符为'e''E'则视作科学计数法的符号
final int outLen = out.length();
if (outLen > 0) {
final char prevOut = out.charAt(outLen - 1);
if (prevOut == 'e' || prevOut == 'E') {
// 在e/E
// '+' 可以安全丢弃1e+3 == 1e3
// '-' 必须保留但不能被当作二元运算符故用'~'临时替代后续再还原为'-'
if (c == '-') {
out.append('~');
}
continue;
}
}
// 查找前一个非空字符原串中的用于判断是否为一元上下文
int j = i - 1;
while (j >= 0 && Character.isWhitespace(arr[j])) j--;
final boolean unaryContext = (j < 0) || isPrevCharOperatorOrLeftParen(arr[j]);
if (unaryContext) {
// 收集连续的一系列 + -例如 --+ - -> 合并为一个净符号
int k = i;
int minusCount = 0;
while (k < arr.length && (arr[k] == '+' || arr[k] == '-')) {
if (arr[k] == '-') minusCount++;
k++;
}
final boolean netNegative = (minusCount % 2 == 1);
if (netNegative) {
// ~标记一元负号与原实现保持兼容
out.append('~');
}
i = k - 1;
} else { } else {
final char c = arr[i - 1]; //二元运算符直接写入 + -
if (c == '+' || c == '-' || c == '*' || c == '/' || c == '(' || c == 'E' || c == 'e') { out.append(c);
arr[i] = '~';
} }
continue;
} }
} else if(CharUtil.equals(arr[i], 'x', true)){ //其它字符包括数字字母括号eE小数点等直接追加
// issue#3787 x转换为* out.append(c);
arr[i] = '*';
} }
}
if (arr[0] == '~' && (arr.length > 1 && arr[1] == '(')) { // 特殊处理如果开头为 "~("原实现会将其转为 "0~(" 形式改为以0开始的负括号处理
arr[0] = '-'; final String result = out.toString();
return "0" + new String(arr); final char[] resArr = result.toCharArray();
if (resArr.length >= 2 && resArr[0] == '~' && resArr[1] == '(') {
resArr[0] = '-';
return "0" + new String(resArr);
} else { } else {
return new String(arr); return result;
} }
} }
/**
* 判断给定位置前一个非空字符是否为运算符或左括号用于判定是否为一元上下文
*/
private static boolean isPrevCharOperatorOrLeftParen(final char c) {
return c == '+' || c == '-' || c == '*' || c == '/' || c == '(';
}
} }

View File

@ -83,4 +83,34 @@ public class CalculatorTest {
result = calculator1.calculate("0+50/100X(1/0.5)"); result = calculator1.calculate("0+50/100X(1/0.5)");
assertEquals(1D, result); assertEquals(1D, result);
} }
@Test
public void scientificNotationPlusTest() {
// 测试科学记数法中的 + 号是否被正确处理
final double conversion = Calculator.conversion("1e+3");
assertEquals(1000.0, conversion, 0.001);
// 更复杂的科学记数法表达式
final double conversion2 = Calculator.conversion("2.5e+2 + 1.0e-1");
assertEquals(250.1, conversion2, 0.001);
}
@Test
public void unaryOperatorConsistencyTest() {
// 测试连续的一元运算符双重负号--3等同于 -( -3 ) = 3
final double conversion = Calculator.conversion("--3");
assertEquals(3.0, conversion, 0.001);
// 测试连续的一元运算符正号后跟负号等同于 +( -3 ) = -3
final double conversion2 = Calculator.conversion("+-3");
assertEquals(-3.0, conversion2, 0.001);
// 测试表达式开始的一元+运算符
final double conversion3 = Calculator.conversion("+3");
assertEquals(3.0, conversion3, 0.001);
// 测试表达式开始的一元-运算符
final double conversion4 = Calculator.conversion("-3");
assertEquals(-3.0, conversion4, 0.001);
}
} }

View File

@ -16,7 +16,6 @@
package cn.hutool.v7.json; package cn.hutool.v7.json;
import lombok.Data;
import cn.hutool.v7.core.collection.ListUtil; import cn.hutool.v7.core.collection.ListUtil;
import cn.hutool.v7.core.date.DateTime; import cn.hutool.v7.core.date.DateTime;
import cn.hutool.v7.core.date.DateUtil; import cn.hutool.v7.core.date.DateUtil;
@ -24,6 +23,7 @@ import cn.hutool.v7.core.map.MapUtil;
import cn.hutool.v7.json.test.bean.Price; import cn.hutool.v7.json.test.bean.Price;
import cn.hutool.v7.json.test.bean.UserA; import cn.hutool.v7.json.test.bean.UserA;
import cn.hutool.v7.json.test.bean.UserC; import cn.hutool.v7.json.test.bean.UserC;
import lombok.Data;
import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -382,4 +382,10 @@ public class JSONUtilTest {
final String jsonStr = JSONUtil.toJsonStr(userId); final String jsonStr = JSONUtil.toJsonStr(userId);
assertEquals("10101010", jsonStr); assertEquals("10101010", jsonStr);
} }
@Test
void parseEmptyTest(){
final JSON parse = JSONUtil.parse("");
assertNull(parse);
}
} }