package jnpf.elnFormula; import java.util.List; import java.util.Map; public final class FormulaTypeChecker { private final FormulaFunctionRegistry functions; public FormulaTypeChecker(FormulaFunctionRegistry functions) { if (functions == null) { throw new IllegalArgumentException("functions must not be null"); } this.functions = functions; } public FormulaType check(FormulaAst.Node node, Map parameters) { if (node == null || parameters == null) { throw new IllegalArgumentException("node and parameters must not be null"); } return typeOf(node, parameters); } private FormulaType typeOf(FormulaAst.Node node, Map parameters) { if (node instanceof FormulaAst.Literal) { return literalType((FormulaAst.Literal) node); } if (node instanceof FormulaAst.Parameter) { FormulaAst.Parameter parameter = (FormulaAst.Parameter) node; FormulaType type = parameters.get(parameter.getCode()); if (type == null) { throw error("UNKNOWN_PARAMETER", "未知参数: " + parameter.getCode(), node); } return type; } if (node instanceof FormulaAst.Unary) { FormulaAst.Unary unary = (FormulaAst.Unary) node; FormulaType operand = typeOf(unary.getOperand(), parameters); if ("!".equals(unary.getOperator())) { require(operand, FormulaType.BOOLEAN, node); return FormulaType.BOOLEAN; } require(operand, FormulaType.DECIMAL, node); return FormulaType.DECIMAL; } if (node instanceof FormulaAst.Binary) { return binaryType((FormulaAst.Binary) node, parameters); } if (node instanceof FormulaAst.FunctionCall) { return functionType((FormulaAst.FunctionCall) node, parameters); } throw error("TYPE_MISMATCH", "不支持的公式节点", node); } private FormulaType binaryType(FormulaAst.Binary binary, Map parameters) { FormulaType left = typeOf(binary.getLeft(), parameters); FormulaType right = typeOf(binary.getRight(), parameters); String operator = binary.getOperator(); if ("+".equals(operator) || "-".equals(operator) || "*".equals(operator) || "/".equals(operator) || "%".equals(operator) || "^".equals(operator)) { require(left, FormulaType.DECIMAL, binary); require(right, FormulaType.DECIMAL, binary); return FormulaType.DECIMAL; } if (">".equals(operator) || ">=".equals(operator) || "<".equals(operator) || "<=".equals(operator)) { require(left, FormulaType.DECIMAL, binary); require(right, FormulaType.DECIMAL, binary); return FormulaType.BOOLEAN; } if ("&&".equals(operator) || "||".equals(operator)) { require(left, FormulaType.BOOLEAN, binary); require(right, FormulaType.BOOLEAN, binary); return FormulaType.BOOLEAN; } if ("==".equals(operator) || "!=".equals(operator)) { if (left != right || (left != FormulaType.DECIMAL && left != FormulaType.TEXT && left != FormulaType.BOOLEAN)) { throw error("TYPE_MISMATCH", "相等比较仅支持相同的标量类型", binary); } return FormulaType.BOOLEAN; } throw error("TYPE_MISMATCH", "不支持的运算符: " + operator, binary); } private FormulaType functionType(FormulaAst.FunctionCall call, Map parameters) { String name = call.getName(); if (!functions.contains(name)) { throw error("UNKNOWN_FUNCTION", "未知函数: " + name, call); } List arguments = call.getArguments(); if ("ABS".equals(name) || "SQRT".equals(name) || "LN".equals(name) || "LOG10".equals(name) || "EXP".equals(name)) { count(arguments, 1, 1, call); require(typeOf(arguments.get(0), parameters), FormulaType.DECIMAL, call); return FormulaType.DECIMAL; } if ("POW".equals(name) || "LOG".equals(name) || "NTH_ROOT".equals(name)) { count(arguments, 2, 2, call); requireAll(arguments, parameters, FormulaType.DECIMAL, call); return FormulaType.DECIMAL; } if ("MIN".equals(name) || "MAX".equals(name)) { count(arguments, 1, FormulaLimits.DEFAULTS.getMaxFunctionArguments(), call); requireAll(arguments, parameters, FormulaType.DECIMAL, call); return FormulaType.DECIMAL; } if ("IF".equals(name)) { count(arguments, 3, 3, call); require(typeOf(arguments.get(0), parameters), FormulaType.BOOLEAN, call); return merge(typeOf(arguments.get(1), parameters), typeOf(arguments.get(2), parameters), call); } if ("COALESCE".equals(name)) { count(arguments, 1, FormulaLimits.DEFAULTS.getMaxFunctionArguments(), call); FormulaType result = FormulaType.NULL; for (FormulaAst.Node argument : arguments) { result = merge(result, typeOf(argument, parameters), call); } return result; } if ("IS_BLANK".equals(name)) { count(arguments, 1, 1, call); typeOf(arguments.get(0), parameters); return FormulaType.BOOLEAN; } 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(arguments, 1, 1, call); require(typeOf(arguments.get(0), parameters), FormulaType.DECIMAL_LIST, call); return FormulaType.DECIMAL; } if ("WEIGHTED_MEAN".equals(name) || "SLOPE".equals(name) || "INTERCEPT".equals(name) || "CORREL".equals(name)) { count(arguments, 2, 2, call); requireAll(arguments, parameters, FormulaType.DECIMAL_LIST, call); return FormulaType.DECIMAL; } if ("LINEAR_PREDICT".equals(name)) { count(arguments, 3, 3, call); require(typeOf(arguments.get(0), parameters), FormulaType.DECIMAL, call); require(typeOf(arguments.get(1), parameters), FormulaType.DECIMAL_LIST, call); require(typeOf(arguments.get(2), parameters), FormulaType.DECIMAL_LIST, call); return FormulaType.DECIMAL; } if ("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)) { count(arguments, 2, 2, call); requireAll(arguments, parameters, FormulaType.DECIMAL, call); return FormulaType.DECIMAL; } if ("CONVERT".equals(name)) { count(arguments, 3, 3, call); require(typeOf(arguments.get(0), parameters), FormulaType.DECIMAL, call); require(typeOf(arguments.get(1), parameters), FormulaType.TEXT, call); require(typeOf(arguments.get(2), parameters), FormulaType.TEXT, call); return FormulaType.DECIMAL; } if ("LOOKUP_EXACT".equals(name)) { lookupShape(arguments, 3, call); requireAll(arguments, parameters, FormulaType.DECIMAL, call); return FormulaType.DECIMAL; } if ("LOOKUP_LINEAR".equals(name)) { lookupShape(arguments, 5, call); requireAll(arguments, parameters, FormulaType.DECIMAL, call); return FormulaType.DECIMAL; } throw error("UNKNOWN_FUNCTION", "未知函数: " + name, call); } private static FormulaType literalType(FormulaAst.Literal literal) { switch (literal.getKind()) { case DECIMAL: return FormulaType.DECIMAL; case TEXT: return FormulaType.TEXT; case BOOLEAN: return FormulaType.BOOLEAN; case NULL: return FormulaType.NULL; default: throw new IllegalStateException("unknown literal kind"); } } private static void lookupShape(List arguments, int minimum, FormulaAst.Node node) { if (arguments.size() < minimum || arguments.size() % 2 == 0) { throw error("FUNCTION_ARGUMENT_COUNT_MISMATCH", "查值函数参数必须为 key 和数值点对", node); } } private void requireAll(List arguments, Map parameters, FormulaType expected, FormulaAst.Node node) { for (FormulaAst.Node argument : arguments) { require(typeOf(argument, parameters), expected, node); } } private static FormulaType merge(FormulaType left, FormulaType right, FormulaAst.Node node) { if (left == FormulaType.NULL) { return right; } if (right == FormulaType.NULL) { return left; } if (left != right) { throw error("TYPE_MISMATCH", "分支或参数类型不一致", node); } return left; } 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 void require(FormulaType actual, FormulaType expected, FormulaAst.Node node) { requireStatic(actual, expected, node); } private static void requireStatic(FormulaType actual, FormulaType expected, FormulaAst.Node node) { if (actual != expected) { throw error("TYPE_MISMATCH", "需要 " + expected + ",实际为 " + actual, node); } } private static FormulaException error(String code, String message, FormulaAst.Node node) { return new FormulaException(code, message, node.getStartOffset(), node.getEndOffset()); } }