From ef5981b4735a3c9ac68f353ece60ae2b786c7b96 Mon Sep 17 00:00:00 2001 From: Clemens Schwaighofer Date: Wed, 24 Sep 2025 15:25:53 +0900 Subject: [PATCH] convert_to_seconds allow negative time strings and add pytests --- src/corelibs/debug_handling/debug_helpers.py | 1 + src/corelibs/iterator_handling/data_search.py | 1 + src/corelibs/json_handling/json_helper.py | 1 + .../string_handling/timestamp_strings.py | 7 +- test-run/string_handling/timestamp_strings.py | 10 + .../test_convert_to_seconds.py | 205 ++++++++++++++++++ 6 files changed, 224 insertions(+), 1 deletion(-) create mode 100644 tests/unit/string_handling/test_convert_to_seconds.py diff --git a/src/corelibs/debug_handling/debug_helpers.py b/src/corelibs/debug_handling/debug_helpers.py index a042812..30d0c51 100644 --- a/src/corelibs/debug_handling/debug_helpers.py +++ b/src/corelibs/debug_handling/debug_helpers.py @@ -11,6 +11,7 @@ from types import TracebackType # _typeshed.OptExcInfo OptExcInfo = Tuple[None, None, None] | Tuple[Type[BaseException], BaseException, TracebackType] + def call_stack( start: int = 0, skip_last: int = -1, diff --git a/src/corelibs/iterator_handling/data_search.py b/src/corelibs/iterator_handling/data_search.py index 23c2cc3..5dc550d 100644 --- a/src/corelibs/iterator_handling/data_search.py +++ b/src/corelibs/iterator_handling/data_search.py @@ -22,6 +22,7 @@ def array_search( """depreacted, old call order""" return find_in_array_from_list(data, search_params, return_index) + def find_in_array_from_list( data: list[dict[str, Any]], search_params: list[ArraySearchList], diff --git a/src/corelibs/json_handling/json_helper.py b/src/corelibs/json_handling/json_helper.py index 4da8ecc..8411a53 100644 --- a/src/corelibs/json_handling/json_helper.py +++ b/src/corelibs/json_handling/json_helper.py @@ -28,6 +28,7 @@ def default(obj: Any) -> str | None: return obj.isoformat() return None + def json_dumps(data: Any): """ wrapper for json.dumps with sure dump without throwing Exceptions diff --git a/src/corelibs/string_handling/timestamp_strings.py b/src/corelibs/string_handling/timestamp_strings.py index 375ae4b..f1ca7a1 100644 --- a/src/corelibs/string_handling/timestamp_strings.py +++ b/src/corelibs/string_handling/timestamp_strings.py @@ -60,6 +60,11 @@ def convert_to_seconds(time_string: str | int | float) -> int: return int(round(float(time_string))) time_string = str(time_string) + # Check if the time string is negative + negative = time_string.startswith('-') + if negative: + time_string = time_string[1:] # Remove the negative sign for processing + # Define time unit conversion factors unit_factors: dict[str, int] = { 'Y': 31536000, # 365 days * 86400 seconds/day @@ -109,7 +114,7 @@ def convert_to_seconds(time_string: str | int | float) -> int: seen_units.append(unit) - return total_seconds + return -total_seconds if negative else total_seconds def seconds_to_string(seconds: str | int | float, show_microseconds: bool = False) -> str: diff --git a/test-run/string_handling/timestamp_strings.py b/test-run/string_handling/timestamp_strings.py index 4cd472d..a018381 100644 --- a/test-run/string_handling/timestamp_strings.py +++ b/test-run/string_handling/timestamp_strings.py @@ -23,6 +23,16 @@ def main() -> None: "1d 12h", # 1 day, 12 hours "3M 2d 4h", # 3 months, 2 days, 4 hours "45s", # 45 seconds + "-45s", # -45 seconds + "-1h", # -1 hour + "-30m", # -30 minutes + "-2h 30m 45s", # -2 hours, 30 minutes, 45 seconds + "-1d 12h", # -1 day, 12 hours + "-3M 2d 4h", # -3 months, 2 days, 4 hours + "-1Y 2M 3d", # -1 year, 2 months, 3 days + "-2 hours 15 minutes", # -2 hours, 15 minutes + "-1 year 2 months", # -1 year, 2 months + "-2Y 6M 15d 8h 30m 45s", # Complex negative example "1 year 2 months", # 1 year, 2 months "2Y 6M 15d 8h 30m 45s", # Complex example # invalid tests diff --git a/tests/unit/string_handling/test_convert_to_seconds.py b/tests/unit/string_handling/test_convert_to_seconds.py new file mode 100644 index 0000000..cd0991c --- /dev/null +++ b/tests/unit/string_handling/test_convert_to_seconds.py @@ -0,0 +1,205 @@ +""" +Unit tests for convert_to_seconds function from timestamp_strings module. +""" + +import pytest +from corelibs.string_handling.timestamp_strings import convert_to_seconds, TimeParseError, TimeUnitError + + +class TestConvertToSeconds: + """Test class for convert_to_seconds function.""" + + def test_numeric_input_int(self): + """Test with integer input.""" + assert convert_to_seconds(42) == 42 + assert convert_to_seconds(0) == 0 + assert convert_to_seconds(-5) == -5 + + def test_numeric_input_float(self): + """Test with float input.""" + assert convert_to_seconds(42.7) == 43 # rounds to 43 + assert convert_to_seconds(42.3) == 42 # rounds to 42 + assert convert_to_seconds(42.5) == 42 # rounds to 42 (banker's rounding) + assert convert_to_seconds(0.0) == 0 + assert convert_to_seconds(-5.7) == -6 + + def test_numeric_string_input(self): + """Test with numeric string input.""" + assert convert_to_seconds("42") == 42 + assert convert_to_seconds("42.7") == 43 + assert convert_to_seconds("42.3") == 42 + assert convert_to_seconds("0") == 0 + assert convert_to_seconds("-5.7") == -6 + + def test_single_unit_seconds(self): + """Test with seconds unit.""" + assert convert_to_seconds("30s") == 30 + assert convert_to_seconds("1s") == 1 + assert convert_to_seconds("0s") == 0 + + def test_single_unit_minutes(self): + """Test with minutes unit.""" + assert convert_to_seconds("5m") == 300 # 5 * 60 + assert convert_to_seconds("1m") == 60 + assert convert_to_seconds("0m") == 0 + + def test_single_unit_hours(self): + """Test with hours unit.""" + assert convert_to_seconds("2h") == 7200 # 2 * 3600 + assert convert_to_seconds("1h") == 3600 + assert convert_to_seconds("0h") == 0 + + def test_single_unit_days(self): + """Test with days unit.""" + assert convert_to_seconds("1d") == 86400 # 1 * 86400 + assert convert_to_seconds("2d") == 172800 # 2 * 86400 + assert convert_to_seconds("0d") == 0 + + def test_single_unit_months(self): + """Test with months unit (30 days * 12 = 1 year).""" + # Note: The code has M: 2592000 * 12 which is 1 year, not 1 month + # This seems like a bug in the original code, but testing what it actually does + assert convert_to_seconds("1M") == 31104000 # 2592000 * 12 + assert convert_to_seconds("2M") == 62208000 # 2 * 2592000 * 12 + + def test_single_unit_years(self): + """Test with years unit.""" + assert convert_to_seconds("1Y") == 31536000 # 365 * 86400 + assert convert_to_seconds("2Y") == 63072000 # 2 * 365 * 86400 + + def test_long_unit_names(self): + """Test with long unit names.""" + assert convert_to_seconds("1year") == 31536000 + assert convert_to_seconds("2years") == 63072000 + assert convert_to_seconds("1month") == 31104000 + assert convert_to_seconds("2months") == 62208000 + assert convert_to_seconds("1day") == 86400 + assert convert_to_seconds("2days") == 172800 + assert convert_to_seconds("1hour") == 3600 + assert convert_to_seconds("2hours") == 7200 + assert convert_to_seconds("1minute") == 60 + assert convert_to_seconds("2minutes") == 120 + assert convert_to_seconds("30min") == 1800 + assert convert_to_seconds("1second") == 1 + assert convert_to_seconds("2seconds") == 2 + assert convert_to_seconds("30sec") == 30 + + def test_multiple_units(self): + """Test with multiple units combined.""" + assert convert_to_seconds("1h30m") == 5400 # 3600 + 1800 + assert convert_to_seconds("1d2h") == 93600 # 86400 + 7200 + assert convert_to_seconds("1h30m45s") == 5445 # 3600 + 1800 + 45 + assert convert_to_seconds("2d3h4m5s") == 183845 # 172800 + 10800 + 240 + 5 + + def test_multiple_units_with_spaces(self): + """Test with multiple units and spaces.""" + assert convert_to_seconds("1h 30m") == 5400 + assert convert_to_seconds("1d 2h") == 93600 + assert convert_to_seconds("1h 30m 45s") == 5445 + assert convert_to_seconds("2d 3h 4m 5s") == 183845 + + def test_mixed_unit_formats(self): + """Test with mixed short and long unit names.""" + assert convert_to_seconds("1hour 30min") == 5400 + assert convert_to_seconds("1day 2hours") == 93600 + assert convert_to_seconds("1h 30minutes 45sec") == 5445 + + def test_negative_values(self): + """Test with negative time strings.""" + assert convert_to_seconds("-30s") == -30 + assert convert_to_seconds("-1h") == -3600 + assert convert_to_seconds("-1h30m") == -5400 + assert convert_to_seconds("-2d3h4m5s") == -183845 + + def test_case_insensitive_long_names(self): + """Test that long unit names are case insensitive.""" + assert convert_to_seconds("1Hour") == 3600 + assert convert_to_seconds("1MINUTE") == 60 + assert convert_to_seconds("1Day") == 86400 + assert convert_to_seconds("2YEARS") == 63072000 + + def test_duplicate_units_error(self): + """Test that duplicate units raise TimeParseError.""" + with pytest.raises(TimeParseError, match="Unit 'h' appears more than once"): + convert_to_seconds("1h2h") + + with pytest.raises(TimeParseError, match="Unit 's' appears more than once"): + convert_to_seconds("30s45s") + + with pytest.raises(TimeParseError, match="Unit 'm' appears more than once"): + convert_to_seconds("1m30m") + + def test_invalid_units_error(self): + """Test that invalid units raise TimeUnitError.""" + with pytest.raises(TimeUnitError, match="Unit 'x' is not a valid unit name"): + convert_to_seconds("30x") + + with pytest.raises(TimeUnitError, match="Unit 'invalid' is not a valid unit name"): + convert_to_seconds("1invalid") + + with pytest.raises(TimeUnitError, match="Unit 'z' is not a valid unit name"): + convert_to_seconds("1h30z") + + def test_empty_string(self): + """Test with empty string.""" + assert convert_to_seconds("") == 0 + + def test_no_matches(self): + """Test with string that has no time units.""" + assert convert_to_seconds("hello") == 0 + assert convert_to_seconds("no time here") == 0 + + def test_zero_values(self): + """Test with zero values for different units.""" + assert convert_to_seconds("0s") == 0 + assert convert_to_seconds("0m") == 0 + assert convert_to_seconds("0h") == 0 + assert convert_to_seconds("0d") == 0 + assert convert_to_seconds("0h0m0s") == 0 + + def test_large_values(self): + """Test with large time values.""" + assert convert_to_seconds("999d") == 86313600 # 999 * 86400 + assert convert_to_seconds("100Y") == 3153600000 # 100 * 31536000 + + def test_order_independence(self): + """Test that order of units doesn't matter.""" + assert convert_to_seconds("30m1h") == 5400 # same as 1h30m + assert convert_to_seconds("45s30m1h") == 5445 # same as 1h30m45s + assert convert_to_seconds("5s4m3h2d") == 183845 # same as 2d3h4m5s + + def test_whitespace_handling(self): + """Test various whitespace scenarios.""" + assert convert_to_seconds("1 h") == 3600 + assert convert_to_seconds("1h 30m") == 5400 + assert convert_to_seconds(" 1h30m ") == 5400 + assert convert_to_seconds("1h\t30m") == 5400 + + def test_mixed_case_short_units(self): + """Test that short units work with different cases.""" + # Note: The regex only matches [a-zA-Z]+ so case matters for the lookup + with pytest.raises(TimeUnitError, match="Unit 'H' is not a valid unit name"): + convert_to_seconds("1H") # 'H' is not in unit_factors, raises error + assert convert_to_seconds("1h") == 3600 # lowercase works + + def test_boundary_conditions(self): + """Test boundary conditions and edge cases.""" + # Test with leading zeros + assert convert_to_seconds("01h") == 3600 + assert convert_to_seconds("001m") == 60 + + # Test very small values + assert convert_to_seconds("1s") == 1 + + def test_negative_with_multiple_units(self): + """Test negative values with multiple units.""" + assert convert_to_seconds("-1h30m45s") == -5445 + assert convert_to_seconds("-2d3h") == -183600 + + def test_duplicate_with_long_names(self): + """Test duplicate detection with long unit names.""" + with pytest.raises(TimeParseError, match="Unit 'h' appears more than once"): + convert_to_seconds("1hour2h") # both resolve to 'h' + + with pytest.raises(TimeParseError, match="Unit 's' appears more than once"): + convert_to_seconds("1second30sec") # both resolve to 's'