Skip to content

Commit d890488

Browse files
committed
Improves diff_for_humans() method.
1 parent ce3edce commit d890488

File tree

10 files changed

+162
-135
lines changed

10 files changed

+162
-135
lines changed

CHANGELOG.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Change Log
22

3+
[Unreleased]
4+
5+
### Changed
6+
7+
- Improved `diff_for_humans()` method to display more intuitive strings on edge cases.
8+
39

410
## [1.0.1]
511

@@ -309,7 +315,7 @@ This version causes major breaking API changes to simplify it and making it more
309315
Initial release
310316

311317

312-
[Unreleased]: https://github.com/sdispater/pendulum/compare/1.0.1...master
318+
[Unreleased]: https://github.com/sdispater/pendulum/compare/1.0.1...develop
313319
[1.0.1]: https://github.com/sdispater/pendulum/releases/tag/1.0.1
314320
[1.0.0]: https://github.com/sdispater/pendulum/releases/tag/1.0.0
315321
[0.8.0]: https://github.com/sdispater/pendulum/releases/tag/0.8.0

pendulum/date.py

Lines changed: 16 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from dateutil.relativedelta import relativedelta
99

1010
from .period import Period
11+
from .formatting.difference_formatter import DifferenceFormatter
1112
from .mixins.default import TranslatableMixin, FormattableMixing, TestableMixin
1213
from .constants import (
1314
DAYS_PER_WEEK, YEARS_PER_DECADE, YEARS_PER_CENTURY,
@@ -44,6 +45,8 @@ class Date(TranslatableMixin, FormattableMixing, TestableMixin, date):
4445

4546
_MODIFIERS_VALID_UNITS = ['day', 'week', 'month', 'year', 'decade', 'century']
4647

48+
_diff_formatter = None
49+
4750
@classmethod
4851
def instance(cls, dt):
4952
"""
@@ -609,6 +612,18 @@ def __sub__(self, other):
609612

610613
# DIFFERENCES
611614

615+
@property
616+
def diff_formatter(self):
617+
"""
618+
Returns a DifferenceFormatter instance.
619+
620+
:rtype: DifferenceFormatter
621+
"""
622+
if not self.__class__._diff_formatter:
623+
self.__class__._diff_formatter = DifferenceFormatter(self.__class__.translator())
624+
625+
return self.__class__._diff_formatter
626+
612627
def diff(self, dt=None, abs=True):
613628
"""
614629
Returns the difference between two Date objects as a Period.
@@ -655,50 +670,7 @@ def diff_for_humans(self, other=None, absolute=False, locale=None):
655670
656671
:rtype: str
657672
"""
658-
is_now = other is None
659-
660-
if is_now:
661-
other = self.today()
662-
663-
diff = self.diff(other)
664-
665-
if diff.years > 0:
666-
unit = 'year'
667-
count = diff.years
668-
elif diff.months > 0:
669-
unit = 'month'
670-
count = diff.months
671-
elif diff.weeks > 0:
672-
unit = 'week'
673-
count = diff.weeks
674-
elif diff.days > 0:
675-
unit = 'day'
676-
count = diff.days
677-
else:
678-
unit = 'second'
679-
count = diff.seconds
680-
681-
if count == 0:
682-
count = 1
683-
684-
time = self.translator().transchoice(unit, count, {'count': count}, locale=locale)
685-
686-
if absolute:
687-
return time
688-
689-
is_future = diff.invert
690-
691-
if is_now:
692-
trans_id = 'from_now' if is_future else 'ago'
693-
else:
694-
trans_id = 'after' if is_future else 'before'
695-
696-
# Some langs have special pluralization for past and future tense
697-
try_key_exists = '%s_%s' % (unit, trans_id)
698-
if try_key_exists != self.translator().transchoice(try_key_exists, count, locale=locale):
699-
time = self.translator().transchoice(try_key_exists, count, {'count': count}, locale=locale)
700-
701-
return self.translator().trans(trans_id, {'time': time}, locale=locale)
673+
return self.diff_formatter.diff_for_humans(self, other, absolute, locale)
702674

703675
# MODIFIERS
704676

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# -*- coding: utf-8 -*-
2+
3+
from ..translator import Translator
4+
5+
6+
class DifferenceFormatter(object):
7+
"""
8+
Handles formatting differences in text.
9+
"""
10+
11+
THRESHOLDS = (
12+
('remaining_seconds', 'second', 59),
13+
('minutes', 'minute', 59),
14+
('hours', 'hour', 23),
15+
('remaining_days', 'day', 6),
16+
('weeks', 'week', 3),
17+
('months', 'month', 11),
18+
('years', 'year', None),
19+
)
20+
21+
def __init__(self, translator=Translator()):
22+
self._translator = translator
23+
24+
def diff_for_humans(self, date, other=None, absolute=False, locale=None):
25+
"""
26+
Get the difference in a human readable format.
27+
28+
:param date: The datetime to start with.
29+
:type date: pendulum.Date or pendulum.Pendulum
30+
31+
:param other: The datetime to compare against (defaults to now).
32+
:type other: pendulum.Date or pendulum.Pendulum or None
33+
34+
:param absolute: Removes time difference modifiers ago, after, etc
35+
:type absolute: bool
36+
37+
:param locale: The locale to use
38+
:type locale: str or None
39+
40+
:rtype: str
41+
"""
42+
is_now = other is None
43+
44+
if is_now:
45+
if hasattr(date, 'now'):
46+
other = date.now(date.timezone)
47+
else:
48+
other = date.today()
49+
50+
diff = date.diff(other)
51+
52+
count = diff.remaining_seconds
53+
unit = 'second'
54+
55+
if diff.years > 0:
56+
unit = 'year'
57+
count = diff.years
58+
59+
if diff.months > 6:
60+
count += 1
61+
elif diff.months == 11 and (diff.weeks * 7 + diff.remaining_days) > 15:
62+
unit = 'year'
63+
count = 1
64+
elif diff.months > 0:
65+
unit = 'month'
66+
count = diff.months
67+
68+
if (diff.weeks * 7 + diff.remaining_days) > 28:
69+
count += 1
70+
elif diff.weeks > 0:
71+
unit = 'week'
72+
count = diff.weeks
73+
74+
if diff.remaining_days > 3:
75+
count += 1
76+
elif diff.days > 0:
77+
unit = 'day'
78+
count = diff.days
79+
elif diff.hours > 0:
80+
unit = 'hour'
81+
count = diff.hours
82+
elif diff.minutes > 0:
83+
unit = 'minute'
84+
count = diff.minutes
85+
86+
if count == 0:
87+
count = 1
88+
89+
time = self._translator.transchoice(unit, count, {'count': count}, locale=locale)
90+
91+
if absolute:
92+
return time
93+
94+
is_future = diff.invert
95+
96+
if is_now:
97+
trans_id = 'from_now' if is_future else 'ago'
98+
else:
99+
trans_id = 'after' if is_future else 'before'
100+
101+
# Some langs have special pluralization for past and future tense
102+
try_key_exists = '%s_%s' % (unit, trans_id)
103+
if try_key_exists != self._translator.transchoice(try_key_exists, count, locale=locale):
104+
time = self._translator.transchoice(try_key_exists, count, {'count': count}, locale=locale)
105+
106+
return self._translator.trans(trans_id, {'time': time}, locale=locale)

pendulum/pendulum.py

Lines changed: 0 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1193,87 +1193,6 @@ def diff(self, dt=None, abs=True):
11931193

11941194
return Period(self, self._get_datetime(dt, pendulum=True), absolute=abs)
11951195

1196-
def diff_for_humans(self, other=None, absolute=False, locale=None):
1197-
"""
1198-
Get the difference in a human readable format in the current locale.
1199-
1200-
When comparing a value in the past to default now:
1201-
1 hour ago
1202-
5 months ago
1203-
1204-
When comparing a value in the future to default now:
1205-
1 hour from now
1206-
5 months from now
1207-
1208-
When comparing a value in the past to another value:
1209-
1 hour before
1210-
5 months before
1211-
1212-
When comparing a value in the future to another value:
1213-
1 hour after
1214-
5 months after
1215-
1216-
:type other: Pendulum
1217-
1218-
:param absolute: removes time difference modifiers ago, after, etc
1219-
:type absolute: bool
1220-
1221-
:param locale: The locale to use for localization
1222-
:type locale: str
1223-
1224-
:rtype: str
1225-
"""
1226-
is_now = other is None
1227-
1228-
if is_now:
1229-
other = self.now(self.timezone)
1230-
1231-
diff = self.diff(other)
1232-
1233-
if diff.years > 0:
1234-
unit = 'year'
1235-
count = diff.years
1236-
elif diff.months > 0:
1237-
unit = 'month'
1238-
count = diff.months
1239-
elif diff.weeks > 0:
1240-
unit = 'week'
1241-
count = diff.weeks
1242-
elif diff.days > 0:
1243-
unit = 'day'
1244-
count = diff.days
1245-
elif diff.hours > 0:
1246-
unit = 'hour'
1247-
count = diff.hours
1248-
elif diff.minutes > 0:
1249-
unit = 'minute'
1250-
count = diff.minutes
1251-
else:
1252-
unit = 'second'
1253-
count = diff.seconds
1254-
1255-
if count == 0:
1256-
count = 1
1257-
1258-
time = self.translator().transchoice(unit, count, {'count': count}, locale=locale)
1259-
1260-
if absolute:
1261-
return time
1262-
1263-
is_future = diff.invert
1264-
1265-
if is_now:
1266-
trans_id = 'from_now' if is_future else 'ago'
1267-
else:
1268-
trans_id = 'after' if is_future else 'before'
1269-
1270-
# Some langs have special pluralization for past and future tense
1271-
try_key_exists = '%s_%s' % (unit, trans_id)
1272-
if try_key_exists != self.translator().transchoice(try_key_exists, count, locale=locale):
1273-
time = self.translator().transchoice(try_key_exists, count, {'count': count}, locale=locale)
1274-
1275-
return self.translator().trans(trans_id, {'time': time}, locale=locale)
1276-
12771196
# Modifiers
12781197
def start_of(self, unit):
12791198
"""

pendulum/period.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ def days(self):
100100
return self._days
101101

102102
@property
103-
def days_exclude_weeks(self):
103+
def remaining_days(self):
104104
return abs(self._delta.days) % 7 * self._sign(self._days)
105105

106106
@property
@@ -173,7 +173,7 @@ def in_words(self, locale=None, separator=' '):
173173
('year', self.years),
174174
('month', self.months),
175175
('week', self.weeks),
176-
('day', self.days_exclude_weeks),
176+
('day', self.remaining_days),
177177
('hour', self.hours),
178178
('minute', self.minutes),
179179
('second', self.remaining_seconds)

pendulum/translator.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
class Translator(object):
88

9-
def __init__(self, locale):
9+
def __init__(self, locale='en'):
1010
self._locale = self._format_locale(locale)
1111
self._translations = TRANSLATIONS
1212

tests/date_tests/test_diff.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# -*- coding: utf-8 -*-
22

33
from datetime import date
4-
from pendulum import Date
4+
from pendulum import Pendulum, Date
55

66
from .. import AbstractTestCase
77

@@ -256,6 +256,19 @@ def test_diff_for_humans_absolute_years(self):
256256
self.assertEqual('1 year', Date.today().diff_for_humans(Date.today().subtract(years=1), True))
257257
self.assertEqual('1 year', Date.today().diff_for_humans(Date.today().add(years=1), True))
258258

259+
def test_diff_for_humans_accuracy(self):
260+
today = Pendulum.today()
261+
262+
with self.wrap_with_test_now(today.add(days=1)):
263+
self.assertEqual('1 year', today.add(years=1).diff_for_humans(absolute=True))
264+
265+
with self.wrap_with_test_now(today):
266+
self.assertEqual('6 days', today.add(days=6).diff_for_humans(absolute=True))
267+
self.assertEqual('1 week', today.add(days=7).diff_for_humans(absolute=True))
268+
self.assertEqual('3 weeks', today.add(days=20).diff_for_humans(absolute=True))
269+
self.assertEqual('2 weeks', today.add(days=14).diff_for_humans(absolute=True))
270+
self.assertEqual('2 weeks', today.add(days=13).diff_for_humans(absolute=True))
271+
259272
def test_subtraction(self):
260273
d = Date(2016, 7, 5)
261274
future_dt = date(2016, 7, 6)

tests/localization_tests/__init__.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# -*- coding: utf-8 -*-
22

3+
import pendulum
34
from pendulum import Pendulum
45

56

@@ -8,13 +9,13 @@ class AbstractLocalizationTestCase(object):
89
locale = None
910

1011
def setUp(self):
11-
Pendulum.set_locale(self.locale)
12+
pendulum.set_locale(self.locale)
1213

1314
def tearDown(self):
14-
Pendulum.set_locale('en')
15+
pendulum.set_locale('en')
1516

1617
def test_diff_for_humans_localized(self):
17-
with Pendulum.test(Pendulum(2016, 8, 29)):
18+
with pendulum.test(Pendulum(2016, 8, 29)):
1819
self.diff_for_humans()
1920

2021
def diff_for_humans(self):

0 commit comments

Comments
 (0)