Skip to content

Commit

Permalink
Merge pull request #722 from hwillard98/master
Browse files Browse the repository at this point in the history
Support for multiple granularities in humanize
  • Loading branch information
jadchaar committed Dec 9, 2019
2 parents de5b642 + a593b06 commit 5395fbb
Show file tree
Hide file tree
Showing 4 changed files with 195 additions and 8 deletions.
73 changes: 65 additions & 8 deletions arrow/arrow.py
Expand Up @@ -56,6 +56,12 @@ class Arrow(object):
_ATTRS = ["year", "month", "day", "hour", "minute", "second", "microsecond"]
_ATTRS_PLURAL = ["{}s".format(a) for a in _ATTRS]
_MONTHS_PER_QUARTER = 3
_SECS_PER_MINUTE = float(60)
_SECS_PER_HOUR = float(60 * 60)
_SECS_PER_DAY = float(60 * 60 * 24)
_SECS_PER_WEEK = float(60 * 60 * 24 * 7)
_SECS_PER_MONTH = float(60 * 60 * 24 * 30.5)
_SECS_PER_YEAR = float(60 * 60 * 24 * 365.25)

def __init__(
self, year, month, day, hour=0, minute=0, second=0, microsecond=0, tzinfo=None
Expand Down Expand Up @@ -870,7 +876,8 @@ def humanize(
Defaults to now in the current :class:`Arrow <arrow.arrow.Arrow>` object's timezone.
:param locale: (optional) a ``str`` specifying a locale. Defaults to 'en_us'.
: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'.
: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
Usage::
Expand Down Expand Up @@ -903,6 +910,9 @@ def humanize(
else:
raise TypeError()

if isinstance(granularity, list) and len(granularity) == 1:
granularity = granularity[0]

delta = int(round(util.total_seconds(self._datetime - dt)))
sign = -1 if delta < 0 else 1
diff = abs(delta)
Expand Down Expand Up @@ -963,23 +973,23 @@ def humanize(
years = sign * int(max(delta / 31536000, 2))
return locale.describe("years", years, only_distance=only_distance)

else:
elif util.isstr(granularity):
if granularity == "second":
delta = sign * delta
if abs(delta) < 2:
return locale.describe("now", only_distance=only_distance)
elif granularity == "minute":
delta = sign * delta / float(60)
delta = sign * delta / self._SECS_PER_MINUTE
elif granularity == "hour":
delta = sign * delta / float(60 * 60)
delta = sign * delta / self._SECS_PER_HOUR
elif granularity == "day":
delta = sign * delta / float(60 * 60 * 24)
delta = sign * delta / self._SECS_PER_DAY
elif granularity == "week":
delta = sign * delta / float(60 * 60 * 24 * 7)
delta = sign * delta / self._SECS_PER_WEEK
elif granularity == "month":
delta = sign * delta / float(60 * 60 * 24 * 30.5)
delta = sign * delta / self._SECS_PER_MONTH
elif granularity == "year":
delta = sign * delta / float(60 * 60 * 24 * 365.25)
delta = sign * delta / self._SECS_PER_YEAR
else:
raise AttributeError(
"Invalid level of granularity. Please select between 'second', 'minute', 'hour', 'day', 'week', 'month' or 'year'"
Expand All @@ -988,6 +998,53 @@ def humanize(
if trunc(abs(delta)) != 1:
granularity += "s"
return locale.describe(granularity, delta, only_distance=only_distance)

else:
timeframes = []
if "year" in granularity:
years = sign * delta / self._SECS_PER_YEAR
delta %= self._SECS_PER_YEAR
timeframes.append(["year", years])

if "month" in granularity:
months = sign * delta / self._SECS_PER_MONTH
delta %= self._SECS_PER_MONTH
timeframes.append(["month", months])

if "week" in granularity:
weeks = sign * delta / self._SECS_PER_WEEK
delta %= self._SECS_PER_WEEK
timeframes.append(["week", weeks])

if "day" in granularity:
days = sign * delta / self._SECS_PER_DAY
delta %= self._SECS_PER_DAY
timeframes.append(["day", days])

if "hour" in granularity:
hours = sign * delta / self._SECS_PER_HOUR
delta %= self._SECS_PER_HOUR
timeframes.append(["hour", hours])

if "minute" in granularity:
minutes = sign * delta / self._SECS_PER_MINUTE
delta %= self._SECS_PER_MINUTE
timeframes.append(["minute", minutes])

if "second" in granularity:
seconds = sign * delta
timeframes.append(["second", seconds])

if len(timeframes) < len(granularity):
raise AttributeError(
"Invalid level of granularity. Please select between 'second', 'minute', 'hour', 'day', 'week', 'month' or 'year'"
)

for index, (_, delta) in enumerate(timeframes):
if trunc(abs(delta)) != 1:
timeframes[index][0] += "s"
return locale.describe_multi(timeframes, only_distance=only_distance)

except KeyError as e:
raise ValueError(
"Humanization of the {} granularity is not currently translated in the '{}' locale. Please consider making a contribution to this locale.".format(
Expand Down
36 changes: 36 additions & 0 deletions arrow/locales.py
Expand Up @@ -51,6 +51,7 @@ class Locale(object):

past = None
future = None
and_word = None

month_names = []
month_abbreviations = []
Expand Down Expand Up @@ -78,6 +79,26 @@ def describe(self, timeframe, delta=0, only_distance=False):

return humanized

def describe_multi(self, timeframes, only_distance=False):
""" Describes a delta within multiple timeframes in plain language.
:param timeframes: a list of string, quantity pairs each representing a timeframe and delta.
:param only_distance: return only distance eg: "2 hours 11 seconds" without "in" or "ago" keywords
"""

humanized = ""
for index, (timeframe, delta) in enumerate(timeframes):
humanized += self._format_timeframe(timeframe, delta)
if index == len(timeframes) - 2 and self.and_word:
humanized += " " + self.and_word + " "
elif index < len(timeframes) - 1:
humanized += " "

if not only_distance:
humanized = self._format_relative(humanized, timeframe, delta)

return humanized

def day_name(self, day):
""" Returns the day name for a specified day of the week.
Expand Down Expand Up @@ -200,6 +221,7 @@ class EnglishLocale(Locale):

past = "{0} ago"
future = "in {0}"
and_word = "and"

timeframes = {
"now": "just now",
Expand Down Expand Up @@ -295,6 +317,7 @@ class ItalianLocale(Locale):
names = ["it", "it_it"]
past = "{0} fa"
future = "tra {0}"
and_word = "e"

timeframes = {
"now": "adesso",
Expand Down Expand Up @@ -364,6 +387,7 @@ class SpanishLocale(Locale):
names = ["es", "es_es"]
past = "hace {0}"
future = "en {0}"
and_word = "y"

timeframes = {
"now": "ahora",
Expand Down Expand Up @@ -437,6 +461,7 @@ class FrenchLocale(Locale):
names = ["fr", "fr_fr"]
past = "il y a {0}"
future = "dans {0}"
and_word = "et"

timeframes = {
"now": "maintenant",
Expand Down Expand Up @@ -514,6 +539,7 @@ class GreekLocale(Locale):

past = "{0} πριν"
future = "σε {0}"
and_word = "και"

timeframes = {
"now": "τώρα",
Expand Down Expand Up @@ -639,6 +665,7 @@ class SwedishLocale(Locale):

past = "för {0} sen"
future = "om {0}"
and_word = "och"

timeframes = {
"now": "just nu",
Expand Down Expand Up @@ -1527,6 +1554,7 @@ class DeutschBaseLocale(Locale):

past = "vor {0}"
future = "in {0}"
and_word = "und"

timeframes = {
"now": "gerade eben",
Expand Down Expand Up @@ -1778,6 +1806,7 @@ class PortugueseLocale(Locale):

past = "há {0}"
future = "em {0}"
and_word = "e"

timeframes = {
"now": "agora",
Expand Down Expand Up @@ -2495,6 +2524,7 @@ class DanishLocale(Locale):

past = "for {0} siden"
future = "efter {0}"
and_word = "og"

timeframes = {
"now": "lige nu",
Expand Down Expand Up @@ -2802,6 +2832,7 @@ class SlovakLocale(Locale):

past = "Pred {0}"
future = "O {0}"
and_word = "a"

month_names = [
"",
Expand Down Expand Up @@ -3101,6 +3132,7 @@ class CatalanLocale(Locale):
names = ["ca", "ca_es", "ca_ad", "ca_fr", "ca_it"]
past = "Fa {0}"
future = "En {0}"
and_word = "i"

timeframes = {
"now": "Ara mateix",
Expand Down Expand Up @@ -3685,6 +3717,7 @@ class RomanianLocale(Locale):

past = "{0} în urmă"
future = "peste {0}"
and_word = "și"

timeframes = {
"now": "acum",
Expand Down Expand Up @@ -3750,6 +3783,7 @@ class SlovenianLocale(Locale):

past = "pred {0}"
future = "čez {0}"
and_word = "in"

timeframes = {
"now": "zdaj",
Expand Down Expand Up @@ -3820,6 +3854,7 @@ class IndonesianLocale(Locale):

past = "{0} yang lalu"
future = "dalam {0}"
and_word = "dan"

timeframes = {
"now": "baru saja",
Expand Down Expand Up @@ -3957,6 +3992,7 @@ class EstonianLocale(Locale):

past = "{0} tagasi"
future = "{0} pärast"
and_word = "ja"

timeframes = {
"now": {"past": "just nüüd", "future": "just nüüd"},
Expand Down
27 changes: 27 additions & 0 deletions docs/index.rst
Expand Up @@ -208,6 +208,33 @@ Or another Arrow, or datetime:
>>> future.humanize(present)
'in 2 hours'
Indicate time as relative or include only the distance

.. code-block:: python
>>> present = arrow.utcnow()
>>> future = present.shift(hours=2)
>>> future.humanize(present)
'in 2 hours'
>>> future.humanize(present, only_distance=True)
'2 hours'
Indicate a specific time granularity (or multiple):

.. code-block:: python
>>> present = arrow.utcnow()
>>> future = present.shift(minutes=66)
>>> future.humanize(present, granularity="minute")
'in 66 minutes'
>>> future.humanize(present, granularity=["hour", "minute"])
'in an hour and 6 minutes'
>>> present.humanize(future, granularity=["hour", "minute"])
'an hour and 6 minutes ago'
>>> future.humanize(present, only_distance=True, granularity=["hour", "minute"])
'an hour and 6 minutes'
Support for a growing number of locales (see ``locales.py`` for supported languages):

.. code-block:: python
Expand Down
67 changes: 67 additions & 0 deletions tests/arrow_tests.py
Expand Up @@ -1499,6 +1499,12 @@ def test_granularity(self):
self.assertEqual(
later105.humanize(self.now, granularity="month"), "in 0 months"
)
self.assertEqual(
self.now.humanize(later105, granularity=["month"]), "0 months ago"
)
self.assertEqual(
later105.humanize(self.now, granularity=["month"]), "in 0 months"
)

later106 = self.now.shift(seconds=3 * 10 ** 6)
self.assertEqual(self.now.humanize(later106, granularity="day"), "34 days ago")
Expand Down Expand Up @@ -1543,9 +1549,70 @@ def test_granularity(self):
),
"3 years",
)

with self.assertRaises(AttributeError):
self.now.humanize(later108, granularity="years")

def test_multiple_granularity(self):
self.assertEqual(self.now.humanize(granularity="second"), "just now")
self.assertEqual(self.now.humanize(granularity=["second"]), "just now")
self.assertEqual(
self.now.humanize(granularity=["year", "month", "day", "hour", "second"]),
"in 0 years 0 months 0 days 0 hours and seconds",
)

later4000 = self.now.shift(seconds=4000)
self.assertEqual(
later4000.humanize(self.now, granularity=["hour", "minute"]),
"in an hour and 6 minutes",
)
self.assertEqual(
self.now.humanize(later4000, granularity=["hour", "minute"]),
"an hour and 6 minutes ago",
)
self.assertEqual(
later4000.humanize(
self.now, granularity=["hour", "minute"], only_distance=True
),
"an hour and 6 minutes",
)
self.assertEqual(
later4000.humanize(self.now, granularity=["day", "hour", "minute"]),
"in 0 days an hour and 6 minutes",
)
self.assertEqual(
self.now.humanize(later4000, granularity=["day", "hour", "minute"]),
"0 days an hour and 6 minutes ago",
)

later105 = self.now.shift(seconds=10 ** 5)
self.assertEqual(
self.now.humanize(later105, granularity=["hour", "day", "minute"]),
"a day 3 hours and 46 minutes ago",
)
with self.assertRaises(AttributeError):
self.now.humanize(later105, granularity=["error", "second"])

later108onlydistance = self.now.shift(seconds=10 ** 8)
self.assertEqual(
self.now.humanize(
later108onlydistance, only_distance=True, granularity=["year"]
),
"3 years",
)
self.assertEqual(
self.now.humanize(
later108onlydistance, only_distance=True, granularity=["month", "week"]
),
"37 months and 4 weeks",
)
self.assertEqual(
self.now.humanize(
later108onlydistance, only_distance=True, granularity=["year", "second"]
),
"3 years and seconds",
)

def test_seconds(self):

later = self.now.shift(seconds=10)
Expand Down

0 comments on commit 5395fbb

Please sign in to comment.