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 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 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 parameterTypes = new LinkedHashMap<>(); Map 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 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 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 parameters = request.getParameters(); if (parameters == null) { diagnostics.add(error("PARAMETERS_REQUIRED", "参数列表不能为空", -1, -1)); } else { Set 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 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 used, List diagnostics, Map 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 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 diagnostics, String latex, FormulaType inferredType) { return new ValidationResult(diagnostics, latex, inferredType); } private static boolean hasErrors(List 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); } }