From f7b0444fce7e47fdba021233049c624645353325 Mon Sep 17 00:00:00 2001 From: Friday Date: Wed, 18 Feb 2026 06:16:05 +0000 Subject: [PATCH] Fix dehumanize not recognizing singular nouns like '1 day ago' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The English locale's plural timeframes (seconds, minutes, hours, etc.) only had a single format string (e.g. "{0} days"), so dehumanize could match "2 days ago" but not "1 day ago". The singular entries (second, minute, etc.) mapped to "a second", "a minute", etc. — matching "a day ago" but not "1 day ago". Changed the plural timeframe entries to Mapping dicts with both "singular" ("{0} day") and "plural" ("{0} days") forms, and overrode _format_timeframe to select the correct form based on delta. This allows dehumanize to match both "1 day ago" and "2 days ago", while keeping humanize output unchanged ("a day ago" for delta=1, "2 days ago" for delta=2). Fixes #1150. Co-Authored-By: Claude Opus 4.6 --- arrow/locales.py | 22 ++++++++++++++-------- tests/test_arrow.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 8 deletions(-) diff --git a/arrow/locales.py b/arrow/locales.py index 757df480..235d1ee5 100644 --- a/arrow/locales.py +++ b/arrow/locales.py @@ -307,25 +307,31 @@ class EnglishLocale(Locale): timeframes = { "now": "just now", "second": "a second", - "seconds": "{0} seconds", + "seconds": {"singular": "{0} second", "plural": "{0} seconds"}, "minute": "a minute", - "minutes": "{0} minutes", + "minutes": {"singular": "{0} minute", "plural": "{0} minutes"}, "hour": "an hour", - "hours": "{0} hours", + "hours": {"singular": "{0} hour", "plural": "{0} hours"}, "day": "a day", - "days": "{0} days", + "days": {"singular": "{0} day", "plural": "{0} days"}, "week": "a week", - "weeks": "{0} weeks", + "weeks": {"singular": "{0} week", "plural": "{0} weeks"}, "month": "a month", - "months": "{0} months", + "months": {"singular": "{0} month", "plural": "{0} months"}, "quarter": "a quarter", - "quarters": "{0} quarters", + "quarters": {"singular": "{0} quarter", "plural": "{0} quarters"}, "year": "a year", - "years": "{0} years", + "years": {"singular": "{0} year", "plural": "{0} years"}, } meridians = {"am": "am", "pm": "pm", "AM": "AM", "PM": "PM"} + def _format_timeframe(self, timeframe: TimeFrameLiteral, delta: int) -> str: + form = self.timeframes[timeframe] + if isinstance(form, Mapping): + form = form["singular"] if abs(delta) == 1 else form["plural"] + return form.format(trunc(abs(delta))) + month_names = [ "", "January", diff --git a/tests/test_arrow.py b/tests/test_arrow.py index b595e4e2..a1ec26fa 100644 --- a/tests/test_arrow.py +++ b/tests/test_arrow.py @@ -2987,6 +2987,34 @@ def test_czech_slovak(self): assert arw.dehumanize(past_string, locale=lang) == past assert arw.dehumanize(future_string, locale=lang) == future + def test_singular_nouns_english(self): + """Test that dehumanize recognizes '1 day ago', '1 hour ago', etc. + + Regression test for https://github.com/arrow-py/arrow/issues/1150 + """ + arw = arrow.Arrow(2000, 6, 18, 5, 55, 0) + + # All singular units should work with numeric "1" + assert arw.dehumanize("1 second ago") == arw.shift(seconds=-1) + assert arw.dehumanize("1 minute ago") == arw.shift(minutes=-1) + assert arw.dehumanize("1 hour ago") == arw.shift(hours=-1) + assert arw.dehumanize("1 day ago") == arw.shift(days=-1) + assert arw.dehumanize("1 week ago") == arw.shift(weeks=-1) + assert arw.dehumanize("1 month ago") == arw.shift(months=-1) + assert arw.dehumanize("1 year ago") == arw.shift(years=-1) + + # Future tense + assert arw.dehumanize("in 1 day") == arw.shift(days=1) + assert arw.dehumanize("in 1 hour") == arw.shift(hours=1) + + # "a/an" forms should still work + assert arw.dehumanize("a day ago") == arw.shift(days=-1) + assert arw.dehumanize("an hour ago") == arw.shift(hours=-1) + + # Plural forms should still work + assert arw.dehumanize("2 days ago") == arw.shift(days=-2) + assert arw.dehumanize("3 hours ago") == arw.shift(hours=-3) + class TestArrowIsBetween: def test_start_before_end(self):