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<String, FormulaType> 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<String, FormulaType> 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<String, FormulaType> 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<String, FormulaType> parameters) {
|
String name = call.getName();
|
if (!functions.contains(name)) {
|
throw error("UNKNOWN_FUNCTION", "未知函数: " + name, call);
|
}
|
List<FormulaAst.Node> 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<FormulaAst.Node> 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<FormulaAst.Node> arguments,
|
Map<String, FormulaType> 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<FormulaAst.Node> 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());
|
}
|
}
|