From 099114cd743b4460a46e73c5dcb84e3f02e84f82 Mon Sep 17 00:00:00 2001 From: Gaurav Kumar Biswas <2025sl93041@wilp.bits-pilani.ac.in> Date: Mon, 9 Nov 2026 22:43:28 +0530 Subject: [PATCH] fix: support fractional values in dehumanize() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `dehumanize()` used `re.compile(r"\d+")` to extract numbers from the input string. This pattern only matches whole integers, so a decimal like "1.5" was never matched as a unit — each digit ran independently, producing a wildly wrong result (e.g. "1.5 hours ago" shifted by -5 h instead of -1.5 h). Fix: change the number regex to `r"\d+(?:\.\d+)?"` everywhere it is used (the standalone `num_pattern` and the per-timeframe format call), and switch storage and parsing to `float` so that fractional values propagate correctly to `shift()`. Closes #1341 --- arrow/arrow.py | 12 ++++++------ tests/test_arrow.py | 7 +++++++ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index eecf23266..abb390756 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -1382,7 +1382,7 @@ def dehumanize(self, input_string: str, locale: str = "en_us") -> "Arrow": # Create an object containing the relative time info time_object_info = dict.fromkeys( - ["seconds", "minutes", "hours", "days", "weeks", "months", "years"], 0 + ["seconds", "minutes", "hours", "days", "weeks", "months", "years"], 0.0 ) # Create an object representing if unit has been seen @@ -1391,8 +1391,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 (integers and decimals like "1.5") + 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(): @@ -1408,7 +1408,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}") @@ -1425,10 +1425,10 @@ def dehumanize(self, input_string: str, locale: str = "en_us") -> "Arrow": # 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)) + 1.0 if not time_delta.isnumeric() else abs(float(time_delta)) ) else: - change_value = int(num_match.group()) + change_value = float(num_match.group()) # 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 b595e4e21..fa9faa953 100644 --- a/tests/test_arrow.py +++ b/tests/test_arrow.py @@ -2987,6 +2987,13 @@ def test_czech_slovak(self): assert arw.dehumanize(past_string, locale=lang) == past assert arw.dehumanize(future_string, locale=lang) == future + def test_fractional_units(self): + arw = arrow.Arrow(2026, 1, 1, tzinfo="UTC") + assert arw.dehumanize("1.5 hours ago") == arw.shift(hours=-1.5) + assert arw.dehumanize("0.5 days ago") == arw.shift(days=-0.5) + assert arw.dehumanize("in 1.5 hours") == arw.shift(hours=1.5) + assert arw.dehumanize("2.5 weeks ago") == arw.shift(weeks=-2.5) + class TestArrowIsBetween: def test_start_before_end(self):