Skip to content

Support decimal values in dehumanize - #1334

Open
dchaudhari7177 wants to merge 2 commits into
arrow-py:masterfrom
dchaudhari7177:feat/dehumanize-fractional-values
Open

Support decimal values in dehumanize#1334
dchaudhari7177 wants to merge 2 commits into
arrow-py:masterfrom
dchaudhari7177:feat/dehumanize-fractional-values

Conversation

@dchaudhari7177

Copy link
Copy Markdown

Fixes #1237.

Problem

>>> arrow.get("2025-12-10T09:00:00").dehumanize("2 days 3.5 hours ago")
<Arrow [2025-12-08T04:00:00+00:00]>   # 2 days 5 hours

The number pattern is \d+, so for 3.5 hours the search lands on the digits after the separator and the 3 is dropped. Expected <Arrow [2025-12-08T05:30:00+00:00]>.

Fix

The pattern becomes \d+(?:[.,]\d+)?, shared as _NUMBER_PATTERN between the two places that needed it (the per-timeframe search string and the number extraction). A matched value keeps parsing as int when it has no fraction, so nothing changes for the strings humanize() produces — only the new decimal case yields a float.

relativedelta accepts floats for seconds through weeks, so shift() needed no change.

On the separator

Both . and , are accepted, as the issue suggests. The locale objects carry no decimal-separator information, and dehumanize input is written by hand rather than produced by humanize(), so there is nothing to key the choice off. The separator only counts when digits follow it, which is what keeps it from firing on a locale's own punctuation.

The trade-off is that digit grouping ("1,500 hours") is read as 1.5. That input is already wrong today — \d+ matches just 1 — so this is not a regression, but it is not a case I tried to support; I noted it in a comment next to the pattern. Happy to restrict to . only if you would rather not accept the ambiguity.

Fractional months and years

relativedelta rejects these outright ("Non-integer years and months are ambiguous and not currently supported"), which surfaces as a ValueError from dehumanize. That seemed better than silently truncating; there is a test pinning it.

Tests

  • test_fractional_value — hours, minutes and days, both directions.
  • test_fractional_value_comma_separator1,5 matches 1.5.
  • test_fractional_value_with_multiple_units — the exact case from the issue, plus its future form.
  • test_fractional_months_and_years_are_rejected.

Full suite passes (1903 passed, 1 skipped).

🤖 Generated with Claude Code

`dehumanize("2 days 3.5 hours ago")` shifted by 2 days and 5 hours: the
number pattern was `\d+`, so it matched the digits after the separator
and ignored the ones before it.

Match an optional decimal fraction and keep the value as a float when
one is present. Integers still parse as int, so nothing changes for the
strings humanize() produces.

Both `.` and `,` are accepted as the separator: dehumanize input is
written by hand, and the locale objects carry no separator information.
A separator only counts when digits follow it, so this cannot fire on
a locale's punctuation.

Fractional months and years still raise, from relativedelta, which
cannot represent them unambiguously.

Fixes arrow-py#1237
Copilot AI lite review requested due to automatic review settings August 8, 2026 09:30
@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 (b30820a).

Additional details and impacted files
@@            Coverage Diff            @@
##            master     #1334   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           10        10           
  Lines         2315      2317    +2     
  Branches       358       358           
=========================================
+ 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 fixes Arrow.dehumanize() so it correctly parses fractional (decimal) quantities like "3.5 hours" (and "3,5 hours") instead of incorrectly matching only the digits after the separator, addressing issue #1237.

Changes:

  • Introduces a shared _NUMBER_PATTERN that matches integers or decimals (\d+(?:[.,]\d+)?) and uses it both for timeframe matching and number extraction.
  • Parses matched numeric values as int when integral and float when fractional (normalizing , to .), relying on relativedelta float support for seconds–weeks.
  • Adds tests covering fractional units (including multiple units and comma separator) and pins the ValueError behavior for fractional months/years.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
arrow/arrow.py Updates dehumanize() regex matching and numeric parsing to support decimal values via a shared _NUMBER_PATTERN.
tests/test_arrow.py Adds targeted tests for decimal parsing, comma separators, multi-unit strings, and fractional months/years error behavior.

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

A decimal value makes change_value a float, but its type was inferred
from the int-only branch above and time_object_info was built with
dict.fromkeys(..., 0), so mypy rejected both assignments.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@dchaudhari7177

Copy link
Copy Markdown
Author

The red windows-latest (pypy-3.11) check here is infrastructure, not this change. The job failed before running any test:

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

A truncated PyPI download while building pyyaml from source on the pypy runner. Every other job in the matrix is green, and #1333 failed identically on the same runner in a separate run. A re-run should clear it — I do not have permission to trigger one here.

Re-ran the suite locally on this branch to be sure: 1903 passed, 1 skipped.

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.

Make dehumanize() handle values with decimal fractions correctly

2 participants