diff --git a/arrow/arrow.py b/arrow/arrow.py index f0a57f04..8c88d7cc 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -17,6 +17,7 @@ from typing import ( Any, ClassVar, + Dict, Final, Generator, Iterable, @@ -1383,8 +1384,8 @@ def dehumanize(self, input_string: str, locale: str = "en_us") -> "Arrow": current_time = self.fromdatetime(self._datetime) # Create an object containing the relative time info - time_object_info = dict.fromkeys( - ["seconds", "minutes", "hours", "days", "weeks", "months", "years"], 0 + time_object_info: Dict[str, float] = dict.fromkeys( + ["seconds", "minutes", "hours", "days", "weeks", "months", "years"], 0.0 ) # Create an object representing if unit has been seen @@ -1393,8 +1394,8 @@ def dehumanize(self, input_string: str, locale: str = "en_us") -> "Arrow": False, ) - # Create a regex pattern object for numbers - num_pattern = re.compile(r"\d+") + # Create a regex pattern object for numbers (supports decimals with . or ,) + num_pattern = re.compile(r"\d+(?:[.,]\d+)?") # Search input string for each time unit within locale for unit, unit_object in locale_obj.timeframes.items(): @@ -1410,7 +1411,7 @@ def dehumanize(self, input_string: str, locale: str = "en_us") -> "Arrow": for time_delta, time_string in strings_to_search.items(): # Replace {0} with regex \d representing digits search_string = str(time_string) - search_string = search_string.format(r"\d+") + search_string = search_string.format(r"\d+(?:[.,]\d+)?") # Create search pattern and find within string pattern = re.compile(rf"(^|\b|\d){search_string}") @@ -1426,11 +1427,13 @@ def dehumanize(self, input_string: str, locale: str = "en_us") -> "Arrow": # If no number matches # Need for absolute value as some locales have signs included in their objects if not num_match: - change_value = ( - 1 if not time_delta.isnumeric() else abs(int(time_delta)) + change_value: float = ( + 1.0 + if not time_delta.isnumeric() + else float(abs(int(time_delta))) ) else: - change_value = int(num_match.group()) + change_value = float(num_match.group().replace(",", ".")) # No time to update if now is the unit if unit == "now": diff --git a/tests/test_arrow.py b/tests/test_arrow.py index b595e4e2..04b52cdb 100644 --- a/tests/test_arrow.py +++ b/tests/test_arrow.py @@ -2988,6 +2988,32 @@ def test_czech_slovak(self): assert arw.dehumanize(future_string, locale=lang) == future +class TestArrowDehumanizeDecimals: + def test_decimal_hours(self): + arw = arrow.Arrow(2025, 12, 10, 9, 0, 0) + assert arw.dehumanize("3.5 hours ago") == arw.shift(hours=-3.5) + + def test_decimal_with_multiple_units(self): + arw = arrow.Arrow(2025, 12, 10, 9, 0, 0) + assert arw.dehumanize("2 days 3.5 hours ago") == arw.shift(days=-2, hours=-3.5) + + def test_decimal_comma_separator(self): + arw = arrow.Arrow(2025, 12, 10, 9, 0, 0) + assert arw.dehumanize("3,5 hours ago") == arw.shift(hours=-3.5) + + def test_decimal_future(self): + arw = arrow.Arrow(2025, 12, 10, 9, 0, 0) + assert arw.dehumanize("in 1.5 hours") == arw.shift(hours=1.5) + + def test_decimal_minutes(self): + arw = arrow.Arrow(2025, 12, 10, 9, 0, 0) + assert arw.dehumanize("2.5 minutes ago") == arw.shift(minutes=-2.5) + + def test_integer_values_still_work(self): + arw = arrow.Arrow(2025, 12, 10, 9, 0, 0) + assert arw.dehumanize("3 hours ago") == arw.shift(hours=-3) + + class TestArrowIsBetween: def test_start_before_end(self): target = arrow.Arrow.fromdatetime(datetime(2013, 5, 7))