package jnpf.elnFormula;
|
|
import ch.obermuhlner.math.big.BigDecimalMath;
|
|
import java.math.BigDecimal;
|
import java.math.MathContext;
|
import java.math.RoundingMode;
|
import java.util.ArrayList;
|
import java.util.Collections;
|
import java.util.HashMap;
|
import java.util.List;
|
import java.util.Map;
|
|
public final class FormulaEvaluator {
|
|
private static final MathContext MATH_CONTEXT = MathContext.DECIMAL128;
|
private static final BigDecimal TWO = new BigDecimal("2");
|
private static final BigDecimal ONE_HUNDRED = new BigDecimal("100");
|
private static final BigDecimal MAX_EXP_INPUT = new BigDecimal("1000");
|
private static final int MAX_EXPONENT = 999;
|
private static final int MAX_SCALE = 12;
|
private static final int MAX_SIGNIFICANT_DIGITS = 34;
|
private static final int MAX_NEWTON_ITERATIONS = 256;
|
private static final Map<String, Unit> UNITS = units();
|
|
private final FormulaFunctionRegistry functions;
|
private final FormulaLimits limits;
|
|
public FormulaEvaluator(FormulaFunctionRegistry functions) {
|
this(functions, FormulaLimits.DEFAULTS);
|
}
|
|
public FormulaEvaluator(FormulaFunctionRegistry functions, FormulaLimits limits) {
|
if (functions == null || limits == null) {
|
throw new IllegalArgumentException("functions and limits must not be null");
|
}
|
this.functions = functions;
|
this.limits = limits;
|
}
|
|
public FormulaValue evaluate(FormulaAst.Node node, Map<String, FormulaValue> parameters) {
|
if (node == null || parameters == null) {
|
throw new IllegalArgumentException("node and parameters must not be null");
|
}
|
for (FormulaValue value : parameters.values()) {
|
if (value != null && value.getType() == FormulaType.DECIMAL_LIST
|
&& value.getDecimalList().size() > limits.getMaxListItems()) {
|
throw error("LIST_ITEM_LIMIT_EXCEEDED", "数值列表项数超过限制", node);
|
}
|
}
|
return evaluate(node, parameters, new EvaluationContext(limits.getMaxEvaluationNodes()));
|
}
|
|
public FormulaValue round(FormulaValue value, int scale, RoundingMode mode) {
|
if (value == null || mode == null) {
|
throw new IllegalArgumentException("value and mode must not be null");
|
}
|
requirePresent(value, null);
|
if (value.getType() != FormulaType.DECIMAL) {
|
throw new FormulaException("TYPE_MISMATCH", "只有 DECIMAL 结果可以修约");
|
}
|
if (mode != RoundingMode.HALF_UP && mode != RoundingMode.HALF_EVEN
|
&& mode != RoundingMode.UP && mode != RoundingMode.DOWN) {
|
throw new FormulaException("UNSUPPORTED_ROUNDING_MODE", "不支持的修约模式");
|
}
|
validateScale(scale, null);
|
return FormulaValue.decimal(value.getDecimal().setScale(scale, mode));
|
}
|
|
private FormulaValue evaluate(FormulaAst.Node node, Map<String, FormulaValue> parameters,
|
EvaluationContext context) {
|
context.visit(node);
|
if (node instanceof FormulaAst.Literal) {
|
return literal((FormulaAst.Literal) node);
|
}
|
if (node instanceof FormulaAst.Parameter) {
|
FormulaAst.Parameter parameter = (FormulaAst.Parameter) node;
|
FormulaValue value = parameters.get(parameter.getCode());
|
if (value == null) {
|
throw error("MISSING_INPUT", "缺少参数值: " + parameter.getCode(), node);
|
}
|
return value;
|
}
|
if (node instanceof FormulaAst.Unary) {
|
return unary((FormulaAst.Unary) node, parameters, context);
|
}
|
if (node instanceof FormulaAst.Binary) {
|
return binary((FormulaAst.Binary) node, parameters, context);
|
}
|
if (node instanceof FormulaAst.FunctionCall) {
|
return function((FormulaAst.FunctionCall) node, parameters, context);
|
}
|
throw error("TYPE_MISMATCH", "不支持的公式节点", node);
|
}
|
|
private FormulaValue unary(FormulaAst.Unary unary, Map<String, FormulaValue> parameters,
|
EvaluationContext context) {
|
FormulaValue operand = evaluate(unary.getOperand(), parameters, context);
|
if ("!".equals(unary.getOperator())) {
|
return FormulaValue.bool(!booleanValue(operand, unary));
|
}
|
BigDecimal value = decimalValue(operand, unary);
|
if ("+".equals(unary.getOperator())) {
|
return FormulaValue.decimal(value.round(MATH_CONTEXT));
|
}
|
if ("-".equals(unary.getOperator())) {
|
return FormulaValue.decimal(value.negate(MATH_CONTEXT));
|
}
|
throw error("TYPE_MISMATCH", "不支持的一元运算符", unary);
|
}
|
|
private FormulaValue binary(FormulaAst.Binary binary, Map<String, FormulaValue> parameters,
|
EvaluationContext context) {
|
String operator = binary.getOperator();
|
FormulaValue left = evaluate(binary.getLeft(), parameters, context);
|
if ("&&".equals(operator)) {
|
boolean leftValue = booleanValue(left, binary);
|
return leftValue
|
? FormulaValue.bool(booleanValue(evaluate(binary.getRight(), parameters, context), binary))
|
: FormulaValue.bool(false);
|
}
|
if ("||".equals(operator)) {
|
boolean leftValue = booleanValue(left, binary);
|
return leftValue
|
? FormulaValue.bool(true)
|
: FormulaValue.bool(booleanValue(evaluate(binary.getRight(), parameters, context), binary));
|
}
|
FormulaValue right = evaluate(binary.getRight(), parameters, context);
|
if ("==".equals(operator) || "!=".equals(operator)) {
|
boolean equal = equalValues(left, right, binary);
|
return FormulaValue.bool("==".equals(operator) ? equal : !equal);
|
}
|
BigDecimal leftDecimal = decimalValue(left, binary);
|
BigDecimal rightDecimal = decimalValue(right, binary);
|
if ("+".equals(operator)) {
|
return FormulaValue.decimal(leftDecimal.add(rightDecimal, MATH_CONTEXT));
|
}
|
if ("-".equals(operator)) {
|
return FormulaValue.decimal(leftDecimal.subtract(rightDecimal, MATH_CONTEXT));
|
}
|
if ("*".equals(operator)) {
|
return FormulaValue.decimal(leftDecimal.multiply(rightDecimal, MATH_CONTEXT));
|
}
|
if ("/".equals(operator)) {
|
requireNonZero(rightDecimal, binary);
|
return FormulaValue.decimal(leftDecimal.divide(rightDecimal, MATH_CONTEXT));
|
}
|
if ("%".equals(operator)) {
|
requireNonZero(rightDecimal, binary);
|
try {
|
return FormulaValue.decimal(leftDecimal.remainder(rightDecimal)
|
.round(MATH_CONTEXT));
|
} catch (ArithmeticException ex) {
|
throw error("NUMERIC_ERROR", "余数运算失败", binary);
|
}
|
}
|
if ("^".equals(operator)) {
|
return FormulaValue.decimal(power(leftDecimal, rightDecimal, binary));
|
}
|
int comparison = leftDecimal.compareTo(rightDecimal);
|
if (">".equals(operator)) {
|
return FormulaValue.bool(comparison > 0);
|
}
|
if (">=".equals(operator)) {
|
return FormulaValue.bool(comparison >= 0);
|
}
|
if ("<".equals(operator)) {
|
return FormulaValue.bool(comparison < 0);
|
}
|
if ("<=".equals(operator)) {
|
return FormulaValue.bool(comparison <= 0);
|
}
|
throw error("TYPE_MISMATCH", "不支持的二元运算符", binary);
|
}
|
|
private FormulaValue function(FormulaAst.FunctionCall call,
|
Map<String, FormulaValue> parameters,
|
EvaluationContext context) {
|
String name = call.getName();
|
if (!functions.contains(name)) {
|
throw error("UNKNOWN_FUNCTION", "未知函数: " + name, call);
|
}
|
List<FormulaAst.Node> arguments = call.getArguments();
|
if (arguments.size() > limits.getMaxFunctionArguments()) {
|
throw error("FUNCTION_ARGUMENT_LIMIT_EXCEEDED", "函数参数数量超过限制", call);
|
}
|
if ("IF".equals(name)) {
|
count(arguments, 3, 3, call);
|
boolean condition = booleanValue(evaluate(arguments.get(0), parameters, context), call);
|
return evaluate(arguments.get(condition ? 1 : 2), parameters, context);
|
}
|
if ("COALESCE".equals(name)) {
|
count(arguments, 1, limits.getMaxFunctionArguments(), call);
|
FormulaValue result = FormulaValue.nullValue();
|
for (FormulaAst.Node argument : arguments) {
|
result = evaluate(argument, parameters, context);
|
if (!result.isNull()) {
|
return result;
|
}
|
}
|
return result;
|
}
|
if ("IS_BLANK".equals(name)) {
|
count(arguments, 1, 1, call);
|
return FormulaValue.bool(isBlank(evaluate(arguments.get(0), parameters, context)));
|
}
|
|
List<FormulaValue> values = evaluateAll(arguments, parameters, context);
|
if ("ABS".equals(name)) {
|
count(values, 1, 1, call);
|
return FormulaValue.decimal(decimalValue(values.get(0), call).abs(MATH_CONTEXT));
|
}
|
if ("SQRT".equals(name)) {
|
count(values, 1, 1, call);
|
return FormulaValue.decimal(squareRoot(decimalValue(values.get(0), call), call));
|
}
|
if ("POW".equals(name)) {
|
count(values, 2, 2, call);
|
return FormulaValue.decimal(power(decimalValue(values.get(0), call),
|
decimalValue(values.get(1), call), call));
|
}
|
if ("LN".equals(name) || "LOG10".equals(name)) {
|
count(values, 1, 1, call);
|
BigDecimal value = decimalValue(values.get(0), call);
|
requirePositiveLogValue(value, call);
|
try {
|
return FormulaValue.decimal("LN".equals(name)
|
? BigDecimalMath.log(value, MATH_CONTEXT)
|
: BigDecimalMath.log10(value, MATH_CONTEXT));
|
} catch (ArithmeticException ex) {
|
throw error("NUMERIC_ERROR", "对数计算失败", call);
|
}
|
}
|
if ("LOG".equals(name)) {
|
count(values, 2, 2, call);
|
BigDecimal value = decimalValue(values.get(0), call);
|
BigDecimal base = decimalValue(values.get(1), call);
|
requirePositiveLogValue(value, call);
|
if (base.signum() <= 0 || base.compareTo(BigDecimal.ONE) == 0) {
|
throw error("INVALID_LOG_BASE", "对数底数必须大于 0 且不能等于 1", call);
|
}
|
try {
|
return FormulaValue.decimal(BigDecimalMath.log(value, MATH_CONTEXT)
|
.divide(BigDecimalMath.log(base, MATH_CONTEXT), MATH_CONTEXT));
|
} catch (ArithmeticException ex) {
|
throw error("NUMERIC_ERROR", "对数计算失败", call);
|
}
|
}
|
if ("EXP".equals(name)) {
|
count(values, 1, 1, call);
|
BigDecimal value = decimalValue(values.get(0), call);
|
if (value.abs().compareTo(MAX_EXP_INPUT) > 0) {
|
throw error("EXPONENT_OUT_OF_RANGE", "指数参数必须在 -1000 到 1000 之间", call);
|
}
|
try {
|
return FormulaValue.decimal(BigDecimalMath.exp(value, MATH_CONTEXT));
|
} catch (ArithmeticException ex) {
|
throw error("NUMERIC_ERROR", "指数计算失败", call);
|
}
|
}
|
if ("NTH_ROOT".equals(name)) {
|
count(values, 2, 2, call);
|
BigDecimal value = decimalValue(values.get(0), call);
|
int degree = rootDegree(decimalValue(values.get(1), call), call);
|
if (value.signum() < 0 && degree % 2 == 0) {
|
throw error("NEGATIVE_EVEN_ROOT", "负数不能执行偶次方根", call);
|
}
|
try {
|
BigDecimal root = BigDecimalMath.root(value.abs(), BigDecimal.valueOf(degree),
|
MATH_CONTEXT);
|
return FormulaValue.decimal(value.signum() < 0 ? root.negate() : root);
|
} catch (ArithmeticException ex) {
|
throw error("NUMERIC_ERROR", "方根计算失败", call);
|
}
|
}
|
if ("MIN".equals(name) || "MAX".equals(name)) {
|
count(values, 1, limits.getMaxFunctionArguments(), call);
|
BigDecimal result = decimalValue(values.get(0), call);
|
for (int i = 1; i < values.size(); i++) {
|
BigDecimal candidate = decimalValue(values.get(i), call);
|
if (("MIN".equals(name) && candidate.compareTo(result) < 0)
|
|| ("MAX".equals(name) && candidate.compareTo(result) > 0)) {
|
result = candidate;
|
}
|
}
|
return FormulaValue.decimal(result.round(MATH_CONTEXT));
|
}
|
if ("SUM".equals(name) || "MEAN".equals(name) || "STDDEV_S".equals(name)
|
|| "STDDEV_P".equals(name) || "VAR_S".equals(name)
|
|| "VAR_P".equals(name) || "RSD".equals(name)
|
|| "COUNT".equals(name) || "MEDIAN".equals(name)
|
|| "GEOMEAN".equals(name)) {
|
count(values, 1, 1, call);
|
List<FormulaValue> items = decimalList(values.get(0), call);
|
return statistic(name, items, call);
|
}
|
if ("WEIGHTED_MEAN".equals(name)) {
|
count(values, 2, 2, call);
|
return FormulaValue.decimal(weightedMean(decimalList(values.get(0), call),
|
decimalList(values.get(1), call), call));
|
}
|
if ("SLOPE".equals(name) || "INTERCEPT".equals(name)
|
|| "CORREL".equals(name)) {
|
count(values, 2, 2, call);
|
RegressionStats stats = regression(decimalList(values.get(0), call),
|
decimalList(values.get(1), call), call);
|
if ("SLOPE".equals(name)) {
|
return FormulaValue.decimal(stats.slope);
|
}
|
if ("INTERCEPT".equals(name)) {
|
return FormulaValue.decimal(stats.intercept);
|
}
|
if (stats.sumYY.signum() == 0) {
|
throw error("ZERO_VARIANCE", "相关系数的 y 值方差不能为 0", call);
|
}
|
BigDecimal varianceProduct = stats.sumXX.multiply(stats.sumYY);
|
if (stats.sumXY.multiply(stats.sumXY).compareTo(varianceProduct) == 0) {
|
return FormulaValue.decimal(stats.sumXY.signum() < 0
|
? BigDecimal.ONE.negate() : BigDecimal.ONE);
|
}
|
BigDecimal denominator = squareRoot(varianceProduct, call);
|
return FormulaValue.decimal(clampCorrelation(
|
stats.sumXY.divide(denominator, MATH_CONTEXT)));
|
}
|
if ("LINEAR_PREDICT".equals(name)) {
|
count(values, 3, 3, call);
|
BigDecimal value = decimalValue(values.get(0), call);
|
RegressionStats stats = regression(decimalList(values.get(1), call),
|
decimalList(values.get(2), call), call);
|
return FormulaValue.decimal(stats.predict(value));
|
}
|
if ("ROUND".equals(name) || "ROUND_UP".equals(name)
|
|| "ROUND_DOWN".equals(name) || "ROUND_HALF_EVEN".equals(name)) {
|
count(values, 2, 2, call);
|
int scale = scale(decimalValue(values.get(1), call), call);
|
RoundingMode mode = "ROUND".equals(name) ? RoundingMode.HALF_UP
|
: ("ROUND_UP".equals(name) ? RoundingMode.UP
|
: ("ROUND_DOWN".equals(name) ? RoundingMode.DOWN : RoundingMode.HALF_EVEN));
|
return round(FormulaValue.decimal(decimalValue(values.get(0), call)), scale, mode);
|
}
|
if ("ROUND_SIG".equals(name) || "ROUND_SIG_HALF_EVEN".equals(name)) {
|
count(values, 2, 2, call);
|
BigDecimal value = decimalValue(values.get(0), call);
|
int digits = significantDigits(decimalValue(values.get(1), call), call);
|
RoundingMode mode = "ROUND_SIG".equals(name)
|
? RoundingMode.HALF_UP : RoundingMode.HALF_EVEN;
|
return FormulaValue.decimal(value.round(new MathContext(digits, mode)));
|
}
|
if ("CONVERT".equals(name)) {
|
count(values, 3, 3, call);
|
return convert(values, call);
|
}
|
if ("LOOKUP_EXACT".equals(name)) {
|
lookupCount(values, 3, call);
|
return lookupExact(values, call);
|
}
|
if ("LOOKUP_LINEAR".equals(name)) {
|
lookupCount(values, 5, call);
|
return lookupLinear(values, call);
|
}
|
throw error("UNKNOWN_FUNCTION", "未知函数: " + name, call);
|
}
|
|
private FormulaValue statistic(String name, List<FormulaValue> items,
|
FormulaAst.Node node) {
|
if ("COUNT".equals(name)) {
|
return FormulaValue.decimal(BigDecimal.valueOf(items.size()));
|
}
|
BigDecimal sum = BigDecimal.ZERO;
|
for (FormulaValue item : items) {
|
sum = sum.add(item.getDecimal());
|
}
|
if ("SUM".equals(name)) {
|
return FormulaValue.decimal(sum.round(MATH_CONTEXT));
|
}
|
if (items.isEmpty()) {
|
throw error("INSUFFICIENT_SAMPLE", "数值列表不能为空", node);
|
}
|
if ("MEDIAN".equals(name)) {
|
List<BigDecimal> sorted = new ArrayList<>(items.size());
|
for (FormulaValue item : items) {
|
sorted.add(item.getDecimal());
|
}
|
Collections.sort(sorted);
|
int middle = sorted.size() / 2;
|
if (sorted.size() % 2 == 1) {
|
return FormulaValue.decimal(sorted.get(middle));
|
}
|
return FormulaValue.decimal(sorted.get(middle - 1).add(sorted.get(middle))
|
.divide(TWO, MATH_CONTEXT));
|
}
|
if ("GEOMEAN".equals(name)) {
|
boolean containsZero = false;
|
for (FormulaValue item : items) {
|
BigDecimal value = item.getDecimal();
|
if (value.signum() < 0) {
|
throw error("GEOMEAN_DOMAIN_ERROR", "几何平均值不接受负数", node);
|
}
|
if (value.signum() == 0) {
|
containsZero = true;
|
}
|
}
|
if (containsZero) {
|
return FormulaValue.decimal(BigDecimal.ZERO);
|
}
|
BigDecimal product = BigDecimal.ONE;
|
for (FormulaValue item : items) {
|
BigDecimal value = item.getDecimal();
|
product = product.multiply(value, MATH_CONTEXT);
|
}
|
try {
|
return FormulaValue.decimal(BigDecimalMath.root(product,
|
BigDecimal.valueOf(items.size()), MATH_CONTEXT));
|
} catch (ArithmeticException ex) {
|
throw error("NUMERIC_ERROR", "几何平均值计算失败", node);
|
}
|
}
|
BigDecimal mean = sum.divide(BigDecimal.valueOf(items.size()), MATH_CONTEXT);
|
if ("MEAN".equals(name)) {
|
return FormulaValue.decimal(mean);
|
}
|
boolean sample = "STDDEV_S".equals(name) || "VAR_S".equals(name)
|
|| "RSD".equals(name);
|
if (sample && items.size() < 2) {
|
throw error("INSUFFICIENT_SAMPLE", "样本数至少为 2", node);
|
}
|
BigDecimal variance = variance(items, sample);
|
if ("VAR_S".equals(name) || "VAR_P".equals(name)) {
|
return FormulaValue.decimal(variance);
|
}
|
BigDecimal standardDeviation = squareRoot(variance, node);
|
if ("STDDEV_S".equals(name) || "STDDEV_P".equals(name)) {
|
return FormulaValue.decimal(standardDeviation);
|
}
|
if (mean.signum() == 0) {
|
throw error("ZERO_MEAN", "RSD 的均值不能为 0", node);
|
}
|
return FormulaValue.decimal(standardDeviation.divide(mean.abs(), MATH_CONTEXT)
|
.multiply(ONE_HUNDRED, MATH_CONTEXT));
|
}
|
|
private static BigDecimal variance(List<FormulaValue> items, boolean sample) {
|
BigDecimal anchor = items.get(0).getDecimal();
|
BigDecimal deltaSum = BigDecimal.ZERO;
|
BigDecimal deltaSquaredSum = BigDecimal.ZERO;
|
for (FormulaValue item : items) {
|
BigDecimal delta = item.getDecimal().subtract(anchor);
|
deltaSum = deltaSum.add(delta);
|
deltaSquaredSum = deltaSquaredSum.add(delta.multiply(delta));
|
}
|
BigDecimal count = BigDecimal.valueOf(items.size());
|
BigDecimal numerator = count.multiply(deltaSquaredSum)
|
.subtract(deltaSum.multiply(deltaSum));
|
BigDecimal divisor = count.multiply(BigDecimal.valueOf(
|
sample ? items.size() - 1L : items.size()));
|
return numerator.divide(divisor, MATH_CONTEXT);
|
}
|
|
private static BigDecimal weightedMean(List<FormulaValue> items,
|
List<FormulaValue> weights,
|
FormulaAst.Node node) {
|
requireSameNonEmptyLength(items, weights, node);
|
BigDecimal weightedSum = BigDecimal.ZERO;
|
BigDecimal weightSum = BigDecimal.ZERO;
|
for (int i = 0; i < items.size(); i++) {
|
BigDecimal weight = weights.get(i).getDecimal();
|
if (weight.signum() < 0) {
|
throw error("INVALID_WEIGHT", "权重不能为负数", node);
|
}
|
weightedSum = weightedSum.add(items.get(i).getDecimal()
|
.multiply(weight));
|
weightSum = weightSum.add(weight);
|
}
|
if (weightSum.signum() <= 0) {
|
throw error("INVALID_WEIGHT", "权重和必须大于 0", node);
|
}
|
return weightedSum.divide(weightSum, MATH_CONTEXT);
|
}
|
|
private static RegressionStats regression(List<FormulaValue> xValues,
|
List<FormulaValue> yValues,
|
FormulaAst.Node node) {
|
requireSameNonEmptyLength(xValues, yValues, node);
|
if (xValues.size() < 2) {
|
throw error("INSUFFICIENT_SAMPLE", "线性回归至少需要 2 组数据", node);
|
}
|
BigDecimal count = BigDecimal.valueOf(xValues.size());
|
BigDecimal sumX = BigDecimal.ZERO;
|
BigDecimal sumY = BigDecimal.ZERO;
|
BigDecimal anchorX = xValues.get(0).getDecimal();
|
BigDecimal anchorY = yValues.get(0).getDecimal();
|
BigDecimal deltaXSum = BigDecimal.ZERO;
|
BigDecimal deltaYSum = BigDecimal.ZERO;
|
BigDecimal deltaXXSum = BigDecimal.ZERO;
|
BigDecimal deltaYYSum = BigDecimal.ZERO;
|
BigDecimal deltaXYSum = BigDecimal.ZERO;
|
for (int i = 0; i < xValues.size(); i++) {
|
BigDecimal x = xValues.get(i).getDecimal();
|
BigDecimal y = yValues.get(i).getDecimal();
|
BigDecimal deltaX = x.subtract(anchorX);
|
BigDecimal deltaY = y.subtract(anchorY);
|
sumX = sumX.add(x);
|
sumY = sumY.add(y);
|
deltaXSum = deltaXSum.add(deltaX);
|
deltaYSum = deltaYSum.add(deltaY);
|
deltaXXSum = deltaXXSum.add(deltaX.multiply(deltaX));
|
deltaYYSum = deltaYYSum.add(deltaY.multiply(deltaY));
|
deltaXYSum = deltaXYSum.add(deltaX.multiply(deltaY));
|
}
|
BigDecimal sumXX = count.multiply(deltaXXSum)
|
.subtract(deltaXSum.multiply(deltaXSum));
|
BigDecimal sumYY = count.multiply(deltaYYSum)
|
.subtract(deltaYSum.multiply(deltaYSum));
|
BigDecimal sumXY = count.multiply(deltaXYSum)
|
.subtract(deltaXSum.multiply(deltaYSum));
|
if (sumXX.signum() == 0) {
|
throw error("ZERO_VARIANCE", "线性回归的 x 值方差不能为 0", node);
|
}
|
BigDecimal slope = sumXY.divide(sumXX, MATH_CONTEXT);
|
BigDecimal commonDenominator = count.multiply(sumXX);
|
BigDecimal interceptNumerator = sumY.multiply(sumXX)
|
.subtract(sumXY.multiply(sumX));
|
BigDecimal intercept = interceptNumerator.divide(commonDenominator, MATH_CONTEXT);
|
return new RegressionStats(sumXX, sumYY, sumXY, slope, intercept,
|
count.multiply(sumXY), interceptNumerator, commonDenominator);
|
}
|
|
private static BigDecimal clampCorrelation(BigDecimal value) {
|
if (value.compareTo(BigDecimal.ONE) > 0) {
|
return BigDecimal.ONE;
|
}
|
if (value.compareTo(BigDecimal.ONE.negate()) < 0) {
|
return BigDecimal.ONE.negate();
|
}
|
return value;
|
}
|
|
private static void requireSameNonEmptyLength(List<?> left, List<?> right,
|
FormulaAst.Node node) {
|
if (left.size() != right.size()) {
|
throw error("LIST_LENGTH_MISMATCH", "数值列表长度必须一致", node);
|
}
|
if (left.isEmpty()) {
|
throw error("INSUFFICIENT_SAMPLE", "数值列表不能为空", node);
|
}
|
}
|
|
private FormulaValue convert(List<FormulaValue> values, FormulaAst.Node node) {
|
BigDecimal value = decimalValue(values.get(0), node);
|
String fromCode = textValue(values.get(1), node);
|
String toCode = textValue(values.get(2), node);
|
Unit from = UNITS.get(fromCode);
|
Unit to = UNITS.get(toCode);
|
if (from == null || to == null) {
|
throw error("UNKNOWN_UNIT", "不支持的单位", node);
|
}
|
if (!from.dimension.equals(to.dimension)) {
|
throw error("UNIT_DIMENSION_MISMATCH", "单位维度不一致", node);
|
}
|
return FormulaValue.decimal(value.multiply(from.baseFactor, MATH_CONTEXT)
|
.divide(to.baseFactor, MATH_CONTEXT));
|
}
|
|
private FormulaValue lookupExact(List<FormulaValue> values, FormulaAst.Node node) {
|
BigDecimal key = decimalValue(values.get(0), node);
|
for (int i = 1; i < values.size(); i += 2) {
|
if (key.compareTo(decimalValue(values.get(i), node)) == 0) {
|
return FormulaValue.decimal(decimalValue(values.get(i + 1), node));
|
}
|
}
|
throw error("LOOKUP_NOT_FOUND", "没有匹配的查值点", node);
|
}
|
|
private FormulaValue lookupLinear(List<FormulaValue> values, FormulaAst.Node node) {
|
BigDecimal key = decimalValue(values.get(0), node);
|
List<BigDecimal> xs = new ArrayList<>((values.size() - 1) / 2);
|
List<BigDecimal> ys = new ArrayList<>((values.size() - 1) / 2);
|
for (int i = 1; i < values.size(); i += 2) {
|
BigDecimal x = decimalValue(values.get(i), node);
|
if (!xs.isEmpty() && x.compareTo(xs.get(xs.size() - 1)) <= 0) {
|
throw error("LOOKUP_POINTS_NOT_INCREASING", "线性插值点必须严格递增", node);
|
}
|
xs.add(x);
|
ys.add(decimalValue(values.get(i + 1), node));
|
}
|
if (key.compareTo(xs.get(0)) < 0 || key.compareTo(xs.get(xs.size() - 1)) > 0) {
|
throw error("OUT_OF_RANGE", "查值超出插值范围", node);
|
}
|
for (int i = 0; i < xs.size(); i++) {
|
int comparison = key.compareTo(xs.get(i));
|
if (comparison == 0) {
|
return FormulaValue.decimal(ys.get(i));
|
}
|
if (comparison < 0) {
|
BigDecimal ratio = key.subtract(xs.get(i - 1), MATH_CONTEXT)
|
.divide(xs.get(i).subtract(xs.get(i - 1), MATH_CONTEXT), MATH_CONTEXT);
|
BigDecimal result = ys.get(i - 1).add(ys.get(i).subtract(ys.get(i - 1),
|
MATH_CONTEXT).multiply(ratio, MATH_CONTEXT), MATH_CONTEXT);
|
return FormulaValue.decimal(result);
|
}
|
}
|
return FormulaValue.decimal(ys.get(ys.size() - 1));
|
}
|
|
private static BigDecimal power(BigDecimal base, BigDecimal exponent,
|
FormulaAst.Node node) {
|
BigDecimal normalized = exponent.stripTrailingZeros();
|
if (normalized.scale() > 0 || normalized.abs().compareTo(
|
BigDecimal.valueOf(MAX_EXPONENT)) > 0) {
|
throw error("INVALID_EXPONENT", "指数必须是绝对值不超过 999 的整数", node);
|
}
|
int value;
|
try {
|
value = normalized.intValueExact();
|
} catch (ArithmeticException ex) {
|
throw error("INVALID_EXPONENT", "指数必须是绝对值不超过 999 的整数", node);
|
}
|
if (value < 0) {
|
if (base.signum() == 0) {
|
throw error("DIVIDE_BY_ZERO", "0 不能取负指数", node);
|
}
|
return BigDecimal.ONE.divide(base.pow(-value, MATH_CONTEXT), MATH_CONTEXT);
|
}
|
return base.pow(value, MATH_CONTEXT);
|
}
|
|
private static BigDecimal squareRoot(BigDecimal value, FormulaAst.Node node) {
|
if (value.signum() < 0) {
|
throw error("NEGATIVE_SQUARE_ROOT", "负数不能开平方", node);
|
}
|
if (value.signum() == 0) {
|
return BigDecimal.ZERO;
|
}
|
int adjustedExponent = value.precision() - value.scale() - 1;
|
BigDecimal current = BigDecimal.ONE.scaleByPowerOfTen(Math.floorDiv(adjustedExponent, 2));
|
BigDecimal previous = null;
|
for (int i = 0; i < MAX_NEWTON_ITERATIONS; i++) {
|
BigDecimal next = current.add(value.divide(current, MATH_CONTEXT), MATH_CONTEXT)
|
.divide(TWO, MATH_CONTEXT);
|
if (next.compareTo(current) == 0 || (previous != null && next.compareTo(previous) == 0)) {
|
return next.round(MATH_CONTEXT);
|
}
|
previous = current;
|
current = next;
|
}
|
throw error("NUMERIC_DID_NOT_CONVERGE", "平方根迭代未收敛", node);
|
}
|
|
private List<FormulaValue> evaluateAll(List<FormulaAst.Node> arguments,
|
Map<String, FormulaValue> parameters,
|
EvaluationContext context) {
|
List<FormulaValue> result = new ArrayList<>(arguments.size());
|
for (FormulaAst.Node argument : arguments) {
|
result.add(evaluate(argument, parameters, context));
|
}
|
return result;
|
}
|
|
private static FormulaValue literal(FormulaAst.Literal literal) {
|
switch (literal.getKind()) {
|
case DECIMAL:
|
return FormulaValue.decimal(literal.getDecimalValue());
|
case TEXT:
|
return FormulaValue.text((String) literal.getValue());
|
case BOOLEAN:
|
return FormulaValue.bool((Boolean) literal.getValue());
|
case NULL:
|
return FormulaValue.nullValue();
|
default:
|
throw new IllegalStateException("unknown literal kind");
|
}
|
}
|
|
private static boolean equalValues(FormulaValue left, FormulaValue right,
|
FormulaAst.Node node) {
|
requirePresent(left, node);
|
requirePresent(right, node);
|
if (left.getType() != right.getType()
|
|| (left.getType() != FormulaType.DECIMAL
|
&& left.getType() != FormulaType.TEXT
|
&& left.getType() != FormulaType.BOOLEAN)) {
|
throw error("TYPE_MISMATCH", "相等比较仅支持相同的标量类型", node);
|
}
|
return left.equals(right);
|
}
|
|
private static boolean isBlank(FormulaValue value) {
|
return value.isNull()
|
|| (value.getType() == FormulaType.TEXT && isBlankText(value.getText()))
|
|| (value.getType() == FormulaType.DECIMAL_LIST
|
&& value.getDecimalList().isEmpty());
|
}
|
|
private static boolean isBlankText(String value) {
|
for (int offset = 0; offset < value.length(); ) {
|
int codePoint = value.codePointAt(offset);
|
if (!Character.isWhitespace(codePoint) && !Character.isSpaceChar(codePoint)) {
|
return false;
|
}
|
offset += Character.charCount(codePoint);
|
}
|
return true;
|
}
|
|
private static BigDecimal decimalValue(FormulaValue value, FormulaAst.Node node) {
|
requirePresent(value, node);
|
if (value.getType() != FormulaType.DECIMAL) {
|
throw error("TYPE_MISMATCH", "需要 DECIMAL,实际为 " + value.getType(), node);
|
}
|
return value.getDecimal();
|
}
|
|
private static List<FormulaValue> decimalList(FormulaValue value, FormulaAst.Node node) {
|
requirePresent(value, node);
|
if (value.getType() != FormulaType.DECIMAL_LIST) {
|
throw error("TYPE_MISMATCH", "需要 DECIMAL_LIST,实际为 " + value.getType(), node);
|
}
|
return value.getDecimalList();
|
}
|
|
private static String textValue(FormulaValue value, FormulaAst.Node node) {
|
requirePresent(value, node);
|
if (value.getType() != FormulaType.TEXT) {
|
throw error("TYPE_MISMATCH", "需要 TEXT,实际为 " + value.getType(), node);
|
}
|
return value.getText();
|
}
|
|
private static boolean booleanValue(FormulaValue value, FormulaAst.Node node) {
|
requirePresent(value, node);
|
if (value.getType() != FormulaType.BOOLEAN) {
|
throw error("TYPE_MISMATCH", "需要 BOOLEAN,实际为 " + value.getType(), node);
|
}
|
return value.getBoolean();
|
}
|
|
private static void requirePresent(FormulaValue value, FormulaAst.Node node) {
|
String missingSlotCode = value.getMissingSlotCode();
|
if (missingSlotCode == null) {
|
return;
|
}
|
String message = "缺少参数值: " + missingSlotCode;
|
if (node == null) {
|
throw new FormulaException("MISSING_INPUT", message);
|
}
|
throw error("MISSING_INPUT", message, node);
|
}
|
|
private static int scale(BigDecimal value, FormulaAst.Node node) {
|
BigDecimal normalized = value.stripTrailingZeros();
|
if (normalized.scale() > 0) {
|
throw error("INVALID_SCALE", "修约位数必须是 0 到 12 的整数", node);
|
}
|
int scale;
|
try {
|
scale = normalized.intValueExact();
|
} catch (ArithmeticException ex) {
|
throw error("INVALID_SCALE", "修约位数必须是 0 到 12 的整数", node);
|
}
|
validateScale(scale, node);
|
return scale;
|
}
|
|
private static int significantDigits(BigDecimal value, FormulaAst.Node node) {
|
BigDecimal normalized = value.stripTrailingZeros();
|
int digits;
|
try {
|
if (normalized.scale() > 0) {
|
throw new ArithmeticException("not an integer");
|
}
|
digits = normalized.intValueExact();
|
} catch (ArithmeticException ex) {
|
throw error("INVALID_SIGNIFICANT_DIGITS", "有效数字必须是 1 到 34 的整数", node);
|
}
|
if (digits < 1 || digits > MAX_SIGNIFICANT_DIGITS) {
|
throw error("INVALID_SIGNIFICANT_DIGITS", "有效数字必须是 1 到 34 的整数", node);
|
}
|
return digits;
|
}
|
|
private static int rootDegree(BigDecimal value, FormulaAst.Node node) {
|
BigDecimal normalized = value.stripTrailingZeros();
|
int degree;
|
try {
|
if (normalized.scale() > 0) {
|
throw new ArithmeticException("not an integer");
|
}
|
degree = normalized.intValueExact();
|
} catch (ArithmeticException ex) {
|
throw error("INVALID_ROOT_DEGREE", "方根次数必须是 2 到 999 的整数", node);
|
}
|
if (degree < 2 || degree > MAX_EXPONENT) {
|
throw error("INVALID_ROOT_DEGREE", "方根次数必须是 2 到 999 的整数", node);
|
}
|
return degree;
|
}
|
|
private static void requirePositiveLogValue(BigDecimal value, FormulaAst.Node node) {
|
if (value.signum() <= 0) {
|
throw error("LOG_DOMAIN_ERROR", "对数真数必须大于 0", node);
|
}
|
}
|
|
private static void validateScale(int scale, FormulaAst.Node node) {
|
if (scale < 0 || scale > MAX_SCALE) {
|
if (node == null) {
|
throw new FormulaException("INVALID_SCALE", "修约位数必须在 0 到 12 之间");
|
}
|
throw error("INVALID_SCALE", "修约位数必须在 0 到 12 之间", node);
|
}
|
}
|
|
private static void requireNonZero(BigDecimal value, FormulaAst.Node node) {
|
if (value.signum() == 0) {
|
throw error("DIVIDE_BY_ZERO", "除数不能为 0", node);
|
}
|
}
|
|
private static void lookupCount(List<?> arguments, int minimum, FormulaAst.Node node) {
|
if (arguments.size() < minimum || arguments.size() % 2 == 0) {
|
throw error("FUNCTION_ARGUMENT_COUNT_MISMATCH", "查值函数参数必须为 key 和数值点对", node);
|
}
|
}
|
|
private static void count(List<?> arguments, int minimum, int maximum,
|
FormulaAst.Node node) {
|
if (arguments.size() < minimum || arguments.size() > maximum) {
|
throw error("FUNCTION_ARGUMENT_COUNT_MISMATCH", "函数参数数量不正确", node);
|
}
|
}
|
|
private static FormulaException error(String code, String message, FormulaAst.Node node) {
|
return new FormulaException(code, message, node.getStartOffset(), node.getEndOffset());
|
}
|
|
private static Map<String, Unit> units() {
|
Map<String, Unit> values = new HashMap<>();
|
values.put("kg", new Unit("MASS", new BigDecimal("1")));
|
values.put("g", new Unit("MASS", new BigDecimal("0.001")));
|
values.put("mg", new Unit("MASS", new BigDecimal("0.000001")));
|
values.put("ug", new Unit("MASS", new BigDecimal("0.000000001")));
|
values.put("L", new Unit("VOLUME", new BigDecimal("1")));
|
values.put("mL", new Unit("VOLUME", new BigDecimal("0.001")));
|
values.put("uL", new Unit("VOLUME", new BigDecimal("0.000001")));
|
return Collections.unmodifiableMap(values);
|
}
|
|
private static final class Unit {
|
private final String dimension;
|
private final BigDecimal baseFactor;
|
|
private Unit(String dimension, BigDecimal baseFactor) {
|
this.dimension = dimension;
|
this.baseFactor = baseFactor;
|
}
|
}
|
|
private static final class RegressionStats {
|
private final BigDecimal sumXX;
|
private final BigDecimal sumYY;
|
private final BigDecimal sumXY;
|
private final BigDecimal slope;
|
private final BigDecimal intercept;
|
private final BigDecimal predictionSlopeNumerator;
|
private final BigDecimal interceptNumerator;
|
private final BigDecimal commonDenominator;
|
|
private RegressionStats(BigDecimal sumXX, BigDecimal sumYY, BigDecimal sumXY,
|
BigDecimal slope, BigDecimal intercept,
|
BigDecimal predictionSlopeNumerator,
|
BigDecimal interceptNumerator,
|
BigDecimal commonDenominator) {
|
this.sumXX = sumXX;
|
this.sumYY = sumYY;
|
this.sumXY = sumXY;
|
this.slope = slope;
|
this.intercept = intercept;
|
this.predictionSlopeNumerator = predictionSlopeNumerator;
|
this.interceptNumerator = interceptNumerator;
|
this.commonDenominator = commonDenominator;
|
}
|
|
private BigDecimal predict(BigDecimal value) {
|
return predictionSlopeNumerator.multiply(value).add(interceptNumerator)
|
.divide(commonDenominator, MATH_CONTEXT);
|
}
|
}
|
|
private static final class EvaluationContext {
|
private final int maximum;
|
private int visited;
|
|
private EvaluationContext(int maximum) {
|
this.maximum = maximum;
|
}
|
|
private void visit(FormulaAst.Node node) {
|
visited++;
|
if (visited > maximum) {
|
throw error("EVALUATION_NODE_LIMIT_EXCEEDED", "求值节点数量超过限制", node);
|
}
|
}
|
}
|
}
|