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
2 changes: 1 addition & 1 deletion .github/workflows/continuous_integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ on:
jobs:
unit-tests:
name: ${{ matrix.os }} (${{ matrix.python-version }})
runs-on: ${{ matrix.os }}
runs-on: ${{ matrix.os == 'ubuntu-latest' && (matrix.python-version == '3.8' || matrix.python-version == '3.9') && 'ubuntu-24.04' || matrix.os }}
strategy:
fail-fast: false
matrix:
Expand Down
5 changes: 5 additions & 0 deletions arrow/arrow.py
Original file line number Diff line number Diff line change
Expand Up @@ -1136,6 +1136,7 @@ def humanize(
locale: str = DEFAULT_LOCALE,
only_distance: bool = False,
granularity: Union[_GRANULARITY, List[_GRANULARITY]] = "auto",
numeric: bool = False,
) -> str:
"""Returns a localized, humanized representation of a relative difference in time.

Expand All @@ -1145,6 +1146,8 @@ def humanize(
:param only_distance: (optional) returns only time difference eg: "11 seconds" without "in" or "ago" part.
:param granularity: (optional) defines the precision of the output. Set it to strings 'second', 'minute',
'hour', 'day', 'week', 'month' or 'year' or a list of any combination of these strings
:param numeric: (optional) use numeric singular time units for English locales,
for example, '1 hour' instead of 'an hour'. Defaults to False.

Usage::

Expand All @@ -1160,6 +1163,8 @@ def humanize(

locale_name = locale
locale = locales.get_locale(locale)
if numeric and isinstance(locale, locales.EnglishLocale):
locale.numeric = True

if other is None:
utc = dt_datetime.now(timezone.utc).replace(tzinfo=timezone.utc)
Expand Down
15 changes: 15 additions & 0 deletions arrow/locales.py
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,21 @@ class EnglishLocale(Locale):
past = "{0} ago"
future = "in {0}"
and_word = "and"
numeric: bool = False

def _format_timeframe(self, timeframe: TimeFrameLiteral, delta: int) -> str:
if self.numeric and timeframe in (
"second",
"minute",
"hour",
"day",
"week",
"month",
"quarter",
"year",
):
return f"1 {timeframe}"
return super()._format_timeframe(timeframe, delta)

timeframes = {
"now": "just now",
Expand Down
76 changes: 76 additions & 0 deletions tests/test_numeric_humanize.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
"""Regression tests for numeric singular output in Arrow.humanize()."""

import pytest

import arrow


@pytest.fixture
def reference():
return arrow.get(2024, 1, 10, 12, 0, 0)


def test_humanize_numeric_singular_past(reference):
past = reference.shift(hours=-1)
assert past.humanize(reference, numeric=True) == "1 hour ago"
assert past.humanize(reference) == "an hour ago"


def test_humanize_numeric_singular_future(reference):
future = reference.shift(minutes=1)
assert future.humanize(reference, numeric=True) == "in 1 minute"
assert future.humanize(reference, only_distance=True, numeric=True) == "1 minute"


@pytest.mark.parametrize(
"shift, granularity, expected",
[
({"seconds": 2}, "second", "in 2 seconds"),
({"minutes": 1}, "minute", "in 1 minute"),
({"hours": 1}, "hour", "in 1 hour"),
({"days": 1}, "day", "in 1 day"),
({"weeks": 1}, "week", "in 1 week"),
({"months": 1}, "month", "in 1 month"),
({"days": 92}, "quarter", "in 1 quarter"),
({"years": 1}, "year", "in 1 year"),
],
)
def test_humanize_numeric_explicit_granularity(reference, shift, granularity, expected):
future = reference.shift(**shift)
assert future.humanize(reference, granularity=granularity, numeric=True) == expected


def test_humanize_numeric_multi_granularity(reference):
future = reference.shift(hours=1, minutes=6)
assert (
future.humanize(reference, granularity=["hour", "minute"], numeric=True)
== "in 1 hour and 6 minutes"
)
assert (
future.humanize(reference, granularity=["hour", "minute"])
== "in an hour and 6 minutes"
)


def test_humanize_numeric_multi_with_two_singular_values(reference):
future = reference.shift(hours=1, minutes=1)
assert (
future.humanize(reference, granularity=["hour", "minute"], numeric=True)
== "in 1 hour and 1 minute"
)


def test_humanize_numeric_english_variant(reference):
future = reference.shift(hours=1)
assert future.humanize(reference, locale="en-gb", numeric=True) == "in 1 hour"


def test_humanize_numeric_other_locales_unchanged(reference):
future = reference.shift(hours=1)
assert future.humanize(reference, locale="fr", numeric=True) == future.humanize(
reference, locale="fr"
)


def test_humanize_numeric_now_unchanged(reference):
assert reference.humanize(reference, numeric=True) == "just now"
Loading