From baa58ee545f6be8f8cd377f52f9e73e376e7dbac Mon Sep 17 00:00:00 2001 From: eeshsaxena Date: Sat, 4 Jul 2026 21:27:41 +0530 Subject: [PATCH] Reject negative numbers in dehumanize() dehumanize() extracts numbers with an unsigned `\d+` pattern, and the surrounding match anchors on a word boundary, so a minus sign attached to a number (e.g. "in -1 hours") is silently discarded. The result was a datetime in the opposite direction from what the string expressed: "in -1 hours" produced the same value as "in 1 hours". Humanized strings encode direction with words ("ago"/"in"), never with an arithmetic sign, so humanize() never emits one. Reject any input containing a negative number with a ValueError instead of returning a quietly wrong result. Fixes #1278 --- arrow/arrow.py | 11 +++++++++++ tests/test_arrow.py | 9 +++++++++ 2 files changed, 20 insertions(+) diff --git a/arrow/arrow.py b/arrow/arrow.py index eecf23266..953ec46f3 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -1380,6 +1380,17 @@ def dehumanize(self, input_string: str, locale: str = "en_us") -> "Arrow": current_time = self.fromdatetime(self._datetime) + # Humanized strings express direction with words ("ago"/"in"), never + # with an arithmetic sign, so ``humanize`` itself never emits one. A + # minus sign attached to a number (e.g. "in -1 hours") would otherwise + # be silently dropped by the unsigned ``\d+`` extraction below and yield + # a result in the opposite direction from what was written, so reject it. + if re.search(r"-\d", input_string): + raise ValueError( + f"Invalid input string: {input_string!r}. Negative numbers are " + "not supported; use 'ago' or 'in' to indicate direction." + ) + # Create an object containing the relative time info time_object_info = dict.fromkeys( ["seconds", "minutes", "hours", "days", "weeks", "months", "years"], 0 diff --git a/tests/test_arrow.py b/tests/test_arrow.py index b595e4e21..d250f2994 100644 --- a/tests/test_arrow.py +++ b/tests/test_arrow.py @@ -2868,6 +2868,15 @@ def test_normalized_locale(self): assert arw.dehumanize(second_ago_string, locale="zh_hk") == second_ago assert arw.dehumanize(second_future_string, locale="zh_hk") == second_future + # Ensures a negative number is rejected rather than silently dropping the + # sign and producing a result in the opposite direction (see gh-1278). + 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"): + with pytest.raises(ValueError): + arw.dehumanize(input_string) + # Ensures relative units are required in string def test_require_relative_unit(self, locale_list_no_weeks: List[str]): for lang in locale_list_no_weeks: