Skip to content

Commit b3f5342

Browse files
committed
Improves diff() performance
1 parent 0775cdc commit b3f5342

File tree

5 files changed

+198
-29
lines changed

5 files changed

+198
-29
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
### Changed
1010

11+
- Greatly improved `diff()` performance.
1112
- Improved `diff_for_humans()` method to display more intuitive strings on edge cases.
1213
- Formatting (with f-strings or `format()`) will now use the configured formatter.
1314

pendulum/helpers.py

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

3+
import pendulum
4+
35
from math import copysign
46
from datetime import timedelta
57

@@ -16,7 +18,7 @@ def is_leap(year):
1618

1719

1820
def add_duration(dt, years=0, months=0, weeks=0, days=0,
19-
hours=0, minutes=0, seconds=0, microseconds=0):
21+
hours=0, minutes=0, seconds=0, microseconds=0):
2022
"""
2123
Adds a duration to a datetime instance.
2224
@@ -107,5 +109,106 @@ def add_duration(dt, years=0, months=0, weeks=0, days=0,
107109
)
108110

109111

112+
def precise_diff(d1, d2):
113+
"""
114+
Calculate a precise difference between two datetimes.
115+
116+
:param d1: The first datetime
117+
:type d1: pendulum.Pendulum or pendulum.Date
118+
119+
:param d2: The second datetime
120+
:type d2: pendulum.Pendulum or pendulum.Date
121+
122+
:rtype: dict
123+
"""
124+
diff = {
125+
'years': 0,
126+
'months': 0,
127+
'days': 0,
128+
'hours': 0,
129+
'minutes': 0,
130+
'seconds': 0,
131+
'microseconds': 0
132+
}
133+
sign = 1
134+
135+
if d1 == d2:
136+
return diff
137+
138+
if d1 > d2:
139+
d1, d2 = d2, d1
140+
sign = -1
141+
142+
y_diff = d2.year - d1.year
143+
m_diff = d2.month - d1.month
144+
d_diff = d2.day - d1.day
145+
hour_diff = 0
146+
min_diff = 0
147+
sec_diff = 0
148+
mic_diff = 0
149+
150+
if hasattr(d2, 'hour'):
151+
hour_diff = d2.hour - d1.hour
152+
min_diff = d2.minute - d1.minute
153+
sec_diff = d2.second - d1.second
154+
mic_diff = d2.microsecond - d1.microsecond
155+
156+
if mic_diff < 0:
157+
mic_diff += 1000000
158+
sec_diff -= 1
159+
160+
if sec_diff < 0:
161+
sec_diff += 60
162+
min_diff -= 1
163+
164+
if min_diff < 0:
165+
min_diff += 60
166+
hour_diff -= 1
167+
168+
if hour_diff < 0:
169+
hour_diff += 24
170+
d_diff -= 1
171+
172+
if d_diff < 0:
173+
year = d2.year
174+
month = d2.month
175+
176+
if month == 1:
177+
month = 12
178+
year -= 1
179+
else:
180+
month -= 1
181+
182+
leap = int(is_leap(year))
183+
184+
days_in_last_month = DAYS_PER_MONTHS[leap][month]
185+
days_in_month = DAYS_PER_MONTHS[int(is_leap(d2.year))][d2.month]
186+
187+
if d_diff < days_in_month - days_in_last_month:
188+
# We don't have a full month, we calculate days
189+
if days_in_last_month < d1.day:
190+
d_diff += d1.day
191+
else:
192+
d_diff += days_in_last_month
193+
194+
m_diff -= 1
195+
else:
196+
# We have a full month, remove days
197+
d_diff = 0
198+
199+
if m_diff < 0:
200+
m_diff += 12
201+
y_diff -= 1
202+
203+
diff['microseconds'] = sign * mic_diff
204+
diff['seconds'] = sign * sec_diff
205+
diff['minutes'] = sign * min_diff
206+
diff['hours'] = sign * hour_diff
207+
diff['days'] = sign * d_diff
208+
diff['months'] = sign * m_diff
209+
diff['years'] = sign * y_diff
210+
211+
return diff
212+
110213
def _sign(x):
111214
return int(copysign(1, x))

pendulum/period.py

Lines changed: 20 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
# -*- coding: utf-8 -*-
22

33
import operator
4-
from dateutil.relativedelta import relativedelta
4+
import pendulum
5+
56
from datetime import datetime, date
67

78
from .mixins.interval import WordableIntervalMixin
89
from .interval import BaseInterval, Interval
910
from .constants import MONTHS_PER_YEAR
11+
from .helpers import precise_diff
1012

1113

1214
class Period(WordableIntervalMixin, BaseInterval):
@@ -16,20 +18,17 @@ class Period(WordableIntervalMixin, BaseInterval):
1618
"""
1719

1820
def __new__(cls, start, end, absolute=False):
19-
from .pendulum import Pendulum
20-
from .date import Date
21-
2221
if absolute and start > end:
2322
end, start = start, end
2423

25-
if isinstance(start, Pendulum):
24+
if isinstance(start, pendulum.Pendulum):
2625
start = start._datetime
27-
elif isinstance(start, Date):
26+
elif isinstance(start, pendulum.Date):
2827
start = date(start.year, start.month, start.day)
2928

30-
if isinstance(end, Pendulum):
29+
if isinstance(end, pendulum.Pendulum):
3130
end = end._datetime
32-
elif isinstance(end, Date):
31+
elif isinstance(end, pendulum.Date):
3332
end = date(end.year, end.month, end.day)
3433

3534
delta = end - start
@@ -39,33 +38,30 @@ def __new__(cls, start, end, absolute=False):
3938
)
4039

4140
def __init__(self, start, end, absolute=False):
42-
from .pendulum import Pendulum
43-
from .date import Date
44-
4541
super(Period, self).__init__()
4642

47-
if not isinstance(start, (Pendulum, Date)):
43+
if not isinstance(start, (pendulum.Date)):
4844
if isinstance(start, datetime):
49-
start = Pendulum.instance(start)
45+
start = pendulum.Pendulum.instance(start)
5046
else:
51-
start = Date.instance(start)
47+
start = pendulum.Date.instance(start)
5248

5349
_start = start
5450
else:
55-
if isinstance(start, Pendulum):
51+
if isinstance(start, pendulum.Pendulum):
5652
_start = start._datetime
5753
else:
5854
_start = date(start.year, start.month, start.day)
5955

60-
if not isinstance(end, (Pendulum, Date)):
56+
if not isinstance(end, (pendulum.Date)):
6157
if isinstance(end, datetime):
62-
end = Pendulum.instance(end)
58+
end = pendulum.Pendulum.instance(end)
6359
else:
64-
end = Date.instance(end)
60+
end = pendulum.Date.instance(end)
6561

6662
_end = end
6763
else:
68-
if isinstance(end, Pendulum):
64+
if isinstance(end, pendulum.Pendulum):
6965
_end = end._datetime
7066
else:
7167
_end = date(end.year, end.month, end.day)
@@ -81,27 +77,27 @@ def __init__(self, start, end, absolute=False):
8177
self._absolute = absolute
8278
self._start = start
8379
self._end = end
84-
self._delta = relativedelta(_end, _start)
80+
self._delta = precise_diff(_start, _end)
8581

8682
@property
8783
def years(self):
88-
return self._delta.years
84+
return self._delta['years']
8985

9086
@property
9187
def months(self):
92-
return self._delta.months
88+
return self._delta['months']
9389

9490
@property
9591
def weeks(self):
96-
return self._delta.weeks
92+
return self._delta['days'] // 7
9793

9894
@property
9995
def days(self):
10096
return self._days
10197

10298
@property
10399
def remaining_days(self):
104-
return abs(self._delta.days) % 7 * self._sign(self._days)
100+
return abs(self._delta['days']) % 7 * self._sign(self._days)
105101

106102
@property
107103
def start(self):

tests/date_tests/test_diff.py

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,12 @@ def test_diff_for_humans_now_and_nearly_month(self):
123123
self.assertEqual('3 weeks ago', Date.today().subtract(weeks=3).diff_for_humans())
124124

125125
def test_diff_for_humans_now_and_month(self):
126-
self.assertEqual('4 weeks ago', Date.today().subtract(weeks=4).diff_for_humans())
127-
self.assertEqual('1 month ago', Date.today().subtract(months=1).diff_for_humans())
126+
with self.wrap_with_test_now(Pendulum.create(2016, 3, 1)):
127+
self.assertEqual('4 weeks ago', Date.today().subtract(weeks=4).diff_for_humans())
128+
self.assertEqual('1 month ago', Date.today().subtract(months=1).diff_for_humans())
129+
130+
with self.wrap_with_test_now(Pendulum.create(2017, 2, 28)):
131+
self.assertEqual('1 month ago', Date.today().subtract(weeks=4).diff_for_humans())
128132

129133
def test_diff_for_humans_now_and_months(self):
130134
self.assertEqual('2 months ago', Date.today().subtract(months=2).diff_for_humans())
@@ -161,6 +165,12 @@ def test_diff_for_humans_now_and_future_month(self):
161165
self.assertEqual('4 weeks from now', Date.today().add(weeks=4).diff_for_humans())
162166
self.assertEqual('1 month from now', Date.today().add(months=1).diff_for_humans())
163167

168+
with self.wrap_with_test_now(Pendulum.create(2017, 3, 31)):
169+
self.assertEqual('1 month from now', Date.today().add(months=1).diff_for_humans())
170+
171+
with self.wrap_with_test_now(Pendulum.create(2017, 4, 30)):
172+
self.assertEqual('1 month from now', Date.today().add(months=1).diff_for_humans())
173+
164174
with self.wrap_with_test_now(Pendulum.create(2017, 1, 31)):
165175
self.assertEqual('1 month from now', Date.today().add(weeks=4).diff_for_humans())
166176

@@ -199,6 +209,12 @@ def test_diff_for_humans_other_and_month(self):
199209
self.assertEqual('4 weeks before', Date.today().diff_for_humans(Date.today().add(weeks=4)))
200210
self.assertEqual('1 month before', Date.today().diff_for_humans(Date.today().add(months=1)))
201211

212+
with self.wrap_with_test_now(Pendulum.create(2017, 3, 31)):
213+
self.assertEqual('1 month before', Date.today().diff_for_humans(Date.today().add(months=1)))
214+
215+
with self.wrap_with_test_now(Pendulum.create(2017, 4, 30)):
216+
self.assertEqual('1 month before', Date.today().diff_for_humans(Date.today().add(months=1)))
217+
202218
with self.wrap_with_test_now(Pendulum.create(2017, 1, 31)):
203219
self.assertEqual('1 month before', Date.today().diff_for_humans(Date.today().add(weeks=4)))
204220

@@ -233,8 +249,12 @@ def test_diff_for_humans_other_and_nearly_future_month(self):
233249
self.assertEqual('3 weeks after', Date.today().diff_for_humans(Date.today().subtract(weeks=3)))
234250

235251
def test_diff_for_humans_other_and_future_month(self):
236-
self.assertEqual('4 weeks after', Date.today().diff_for_humans(Date.today().subtract(weeks=4)))
237-
self.assertEqual('1 month after', Date.today().diff_for_humans(Date.today().subtract(months=1)))
252+
with self.wrap_with_test_now(Pendulum.create(2016, 3, 1)):
253+
self.assertEqual('4 weeks after', Date.today().diff_for_humans(Date.today().subtract(weeks=4)))
254+
self.assertEqual('1 month after', Date.today().diff_for_humans(Date.today().subtract(months=1)))
255+
256+
with self.wrap_with_test_now(Pendulum.create(2017, 2, 28)):
257+
self.assertEqual('1 month after', Date.today().diff_for_humans(Date.today().subtract(weeks=4)))
238258

239259
def test_diff_for_humans_other_and_future_months(self):
240260
self.assertEqual('2 months after', Date.today().diff_for_humans(Date.today().subtract(months=2)))

tests/test_helpers.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# -*- coding: utf-8 -*-
2+
3+
from datetime import datetime
4+
from pendulum.helpers import precise_diff
5+
6+
from . import AbstractTestCase
7+
8+
9+
class HelpersTestCase(AbstractTestCase):
10+
11+
def test_precise_diff(self):
12+
dt1 = datetime(2003, 3, 1, 0, 0, 0)
13+
dt2 = datetime(2003, 1, 31, 23, 59, 59)
14+
15+
diff = precise_diff(dt1, dt2)
16+
self.assert_diff(diff, months=-1, seconds=-1)
17+
18+
diff = precise_diff(dt2, dt1)
19+
self.assert_diff(diff, months=1, seconds=1)
20+
21+
dt1 = datetime(2012, 3, 1, 0, 0, 0)
22+
dt2 = datetime(2012, 1, 31, 23, 59, 59)
23+
24+
diff = precise_diff(dt1, dt2)
25+
self.assert_diff(diff, months=-1, seconds=-1)
26+
27+
diff = precise_diff(dt2, dt1)
28+
self.assert_diff(diff, months=1, seconds=1)
29+
30+
dt1 = datetime(2001, 1, 1)
31+
dt2 = datetime(2003, 9, 17, 20, 54, 47, 282310)
32+
33+
diff = precise_diff(dt1, dt2)
34+
self.assert_diff(
35+
diff,
36+
years=2, months=8, days=16,
37+
hours=20, minutes=54, seconds=47, microseconds=282310
38+
)
39+
40+
def assert_diff(self, diff,
41+
years=0, months=0, days=0,
42+
hours=0, minutes=0, seconds=0, microseconds=0):
43+
self.assertEqual(diff['years'], years)
44+
self.assertEqual(diff['months'], months)
45+
self.assertEqual(diff['days'], days)
46+
self.assertEqual(diff['hours'], hours)
47+
self.assertEqual(diff['minutes'], minutes)
48+
self.assertEqual(diff['seconds'], seconds)
49+
self.assertEqual(diff['microseconds'], microseconds)

0 commit comments

Comments
 (0)