Skip to content

Reject negative numbers in dehumanize instead of dropping the sign - #1333

Open
dchaudhari7177 wants to merge 2 commits into
arrow-py:masterfrom
dchaudhari7177:fix/dehumanize-reject-negative
Open

Reject negative numbers in dehumanize instead of dropping the sign#1333
dchaudhari7177 wants to merge 2 commits into
arrow-py:masterfrom
dchaudhari7177:fix/dehumanize-reject-negative

Conversation

@dchaudhari7177

Copy link
Copy Markdown

Fixes #1278.

Problem

>>> now = arrow.now()
>>> (now.dehumanize("in 1 hours")  - now).total_seconds()
3600.0
>>> (now.dehumanize("in -1 hours") - now).total_seconds()
3600.0

The number pattern in dehumanize is re.compile(r"\d+"), so the minus sign is never part of the match. The sign is dropped without a word and the caller gets a time shifted the opposite way — "in -2 days" becomes +2 days, "-3 minutes ago" becomes -3 minutes. Wrong answers of this shape are hard to trace back to their cause.

Fix

Raise ValueError when the input contains a hyphen directly in front of a digit. A humanized string expresses direction with words (ago / in ...), so a sign on the number never carries meaning — there is no correct value to fall back to, and guessing one is what caused the bug.

The check is narrow enough to be safe: I enumerated all 133 locales in DEHUMANIZE_LOCALES and none has a hyphen anywhere in its past, future, or timeframes strings, so -\d in the input can only have come from the caller. It also cannot fire on a hyphen used as a word separator, since it requires a digit immediately after.

The error message names the problem and shows the intended form.

Tests

  • test_negative_number — four negative inputs across units and both directions, each raising ValueError matching "negative number".
  • test_negative_number_not_confused_with_positive — the unsigned equivalents still resolve to the right shift in both directions.

Full suite passes (1901 passed, 1 skipped).

🤖 Generated with Claude Code

`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 arrow-py#1278
Copilot AI lite review requested due to automatic review settings August 8, 2026 09:27
@codecov

codecov Bot commented Aug 8, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (2224255) to head (45c7589).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff            @@
##            master     #1333   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           10        10           
  Lines         2315      2317    +2     
  Branches       358       359    +1     
=========================================
+ Hits          2315      2317    +2     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates Arrow.dehumanize() to explicitly reject negative numeric tokens (e.g., "in -1 hours") instead of silently ignoring the - sign and producing an incorrect time shift, and adds regression tests to cover the new behavior.

Changes:

  • Add an early validation in Arrow.dehumanize() that raises ValueError when the input contains a hyphen directly before a digit.
  • Add tests ensuring negative-number inputs raise, while equivalent positive inputs still parse correctly.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
arrow/arrow.py Adds early input validation to reject negative-number patterns in dehumanize() to avoid silently incorrect results.
tests/test_arrow.py Adds regression tests for negative-number rejection and ensures positive equivalents still work.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread arrow/arrow.py Outdated
Comment on lines +1387 to +1388
if re.search(r"-\d", input_string):
raise ValueError(
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.
@dchaudhari7177

Copy link
Copy Markdown
Author

Good catch, and fixed.

The guard was re.search(r"-\d", input_string), which matches the hyphen in 2020-01-01. So dehumanize("2020-01-01") raised "String contains a negative number" — still an error, but the wrong one: it sends the caller looking for a sign that is not there instead of at the string not being a humanized one.

A hyphen between digits is a separator, so the guard now uses a negative lookbehind, (?<!\d)-\d. ISO-shaped input falls through to the existing Input string not valid error, and the positions the guard exists for still raise: start of string, after a space, after a bracket.

Two tests added — one pinning that the ISO dates get the ordinary error, one pinning that -1 hours ago, in -1 hours and (-1 hours ago) still raise, so the lookbehind cannot quietly widen later.

Full suite: 1903 passed, 1 skipped.

Separately, on the red windows-latest (pypy-3.11) check — it is not this change. The job never got to the tests:

ERROR: Could not install packages due to an OSError: ('Connection broken: IncompleteRead(190689 bytes read, 245 more expected)')
ERROR: Failed to build 'pyyaml' when installing build dependencies for pyyaml

A truncated PyPI download while building pyyaml from source. #1334 failed the same way on the same runner. The push above should re-run it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

dehumanize() silently ignores negative time values, returning wrong results

2 participants