Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions arrow/arrow.py
Original file line number Diff line number Diff line change
Expand Up @@ -1378,6 +1378,24 @@ 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.
#
# 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"(?<!\d)-\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
Expand Down
37 changes: 37 additions & 0 deletions tests/test_arrow.py
Original file line number Diff line number Diff line change
Expand Up @@ -2930,6 +2930,43 @@ 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_hyphen_between_digits_is_not_a_negative_number(self):
arw = arrow.Arrow(2000, 6, 18, 5, 55, 0)

# Still invalid input, but a separator is not a sign. Reporting these as
# negative numbers would send the caller looking for a sign that is not
# there, instead of at the string not being a humanized one.
for input_string in ["2020-01-01", "2020-01-01 00:00:00"]:
with pytest.raises(ValueError, match="Input string not valid"):
arw.dehumanize(input_string)

def test_leading_negative_number_is_still_rejected(self):
arw = arrow.Arrow(2000, 6, 18, 5, 55, 0)

# The lookbehind must not lose the cases the guard exists for: a sign at
# the start of the string, or after a space or an opening bracket.
for input_string in ["-1 hours ago", "in -1 hours", "(-1 hours ago)"]:
with pytest.raises(ValueError, match="negative number"):
arw.dehumanize(input_string)

def test_slavic_locales(self, slavic_locales: List[str]):
# Relevant units for Slavic locale plural logic
units = [
Expand Down
Loading