package jnpf.elnService.formula.impl; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.DoubleNode; import com.fasterxml.jackson.databind.node.FloatNode; import jnpf.elnEntity.formula.ElnFormulaForms; import jnpf.elnEntity.formula.ElnFormulaVos; import jnpf.elnFormula.FormulaAst; import jnpf.elnFormula.FormulaDiagnostic; import jnpf.elnFormula.FormulaEvaluator; import jnpf.elnFormula.FormulaException; import jnpf.elnFormula.FormulaFunction; import jnpf.elnFormula.FormulaFunctionRegistry; import jnpf.elnFormula.FormulaLimits; import jnpf.elnFormula.FormulaParser; import jnpf.elnFormula.FormulaType; import jnpf.elnFormula.FormulaValue; import jnpf.elnService.formula.FormulaPreviewService; import jnpf.elnService.formula.FormulaValidationService; import org.springframework.stereotype.Service; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Pattern; @Service public class FormulaPreviewServiceImpl implements FormulaPreviewService { private static final Pattern DECIMAL = Pattern.compile("[+-]?[0-9]+(?:\\.[0-9]+)?"); private final FormulaValidationService validationService; private final FormulaFunctionRegistry functions; private final FormulaParser parser; private final FormulaEvaluator evaluator; public FormulaPreviewServiceImpl(FormulaValidationService validationService) { this.validationService = validationService; this.functions = new FormulaFunctionRegistry(); this.parser = new FormulaParser(); this.evaluator = new FormulaEvaluator(functions); } @Override public List functions() { return functions.catalog(); } @Override public FormulaValidationService.ValidationResult validate(ElnFormulaForms.Validate form) { return validateRule(form); } @Override public ElnFormulaVos.Preview preview(ElnFormulaForms.Preview form) { FormulaValidationService.ValidationResult validation = validateRule(form); if (validation.hasErrors()) { return result("ERROR", null, null, validation.getDiagnostics(), validation.getLatex()); } List diagnostics = new ArrayList<>(validation.getDiagnostics()); Map inputs = form.getInputs() == null ? Collections.emptyMap() : form.getInputs(); Set declaredCodes = new LinkedHashSet<>(); for (ElnFormulaForms.Parameter parameter : form.getParameters()) { declaredCodes.add(parameter.getCanshuBianma()); } for (String inputCode : inputs.keySet()) { if (!declaredCodes.contains(inputCode)) { diagnostics.add(error("UNKNOWN_INPUT", "未知参数输入: " + inputCode, -1, -1)); } } if (hasErrors(diagnostics)) { return result("ERROR", null, null, diagnostics, validation.getLatex()); } Map values = new LinkedHashMap<>(); for (ElnFormulaForms.Parameter parameter : form.getParameters()) { String code = parameter.getCanshuBianma(); JsonNode input = inputs.get(code); if (!inputs.containsKey(code) || isMissing(input)) { if (parameter.getShifouBitian() == null || parameter.getShifouBitian() == 1) { diagnostics.add(error("MISSING_INPUT", "缺少必填参数值: " + code, -1, -1)); } else { values.put(code, FormulaValue.nullValue(code)); } continue; } try { values.put(code, convert(input, formulaType(parameter.getShujuLeixing()), code)); } catch (InputException ex) { diagnostics.add(error(ex.code, ex.getMessage(), -1, -1)); } } if (hasErrors(diagnostics)) { String status = hasOnlyMissingErrors(diagnostics) ? "MISSING_INPUT" : "ERROR"; return result(status, null, null, diagnostics, validation.getLatex()); } FormulaValue raw; try { FormulaAst.Node ast = parser.parse(form.getBiaodashi()); raw = evaluator.evaluate(ast, values); } catch (FormulaException ex) { diagnostics.add(error(ex.getCode(), ex.getMessage(), ex.getStartOffset(), ex.getEndOffset())); String status = "MISSING_INPUT".equals(ex.getCode()) ? "MISSING_INPUT" : "ERROR"; return result(status, null, null, diagnostics, validation.getLatex()); } if (raw.isNull()) { diagnostics.add(error("MISSING_INPUT", "公式结果为空", -1, -1)); return result("MISSING_INPUT", null, null, diagnostics, validation.getLatex()); } FormulaValue rounded = raw; if (form.getXiuyueXiaoshuWei() != null) { rounded = evaluator.round(raw, form.getXiuyueXiaoshuWei(), RoundingMode.valueOf(form.getXiuyueMoshi())); } return result("SUCCESS", value(raw), value(rounded), diagnostics, validation.getLatex()); } private FormulaValidationService.ValidationResult validateRule( ElnFormulaForms.VersionRule form) { FormulaValidationService.ValidationResult validation = form == null ? validationService.validate(null) : validationService.validate(toValidationRequest(form)); if (form == null || form.getParameters() == null) { return validation; } List diagnostics = new ArrayList<>(validation.getDiagnostics()); for (ElnFormulaForms.Parameter parameter : form.getParameters()) { if (parameter == null) { continue; } Integer required = parameter.getShifouBitian(); if (required == null || (required != 0 && required != 1)) { diagnostics.add(error("INVALID_PARAMETER_REQUIRED_FLAG", "参数必填标志只能是 0 或 1: " + parameter.getCanshuBianma(), -1, -1)); } } String latex = hasErrors(diagnostics) ? null : validation.getLatex(); if (diagnostics.size() == validation.getDiagnostics().size() && latex == validation.getLatex()) { return validation; } return new FormulaValidationService.ValidationResult(diagnostics, latex, validation.getInferredType()); } private static FormulaValidationService.FormulaValidationRequest toValidationRequest( ElnFormulaForms.VersionRule form) { List slots = null; if (form.getParameters() != null) { slots = new ArrayList<>(); for (ElnFormulaForms.Parameter parameter : form.getParameters()) { if (parameter == null) { slots.add(null); } else { slots.add(new FormulaValidationService.ParameterSlot( parameter.getCanshuBianma(), parameter.getCanshuMingcheng(), formulaType(parameter.getShujuLeixing()))); } } } return new FormulaValidationService.FormulaValidationRequest( form.getBiaodashi(), slots, formulaType(form.getJieguoLeixing()), form.getXiuyueXiaoshuWei(), form.getXiuyueMoshi()); } private static FormulaType formulaType(String value) { if (value == null) { return null; } try { return FormulaType.valueOf(value); } catch (IllegalArgumentException ex) { return null; } } private static boolean isMissing(JsonNode input) { return input == null || input.isNull() || (input.isTextual() && input.textValue().isEmpty()); } private static FormulaValue convert(JsonNode input, FormulaType type, String code) { if (type == FormulaType.DECIMAL) { return FormulaValue.decimal(decimal(input, code)); } if (type == FormulaType.DECIMAL_LIST) { if (!input.isArray()) { throw inputError("INVALID_INPUT_TYPE", code, "必须是数值数组"); } if (input.size() > FormulaLimits.DEFAULTS.getMaxListItems()) { throw inputError("LIST_ITEM_LIMIT_EXCEEDED", code, "数值列表最多 1000 项"); } List values = new ArrayList<>(input.size()); for (JsonNode item : input) { values.add(FormulaValue.decimal(decimal(item, code))); } return FormulaValue.decimalList(values); } if (type == FormulaType.TEXT) { if (!input.isTextual()) { throw inputError("INVALID_INPUT_TYPE", code, "必须是文本"); } return FormulaValue.text(input.textValue()); } if (type == FormulaType.BOOLEAN) { if (!input.isBoolean()) { throw inputError("INVALID_INPUT_TYPE", code, "必须是布尔值"); } return FormulaValue.bool(input.booleanValue()); } throw inputError("INVALID_INPUT_TYPE", code, "参数类型不支持试算"); } private static BigDecimal decimal(JsonNode input, String code) { String source; if (input.isNumber()) { if (input instanceof FloatNode || input instanceof DoubleNode) { double value = input.doubleValue(); if (Double.isNaN(value) || Double.isInfinite(value)) { throw inputError("INVALID_DECIMAL_INPUT", code, "不是有限十进制数"); } } source = input instanceof ElnFormulaForms.InputDecimalNode ? ((ElnFormulaForms.InputDecimalNode) input).getSource() : input.toString(); } else if (input.isTextual()) { source = input.textValue(); } else { throw inputError("INVALID_INPUT_TYPE", code, "必须是十进制数或十进制字符串"); } if (source.length() > FormulaLimits.DEFAULTS.getMaxExpressionLength()) { throw inputError("DECIMAL_INPUT_TOO_LONG", code, "十进制数长度超过限制"); } if (!DECIMAL.matcher(source).matches()) { throw inputError("INVALID_DECIMAL_INPUT", code, "十进制格式不正确"); } return new BigDecimal(source); } private static Object value(FormulaValue value) { if (value.getType() == FormulaType.DECIMAL) { return value.getDecimal(); } if (value.getType() == FormulaType.TEXT) { return value.getText(); } if (value.getType() == FormulaType.BOOLEAN) { return value.getBoolean(); } return null; } private static ElnFormulaVos.Preview result(String status, Object raw, Object rounded, List diagnostics, String latex) { ElnFormulaVos.Preview result = new ElnFormulaVos.Preview(); result.setStatus(status); result.setRawResult(raw); result.setRoundedResult(rounded); result.setLatex(latex); for (FormulaDiagnostic diagnostic : diagnostics) { ElnFormulaVos.Diagnostic item = new ElnFormulaVos.Diagnostic(); item.setSeverity(diagnostic.getSeverity().name()); item.setCode(diagnostic.getCode()); item.setMessage(diagnostic.getMessage()); item.setStart(diagnostic.getStart()); item.setEnd(diagnostic.getEnd()); result.getDiagnostics().add(item); } return result; } private static FormulaDiagnostic error(String code, String message, int start, int end) { return new FormulaDiagnostic(FormulaDiagnostic.Severity.ERROR, code, message, start, end); } private static boolean hasErrors(List diagnostics) { for (FormulaDiagnostic diagnostic : diagnostics) { if (diagnostic.getSeverity() == FormulaDiagnostic.Severity.ERROR) { return true; } } return false; } private static boolean hasOnlyMissingErrors(List diagnostics) { for (FormulaDiagnostic diagnostic : diagnostics) { if (diagnostic.getSeverity() == FormulaDiagnostic.Severity.ERROR && !"MISSING_INPUT".equals(diagnostic.getCode())) { return false; } } return true; } private static InputException inputError(String code, String slotCode, String message) { return new InputException(code, "参数 " + slotCode + " " + message); } private static final class InputException extends RuntimeException { private final String code; private InputException(String code, String message) { super(message); this.code = code; } } }