From bf6e229eee6e01ecf46439f1a020443bcf5fb664 Mon Sep 17 00:00:00 2001 From: dchaudhari7177 <111210939+dchaudhari7177@users.noreply.github.com> Date: Sat, 8 Aug 2026 14:57:02 +0530 Subject: [PATCH 1/2] Reject negative numbers in dehumanize instead of dropping the sign `dehumanize("in -1 hours")` returned the same time as `dehumanize("in 1 hours")`. The number pattern is `\d+`, so the minus sign was never captured and the result silently pointed the opposite way. A humanized string carries its direction in the "ago"/"in" wording, so a sign on the number is never meaningful; raise ValueError rather than guess. None of the 133 dehumanize locales has a hyphen anywhere in its past, future or timeframe strings, so a hyphen in front of a digit can only have come from the caller. Fixes #1278 --- arrow/arrow.py | 13 +++++++++++++ tests/test_arrow.py | 18 ++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/arrow/arrow.py b/arrow/arrow.py index eecf23266..d2188a002 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -1378,6 +1378,19 @@ def dehumanize(self, input_string: str, locale: str = "en_us") -> "Arrow": f"Dehumanize does not currently support the {locale} locale, please consider making a contribution to add support for this locale." ) + # A humanized string carries its direction in the "ago"/"in" wording, so + # a sign on the number is never meaningful. The number pattern below + # matches unsigned digits only, which would silently drop the sign and + # return a time shifted the opposite way. No locale's timeframe strings + # contain a hyphen, so one in front of a digit can only have come from + # the caller. + if re.search(r"-\d", input_string): + raise ValueError( + "Invalid input String. String contains a negative number. " + "Humanized strings express direction with words, not signs. " + "Ex: '1 hour ago' rather than 'in -1 hours'." + ) + current_time = self.fromdatetime(self._datetime) # Create an object containing the relative time info diff --git a/tests/test_arrow.py b/tests/test_arrow.py index b595e4e21..d78b531ea 100644 --- a/tests/test_arrow.py +++ b/tests/test_arrow.py @@ -2930,6 +2930,24 @@ def test_no_units_modified(self, locale_list_no_weeks: List[str]): with pytest.raises(ValueError): arw.dehumanize(empty_future_string, locale=lang) + def test_negative_number(self): + arw = arrow.Arrow(2000, 6, 18, 5, 55, 0) + + for input_string in [ + "in -1 hours", + "in -2 days", + "-3 minutes ago", + "in -1 years", + ]: + with pytest.raises(ValueError, match="negative number"): + arw.dehumanize(input_string) + + def test_negative_number_not_confused_with_positive(self): + arw = arrow.Arrow(2000, 6, 18, 5, 55, 0) + + assert arw.dehumanize("in 2 hours") == arw.shift(hours=2) + assert arw.dehumanize("2 hours ago") == arw.shift(hours=-2) + def test_slavic_locales(self, slavic_locales: List[str]): # Relevant units for Slavic locale plural logic units = [ From 45c758984eaefbcabeadd1bd5b4f107472075cbf Mon Sep 17 00:00:00 2001 From: dchaudhari7177 <111210939+dchaudhari7177@users.noreply.github.com> Date: Sat, 8 Aug 2026 22:13:54 +0530 Subject: [PATCH 2/2] Do not report a hyphen between digits as a negative number MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The -\d guard also matched an ISO date: dehumanize("2020-01-01") raised "String contains a negative number", which sends the caller looking for a sign that is not there. A hyphen between digits is a separator. A negative lookbehind leaves those to the existing "Input string not valid" error, and the sign positions the guard exists for — start of string, after a space, after a bracket — still raise. --- arrow/arrow.py | 7 ++++++- tests/test_arrow.py | 19 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/arrow/arrow.py b/arrow/arrow.py index d2188a002..99d006f11 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -1384,7 +1384,12 @@ def dehumanize(self, input_string: str, locale: str = "en_us") -> "Arrow": # return a time shifted the opposite way. No locale's timeframe strings # contain a hyphen, so one in front of a digit can only have come from # the caller. - if re.search(r"-\d", input_string): + # + # A hyphen *between* digits is a separator, not a sign: "2020-01-01" is + # invalid input, but it is not a negative number, and saying so would send + # the caller looking for a sign that is not there. The lookbehind leaves + # those to the ordinary "not a valid humanized string" error below. + if re.search(r"(?