diff --git a/src/jsonlogic/operators/__init__.py b/src/jsonlogic/operators/__init__.py index 8d7ca77..d950480 100644 --- a/src/jsonlogic/operators/__init__.py +++ b/src/jsonlogic/operators/__init__.py @@ -13,6 +13,7 @@ Map, Minus, Modulo, + Multiply, NotEqual, Plus, Var, @@ -29,6 +30,7 @@ "Map", "Minus", "Modulo", + "Multiply", "NotEqual", "Plus", "Var", @@ -49,5 +51,6 @@ operator_registry.register("+", Plus) operator_registry.register("-", Minus) operator_registry.register("/", Division) +operator_registry.register("*", Multiply) operator_registry.register("%", Modulo) operator_registry.register("map", Map) diff --git a/src/jsonlogic/operators/operators.py b/src/jsonlogic/operators/operators.py index 5f67496..c632e0c 100644 --- a/src/jsonlogic/operators/operators.py +++ b/src/jsonlogic/operators/operators.py @@ -235,6 +235,41 @@ class Modulo(BinaryOperator): operator_symbol = "%" +@dataclass +class Multiply(Operator): + arguments: list[OperatorArgument] + + @classmethod + def from_expression(cls, operator: str, arguments: list[OperatorArgument]) -> Self: + if not len(arguments) >= 2: + raise JSONLogicSyntaxError(f"{operator!r} expects at least two arguments, got {len(arguments)}") + return cls(operator=operator, arguments=arguments) + + def typecheck(self, context: TypecheckContext) -> JSONSchemaType: + types = (get_type(obj, context) for obj in self.arguments) + result_type = next(types) + + for i, typ in enumerate(types, start=1): + try: + result_type = result_type.binary_op(typ, "*") + except UnsupportedOperation: + if len(self.arguments) == 2: + msg = f'Operator "*" not supported for types {result_type.name} and {typ.name}' + else: + msg = f'Operator "*" not supported for types {result_type.name} (argument {i}) and {typ.name} (argument {i + 1})' # noqa: E501 + context.add_diagnostic( + msg, + "operator", + self, + ) + return AnyType() + + return result_type + + def evaluate(self, context: EvaluationContext) -> Any: + return functools.reduce(lambda a, b: get_value(a, context) * get_value(b, context), self.arguments) + + @dataclass class Plus(Operator): arguments: list[OperatorArgument] diff --git a/tests/operators/test_evaluate.py b/tests/operators/test_evaluate.py index 1b8ab75..f2e67d8 100644 --- a/tests/operators/test_evaluate.py +++ b/tests/operators/test_evaluate.py @@ -137,6 +137,26 @@ def test_binary_op() -> None: assert rv is True +def test_multiply() -> None: + op_two_operands = as_op({"*": [2, 2]}) + rv = evaluate( + op_two_operands, + data={}, + data_schema=None, + ) + + assert rv == 4 + + op_three_operands = as_op({"*": [2, 2, 2]}) + rv = evaluate( + op_three_operands, + data={}, + data_schema=None, + ) + + assert rv == 8 + + def test_plus() -> None: op_two_operands = as_op({"+": [1, 2]}) rv = evaluate( diff --git a/tests/operators/test_from_expression.py b/tests/operators/test_from_expression.py index a72dc5d..a84bc7d 100644 --- a/tests/operators/test_from_expression.py +++ b/tests/operators/test_from_expression.py @@ -7,6 +7,7 @@ If, Map, Minus, + Multiply, Plus, Var, ) @@ -63,6 +64,14 @@ def test_binary_op() -> None: GreaterThan.from_expression(">", [1, 2, 3]) +def test_multiply() -> None: + multipy = Multiply.from_expression("*", [1, 2, 3, 4]) + assert multipy.arguments == [1, 2, 3, 4] + + with pytest.raises(JSONLogicSyntaxError, match="'*' expects at least two arguments, got 1"): + Plus.from_expression("*", [1]) + + def test_plus() -> None: plus = Plus.from_expression("+", [1, 2, 3, 4]) assert plus.arguments == [1, 2, 3, 4] diff --git a/tests/operators/test_typecheck.py b/tests/operators/test_typecheck.py index ed5d3fa..811178f 100644 --- a/tests/operators/test_typecheck.py +++ b/tests/operators/test_typecheck.py @@ -166,6 +166,30 @@ def test_binary_op() -> None: assert diag.message == 'Operator ">=" not supported for types integer and date' +def test_multiply() -> None: + op_two_operands = as_op({"*": ["a", "b"]}) + rt, diagnostics = typecheck(op_two_operands, {}) + diag = diagnostics[0] + + assert rt == AnyType() + assert diag.category == "operator" + assert diag.message == 'Operator "*" not supported for types string and string' + + op_three_operands = as_op({"*": ["a", 1, 2]}) + rt, diagnostics = typecheck(op_three_operands, {}) + diag = diagnostics[0] + + assert rt == AnyType() + assert diag.category == "operator" + assert diag.message == 'Operator "*" not supported for types string (argument 1) and integer (argument 2)' + + op_ok = as_op({"*": [1, 2, 3, 4]}) + rt, diagnostics = typecheck(op_ok, {}) + + assert rt == IntegerType() + assert diagnostics == [] + + def test_plus() -> None: op_two_operands = as_op({"+": ["a", "b"]}) rt, diagnostics = typecheck(op_two_operands, {})