Skip to content

Fix dehumanize() silently returning wrong times for fractional inputs - #1406

Open
2025sl93041-gkb wants to merge 1 commit into
arrow-py:masterfrom
2025sl93041-gkb:fix/dehumanize-fractional-units
Open

2025sl93041-gkb wants to merge 1 commit into
arrow-py:masterfrom
2025sl93041-gkb:fix/dehumanize-fractional-units

Conversation

@2025sl93041-gkb

Copy link
Copy Markdown

Problem

dehumanize() gives wrong results for any fractional input. For example:

arw = arrow.Arrow(2026, 1, 1, tzinfo="UTC")
arw.dehumanize("1.5 hours ago")   # returns 2025-12-31T19:00:00+00:00  (−5 h, wrong)
arw.dehumanize("0.5 days ago")    # returns 2025-12-27T00:00:00+00:00  (−5 days, wrong)

Expected: −1.5 h and −0.5 days respectively. No exception is raised — the result is just quietly wrong.

Reported in #1341.

Root Cause

The number-matching regex r"\d+" only matches whole digit sequences. For an input like "1.5 hours ago":

  • re.compile(r"\d+").search("1.5 hours ago") returns "1" (stops at the decimal point)
  • The .5 is then matched on its own as 5 in the timeframe search

Both matches write to the same time_object_info["hours"] key, but the second match overwrites the first with 5, giving a shift of -5 hours.

Fix

Three changes to dehumanize() in arrow/arrow.py:

  1. Change num_pattern from r"\d+" to r"\d+(?:\.\d+)?" so the decimal part is captured as one token.
  2. Change the per-timeframe search_string.format(r"\d+") to use the same decimal-aware pattern so locale timeframe patterns also match fractional values.
  3. Switch time_object_info initial values and all int() casts to float so the fractional amount is preserved through to shift().

Testing

Added test_fractional_units to tests/test_arrow.py covering hours, days, and weeks in both past and future directions. Full suite: 1903 passed, 1 warning.

def test_fractional_units(self):
    arw = arrow.Arrow(2026, 1, 1, tzinfo="UTC")
    assert arw.dehumanize("1.5 hours ago") == arw.shift(hours=-1.5)
    assert arw.dehumanize("0.5 days ago") == arw.shift(days=-0.5)
    assert arw.dehumanize("in 1.5 hours") == arw.shift(hours=1.5)
    assert arw.dehumanize("2.5 weeks ago") == arw.shift(weeks=-2.5)

`dehumanize()` used `re.compile(r"\d+")` to extract numbers from the
input string. This pattern only matches whole integers, so a decimal
like "1.5" was never matched as a unit — each digit ran independently,
producing a wildly wrong result (e.g. "1.5 hours ago" shifted by -5 h
instead of -1.5 h).

Fix: change the number regex to `r"\d+(?:\.\d+)?"` everywhere it is
used (the standalone `num_pattern` and the per-timeframe format call),
and switch storage and parsing to `float` so that fractional values
propagate correctly to `shift()`.

Closes arrow-py#1341
@codecov

codecov Bot commented Nov 9, 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 (099114c).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff            @@
##            master     #1406   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           10        10           
  Lines         2315      2315           
  Branches       358       358           
=========================================
  Hits          2315      2315           

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

This branch has not been deployed

No deployments
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.

1 participant