diff --git a/arrow/arrow.py b/arrow/arrow.py index eecf23266..b3632bd3a 100644 --- a/arrow/arrow.py +++ b/arrow/arrow.py @@ -1430,6 +1430,26 @@ def dehumanize(self, input_string: str, locale: str = "en_us") -> "Arrow": else: change_value = int(num_match.group()) + # Reject explicitly-negative values such as "in -1 hours". + # The digit-only num_pattern above never captures a sign, + # so the minus was previously dropped silently and + # "in -1 hours" produced the same (wrong) result as + # "in 1 hours". ``humanize`` always expresses direction + # through the past/future templates ("... ago" / "in ...") + # with a positive magnitude, so a negative number here is + # malformed input rather than a second, conflicting + # direction signal. Detect the sign by inspecting the + # character that immediately precedes the matched digits in + # the original input string. + num_start = match.start() + num_match.start() + if num_start > 0 and input_string[num_start - 1] == "-": + raise ValueError( + f"Invalid input string. Negative time value " + f"'-{num_match.group()}' is not supported; express " + "direction using the past ('... ago') or future " + "('in ...') form with a positive value instead." + ) + # No time to update if now is the unit if unit == "now": unit_visited[unit] = True diff --git a/tests/test_arrow.py b/tests/test_arrow.py index b595e4e21..bb774eb3e 100644 --- a/tests/test_arrow.py +++ b/tests/test_arrow.py @@ -2888,6 +2888,29 @@ def test_require_relative_unit(self, locale_list_no_weeks: List[str]): with pytest.raises(ValueError): arw.dehumanize(second_future_string, locale=lang) + def test_negative_values(self): + # Negative numeric values are malformed input: humanize only ever + # emits positive magnitudes and expresses direction through the + # past/future templates. They were previously silently dropped, so + # "in -1 hours" returned the same (wrong) result as "in 1 hours". + arw = arrow.Arrow(2000, 6, 18, 5, 55, 0) + + # Sanity check: the corresponding positive strings still work and the + # buggy result must not equal the positive result. + assert arw.dehumanize("in 1 hours") == arw.shift(hours=1) + assert arw.dehumanize("1 hours ago") == arw.shift(hours=-1) + + negative_strings = [ + "in -1 hours", + "in -5 days", + "-3 hours ago", + "in -2 years", + "-10 seconds ago", + ] + for negative_string in negative_strings: + with pytest.raises(ValueError): + arw.dehumanize(negative_string) + # Test for scrambled input def test_scrambled_input(self, locale_list_no_weeks: List[str]): for lang in locale_list_no_weeks: