package jnpf.elnService.formula.impl;
|
|
import jnpf.elnFormula.FormulaAst;
|
import jnpf.elnFormula.FormulaDiagnostic;
|
import jnpf.elnFormula.FormulaException;
|
import jnpf.elnFormula.FormulaFunctionRegistry;
|
import jnpf.elnFormula.FormulaLatexRenderer;
|
import jnpf.elnFormula.FormulaLimits;
|
import jnpf.elnFormula.FormulaParser;
|
import jnpf.elnFormula.FormulaType;
|
import jnpf.elnFormula.FormulaTypeChecker;
|
import jnpf.elnService.formula.FormulaValidationService;
|
import org.springframework.stereotype.Service;
|
|
import java.util.ArrayList;
|
import java.util.Arrays;
|
import java.util.HashSet;
|
import java.util.LinkedHashMap;
|
import java.util.List;
|
import java.util.Locale;
|
import java.util.Map;
|
import java.util.Set;
|
import java.util.regex.Pattern;
|
|
@Service
|
public class FormulaValidationServiceImpl implements FormulaValidationService {
|
|
private static final Pattern PARAMETER_CODE = Pattern.compile("[a-z][a-z0-9_]*");
|
private static final Set<String> ROUNDING_MODES = new HashSet<>(
|
Arrays.asList("HALF_UP", "HALF_EVEN", "UP", "DOWN"));
|
|
private final FormulaParser parser;
|
private final FormulaFunctionRegistry functions;
|
private final FormulaTypeChecker typeChecker;
|
private final FormulaLatexRenderer latexRenderer;
|
|
public FormulaValidationServiceImpl() {
|
this.parser = new FormulaParser();
|
this.functions = new FormulaFunctionRegistry();
|
this.typeChecker = new FormulaTypeChecker(functions);
|
this.latexRenderer = new FormulaLatexRenderer();
|
}
|
|
@Override
|
public ValidationResult validate(FormulaValidationRequest request) {
|
List<FormulaDiagnostic> diagnostics = new ArrayList<>();
|
validateInput(request, diagnostics);
|
if (hasErrors(diagnostics)) {
|
return result(diagnostics, null, null);
|
}
|
|
FormulaAst.Node ast;
|
try {
|
ast = parser.parse(request.getExpression());
|
} catch (FormulaParser.ParseException ex) {
|
diagnostics.add(error(ex.getCode(), ex.getMessage(),
|
ex.getStartOffset(), ex.getEndOffset()));
|
return result(diagnostics, null, null);
|
}
|
|
Map<String, FormulaType> parameterTypes = new LinkedHashMap<>();
|
Map<String, String> parameterNames = new LinkedHashMap<>();
|
for (ParameterSlot slot : request.getParameters()) {
|
parameterTypes.put(slot.getCode(), slot.getType());
|
parameterNames.put(slot.getCode(), slot.getDisplayName());
|
if (functions.contains(slot.getCode().toUpperCase(Locale.ROOT))) {
|
diagnostics.add(error("PARAMETER_FUNCTION_NAME_CONFLICT",
|
"参数编码与函数名冲突: " + slot.getCode(), -1, -1));
|
}
|
}
|
|
Set<String> usedParameters = new HashSet<>();
|
collectParameters(ast, usedParameters, diagnostics, parameterTypes);
|
for (ParameterSlot slot : request.getParameters()) {
|
if (!usedParameters.contains(slot.getCode())) {
|
diagnostics.add(warning("UNUSED_PARAMETER",
|
"参数未在公式中使用: " + slot.getCode(), -1, -1));
|
}
|
}
|
if (hasErrors(diagnostics)) {
|
return result(diagnostics, null, null);
|
}
|
|
validateFunctionSignatures(ast, diagnostics);
|
if (hasErrors(diagnostics)) {
|
return result(diagnostics, null, null);
|
}
|
|
FormulaType inferredType;
|
try {
|
inferredType = typeChecker.check(ast, parameterTypes);
|
} catch (FormulaException ex) {
|
diagnostics.add(error(ex.getCode(), ex.getMessage(),
|
ex.getStartOffset(), ex.getEndOffset()));
|
return result(diagnostics, null, null);
|
}
|
|
if (inferredType != request.getResultType()) {
|
diagnostics.add(error("RESULT_TYPE_MISMATCH",
|
"公式结果类型与声明类型不一致", ast.getStartOffset(), ast.getEndOffset()));
|
return result(diagnostics, null, inferredType);
|
}
|
|
validateRounding(request, diagnostics);
|
if (hasErrors(diagnostics)) {
|
return result(diagnostics, null, inferredType);
|
}
|
|
return result(diagnostics, latexRenderer.render(ast, parameterNames), inferredType);
|
}
|
|
private static void validateInput(FormulaValidationRequest request,
|
List<FormulaDiagnostic> diagnostics) {
|
if (request == null) {
|
diagnostics.add(error("INVALID_REQUEST", "验证请求不能为空", -1, -1));
|
return;
|
}
|
String expression = request.getExpression();
|
if (expression == null || expression.trim().isEmpty()) {
|
diagnostics.add(error("EXPRESSION_REQUIRED", "公式不能为空", 0,
|
expression == null ? 0 : expression.length()));
|
}
|
List<ParameterSlot> parameters = request.getParameters();
|
if (parameters == null) {
|
diagnostics.add(error("PARAMETERS_REQUIRED", "参数列表不能为空", -1, -1));
|
} else {
|
Set<String> codes = new HashSet<>();
|
for (ParameterSlot slot : parameters) {
|
if (slot == null) {
|
diagnostics.add(error("INVALID_PARAMETER", "参数项不能为空", -1, -1));
|
continue;
|
}
|
if (slot.getCode() == null || !PARAMETER_CODE.matcher(slot.getCode()).matches()) {
|
diagnostics.add(error("INVALID_PARAMETER_CODE", "参数编码格式不正确", -1, -1));
|
} else {
|
if (FormulaParser.isReservedParameterCode(slot.getCode())) {
|
diagnostics.add(error("RESERVED_PARAMETER_CODE", "参数编码为公式保留字: "
|
+ slot.getCode(), -1, -1));
|
}
|
if (!codes.add(slot.getCode())) {
|
diagnostics.add(error("DUPLICATE_PARAMETER", "参数编码重复: "
|
+ slot.getCode(), -1, -1));
|
}
|
}
|
if (slot.getDisplayName() == null || slot.getDisplayName().trim().isEmpty()) {
|
diagnostics.add(error("INVALID_PARAMETER_DISPLAY_NAME", "参数显示名不能为空", -1, -1));
|
}
|
if (slot.getType() == null || slot.getType() == FormulaType.NULL) {
|
diagnostics.add(error("INVALID_PARAMETER_TYPE", "参数类型不合法", -1, -1));
|
}
|
}
|
}
|
FormulaType resultType = request.getResultType();
|
if (resultType != FormulaType.DECIMAL && resultType != FormulaType.TEXT
|
&& resultType != FormulaType.BOOLEAN) {
|
diagnostics.add(error("INVALID_RESULT_TYPE", "声明结果类型不合法", -1, -1));
|
}
|
}
|
|
private static void validateRounding(FormulaValidationRequest request,
|
List<FormulaDiagnostic> diagnostics) {
|
Integer scale = request.getRoundScale();
|
String mode = request.getRoundMode();
|
if ((scale == null) != (mode == null)) {
|
diagnostics.add(error("INVALID_ROUNDING_CONFIGURATION", "修约位数和模式必须同时提供", -1, -1));
|
return;
|
}
|
if (scale == null) {
|
return;
|
}
|
if (scale < 0 || scale > 12) {
|
diagnostics.add(error("INVALID_ROUNDING_SCALE", "修约位数必须在 0 到 12 之间", -1, -1));
|
}
|
if (!ROUNDING_MODES.contains(mode)) {
|
diagnostics.add(error("UNSUPPORTED_ROUNDING_MODE", "不支持的修约模式", -1, -1));
|
}
|
if (request.getResultType() != FormulaType.DECIMAL) {
|
diagnostics.add(error("ROUNDING_NOT_ALLOWED", "仅数值结果允许配置修约", -1, -1));
|
}
|
}
|
|
private static void collectParameters(FormulaAst.Node node, Set<String> used,
|
List<FormulaDiagnostic> diagnostics,
|
Map<String, FormulaType> declared) {
|
if (node instanceof FormulaAst.Parameter) {
|
FormulaAst.Parameter parameter = (FormulaAst.Parameter) node;
|
used.add(parameter.getCode());
|
if (!declared.containsKey(parameter.getCode())) {
|
diagnostics.add(error("UNKNOWN_PARAMETER", "未知参数: " + parameter.getCode(),
|
parameter.getStartOffset(), parameter.getEndOffset()));
|
}
|
return;
|
}
|
if (node instanceof FormulaAst.Unary) {
|
collectParameters(((FormulaAst.Unary) node).getOperand(), used, diagnostics, declared);
|
} else if (node instanceof FormulaAst.Binary) {
|
FormulaAst.Binary binary = (FormulaAst.Binary) node;
|
collectParameters(binary.getLeft(), used, diagnostics, declared);
|
collectParameters(binary.getRight(), used, diagnostics, declared);
|
} else if (node instanceof FormulaAst.FunctionCall) {
|
for (FormulaAst.Node argument : ((FormulaAst.FunctionCall) node).getArguments()) {
|
collectParameters(argument, used, diagnostics, declared);
|
}
|
}
|
}
|
|
private void validateFunctionSignatures(FormulaAst.Node node,
|
List<FormulaDiagnostic> diagnostics) {
|
if (node instanceof FormulaAst.FunctionCall) {
|
FormulaAst.FunctionCall call = (FormulaAst.FunctionCall) node;
|
String name = call.getName();
|
int count = call.getArguments().size();
|
if (!functions.contains(name)) {
|
diagnostics.add(error("UNKNOWN_FUNCTION", "未知函数: " + name,
|
call.getStartOffset(), call.getEndOffset()));
|
return;
|
}
|
if (!validArgumentCount(name, count)) {
|
diagnostics.add(error("FUNCTION_ARGUMENT_COUNT_MISMATCH", "函数参数数量不正确",
|
call.getStartOffset(), call.getEndOffset()));
|
return;
|
}
|
for (FormulaAst.Node argument : call.getArguments()) {
|
validateFunctionSignatures(argument, diagnostics);
|
if (hasErrors(diagnostics)) {
|
return;
|
}
|
}
|
} else if (node instanceof FormulaAst.Unary) {
|
validateFunctionSignatures(((FormulaAst.Unary) node).getOperand(), diagnostics);
|
} else if (node instanceof FormulaAst.Binary) {
|
FormulaAst.Binary binary = (FormulaAst.Binary) node;
|
validateFunctionSignatures(binary.getLeft(), diagnostics);
|
if (!hasErrors(diagnostics)) {
|
validateFunctionSignatures(binary.getRight(), diagnostics);
|
}
|
}
|
}
|
|
private static boolean validArgumentCount(String name, int count) {
|
if ("ABS".equals(name) || "SQRT".equals(name) || "IS_BLANK".equals(name)
|
|| "LN".equals(name) || "LOG10".equals(name) || "EXP".equals(name)
|
|| "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)) {
|
return count == 1;
|
}
|
if ("POW".equals(name) || "LOG".equals(name) || "NTH_ROOT".equals(name)
|
|| "WEIGHTED_MEAN".equals(name) || "SLOPE".equals(name)
|
|| "INTERCEPT".equals(name) || "CORREL".equals(name)
|
|| "ROUND".equals(name) || "ROUND_UP".equals(name)
|
|| "ROUND_DOWN".equals(name) || "ROUND_HALF_EVEN".equals(name)
|
|| "ROUND_SIG".equals(name) || "ROUND_SIG_HALF_EVEN".equals(name)) {
|
return count == 2;
|
}
|
if ("IF".equals(name) || "CONVERT".equals(name)
|
|| "LINEAR_PREDICT".equals(name)) {
|
return count == 3;
|
}
|
if ("MIN".equals(name) || "MAX".equals(name) || "COALESCE".equals(name)) {
|
return count >= 1 && count <= FormulaLimits.DEFAULTS.getMaxFunctionArguments();
|
}
|
if ("LOOKUP_EXACT".equals(name)) {
|
return count >= 3 && count % 2 == 1;
|
}
|
if ("LOOKUP_LINEAR".equals(name)) {
|
return count >= 5 && count % 2 == 1;
|
}
|
return false;
|
}
|
|
private static ValidationResult result(List<FormulaDiagnostic> diagnostics,
|
String latex, FormulaType inferredType) {
|
return new ValidationResult(diagnostics, latex, inferredType);
|
}
|
|
private static boolean hasErrors(List<FormulaDiagnostic> diagnostics) {
|
for (FormulaDiagnostic diagnostic : diagnostics) {
|
if (diagnostic.getSeverity() == FormulaDiagnostic.Severity.ERROR) {
|
return true;
|
}
|
}
|
return false;
|
}
|
|
private static FormulaDiagnostic error(String code, String message, int start, int end) {
|
return new FormulaDiagnostic(FormulaDiagnostic.Severity.ERROR, code, message, start, end);
|
}
|
|
private static FormulaDiagnostic warning(String code, String message, int start, int end) {
|
return new FormulaDiagnostic(FormulaDiagnostic.Severity.WARNING, code, message, start, end);
|
}
|
}
|