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<FormulaFunction> 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<FormulaDiagnostic> diagnostics = new ArrayList<>(validation.getDiagnostics());
|
Map<String, JsonNode> inputs = form.getInputs() == null
|
? Collections.<String, JsonNode>emptyMap() : form.getInputs();
|
Set<String> 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<String, FormulaValue> 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<FormulaDiagnostic> 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<FormulaValidationService.ParameterSlot> 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<FormulaValue> 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<FormulaDiagnostic> 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<FormulaDiagnostic> diagnostics) {
|
for (FormulaDiagnostic diagnostic : diagnostics) {
|
if (diagnostic.getSeverity() == FormulaDiagnostic.Severity.ERROR) {
|
return true;
|
}
|
}
|
return false;
|
}
|
|
private static boolean hasOnlyMissingErrors(List<FormulaDiagnostic> 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;
|
}
|
}
|
}
|