From c5db7df3f80fd99da1d5a4a43e8b793e9a273305 Mon Sep 17 00:00:00 2001 From: yangmingshuo5577-jpg Date: Sun, 20 Sep 2026 18:17:53 +0800 Subject: [PATCH] fix(data-format): preserve values when expanding scientific notation --- backend/common/utils/data_format.py | 2 +- backend/tests/test_data_format.py | 94 +++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 backend/tests/test_data_format.py diff --git a/backend/common/utils/data_format.py b/backend/common/utils/data_format.py index 56a83f866..33fc335c1 100644 --- a/backend/common/utils/data_format.py +++ b/backend/common/utils/data_format.py @@ -55,7 +55,7 @@ def format_float_without_scientific(value): """格式化浮点数,避免科学记数法""" if value == 0: return "0" - formatted = str(Decimal(str(value))) + formatted = format(Decimal(str(value)), 'f') if '.' in formatted: formatted = formatted.rstrip('0').rstrip('.') return formatted diff --git a/backend/tests/test_data_format.py b/backend/tests/test_data_format.py new file mode 100644 index 000000000..de6af3e84 --- /dev/null +++ b/backend/tests/test_data_format.py @@ -0,0 +1,94 @@ +"""Regression tests for numeric values shared by query results and Excel exports.""" + +import ast +from decimal import Decimal +from pathlib import Path + +import pytest + + +@pytest.fixture(scope="module") +def convert_numbers(): + # Follow the existing isolated tests without importing the chat/LLM stack. + source = Path(__file__).resolve().parents[1] / "common/utils/data_format.py" + tree = ast.parse(source.read_text(encoding="utf-8")) + node = next( + node for node in tree.body if getattr(node, "name", None) == "DataFormat" + ) + namespace = {"Decimal": Decimal} + exec( + compile(ast.Module(body=[node], type_ignores=[]), str(source), "exec"), + namespace, + ) + return namespace["DataFormat"].convert_large_numbers_in_object_array + + +@pytest.mark.parametrize( + ("value", "expected"), + [ + (1.23e20, "123000000000000000000"), + (-1.23e20, "-123000000000000000000"), + (1.23e-10, "0.000000000123"), + (-1.23e-10, "-0.000000000123"), + (1e20, "100000000000000000000"), + (1e-7, "0.0000001"), + (1e10, "10000000000"), + (12345678901.25, "12345678901.25"), + (0.0, "0"), + (-0.0, "0"), + ], +) +def test_float_conversion_preserves_value_without_scientific_notation( + convert_numbers, value, expected +): + assert convert_numbers([{"value": value}]) == [{"value": expected}] + + +def test_values_outside_conversion_thresholds_keep_their_types(convert_numbers): + row = { + "regular_float": 123.4, + "small_float_boundary": 1e-6, + "below_float_threshold": 9999999999.5, + "below_int_threshold": 999999999999999, + "large_integer": 1000000000000000, + "negative_integer": -1000000000000000, + "zero": 0, + "flag": True, + "text": "1.23e20", + "missing": None, + } + + converted = convert_numbers([row])[0] + + assert converted == { + **row, + "large_integer": "1000000000000000", + "negative_integer": "-1000000000000000", + } + for key in ("regular_float", "small_float_boundary", "below_float_threshold"): + assert isinstance(converted[key], float) + + +def test_scientific_notation_is_converted_in_nested_rows(convert_numbers): + rows = [{"metrics": {"amount": 1.23e20}, "series": [{"value": 1.23e-10}]}] + + assert convert_numbers(rows) == [ + { + "metrics": {"amount": "123000000000000000000"}, + "series": [{"value": "0.000000000123"}], + } + ] + assert rows[0]["metrics"]["amount"] == 1.23e20 + + +def test_excel_integer_threshold_remains_supported(convert_numbers): + assert convert_numbers( + [{"below": 99999999999, "boundary": 100000000000, "amount": 1.23e20}], + int_threshold=1e11, + ) == [ + { + "below": 99999999999, + "boundary": "100000000000", + "amount": "123000000000000000000", + } + ]