diff --git a/apps/api/services/workbook/conditions.py b/apps/api/services/workbook/conditions.py index a0855330..a6baee79 100644 --- a/apps/api/services/workbook/conditions.py +++ b/apps/api/services/workbook/conditions.py @@ -24,6 +24,7 @@ import logging import re +from decimal import Decimal, InvalidOperation from typing import Dict logger = logging.getLogger("workbook.conditions") @@ -192,15 +193,20 @@ def _compare(left: str, right: str, op: str) -> bool: """Compare two values with the given operator.""" # Try numeric comparison try: - left_num = float(left) - right_num = float(right) + left_num = Decimal(left) + right_num = Decimal(right) + # Finite decimal operands retain the digits supplied by the caller. + # Preserve the existing float semantics for NaN and infinities. + if not left_num.is_finite() or not right_num.is_finite(): + left_num = float(left) + right_num = float(right) if op == "==": return left_num == right_num if op == "!=": return left_num != right_num if op == ">": return left_num > right_num if op == "<": return left_num < right_num if op == ">=": return left_num >= right_num if op == "<=": return left_num <= right_num - except (ValueError, TypeError): + except (InvalidOperation, ValueError, TypeError): pass # String comparison diff --git a/tests/test_workbook_condition_precision.py b/tests/test_workbook_condition_precision.py new file mode 100644 index 00000000..101d59f4 --- /dev/null +++ b/tests/test_workbook_condition_precision.py @@ -0,0 +1,41 @@ +"""Public workbook conditions preserve numeric distinctions in actual row values.""" + +import pytest + +from apps.api.services.workbook.conditions import evaluate_condition + + +@pytest.mark.parametrize("value, threshold, operator, expected", [ + ("9007199254740993", "9007199254740992", "==", False), + ("9007199254740993", "9007199254740992", "!=", True), + ("9007199254740993", "9007199254740992", ">", True), + ("9007199254740993", "9007199254740992", "<", False), + ("9007199254740993", "9007199254740992", ">=", True), + ("9007199254740993", "9007199254740992", "<=", False), + ("-9007199254740993", "-9007199254740992", "==", False), + ("-9007199254740993", "-9007199254740992", "!=", True), + ("-9007199254740993", "-9007199254740992", ">", False), + ("-9007199254740993", "-9007199254740992", "<", True), + ("-9007199254740993", "-9007199254740992", ">=", False), + ("-9007199254740993", "-9007199254740992", "<=", True), + ("9007199254740992.5", "9007199254740992.25", "==", False), + ("9007199254740992.5", "9007199254740992.25", "!=", True), + ("9007199254740992.5", "9007199254740992.25", ">", True), + ("9007199254740992.5", "9007199254740992.25", "<", False), + ("9007199254740992.5", "9007199254740992.25", ">=", True), + ("9007199254740992.5", "9007199254740992.25", "<=", False), + (9007199254740993, "9007199254740992", "==", False), + (-9007199254740993, "-9007199254740992", "<", True), + ("3.50", "3.5", "==", True), + ("1e3", "1000", "==", True), + ("009", "9", "==", True), + ("-4.25", "-4", "<", True), + ("Acme", '"Acme"', "==", True), + ("Other", '"Acme"', "!=", True), +]) +def test_numeric_conditions_keep_original_operand_distinctions( + value, threshold, operator, expected, +): + cells = {"score": {"value": value, "status": "complete"}} + expression = f"{{score}} {operator} {threshold}" + assert evaluate_condition(expression, cells, []) is expected