Skip to content

Commit 660c6b9

Browse files
committed
Fixes behavior of add()/subtract() methods for years, months and days when a DST transition occurs.
1 parent d90aa23 commit 660c6b9

File tree

4 files changed

+236
-173
lines changed

4 files changed

+236
-173
lines changed

pendulum/pendulum.py

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1429,7 +1429,14 @@ def add(self, years=0, months=0, weeks=0, days=0,
14291429
)
14301430

14311431
dt = self._datetime + delta
1432-
dt = self._tz.convert(dt)
1432+
1433+
if any([years, months, weeks, days]):
1434+
# If we specified any of years, months, weeks or days
1435+
# we will not apply the transition (if any)
1436+
dt = self._tz.convert(dt.replace(tzinfo=None))
1437+
else:
1438+
# Else, we need to apply the transition properly (if any)
1439+
dt = self._tz.convert(dt)
14331440

14341441
return self.instance(dt)
14351442

@@ -1464,17 +1471,12 @@ def subtract(self, years=0, months=0, weeks=0, days=0,
14641471
14651472
:rtype: Pendulum
14661473
"""
1467-
delta = relativedelta(
1468-
years=years, months=months, weeks=weeks, days=days,
1469-
hours=hours, minutes=minutes, seconds=seconds,
1470-
microseconds=microseconds
1474+
return self.add(
1475+
years=-years, months=-months, weeks=-weeks, days=-days,
1476+
hours=-hours, minutes=-minutes, seconds=-seconds,
1477+
microseconds=-microseconds
14711478
)
14721479

1473-
dt = self._datetime - delta
1474-
dt = self._tz.convert(dt)
1475-
1476-
return self.instance(dt)
1477-
14781480
def add_timedelta(self, delta):
14791481
"""
14801482
Add timedelta duration to the instance.

tests/pendulum_tests/test_add.py

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,3 +115,124 @@ def test_add_to_fixed_timezones(self):
115115
self.assertPendulum(dt, 2015, 3, 15, 1, 0, 0)
116116
self.assertEqual('-06:00', dt.timezone_name)
117117
self.assertEqual(-6 * 3600, dt.offset)
118+
119+
def test_add_time_to_new_transition_skipped(self):
120+
dt = pendulum.create(2013, 3, 31, 1, 59, 59, 999999, 'Europe/Paris')
121+
122+
self.assertPendulum(dt, 2013, 3, 31, 1, 59, 59, 999999)
123+
self.assertEqual('Europe/Paris', dt.timezone_name)
124+
self.assertEqual(3600, dt.offset)
125+
self.assertFalse(dt.is_dst)
126+
127+
dt = dt.add(microseconds=1)
128+
129+
self.assertPendulum(dt, 2013, 3, 31, 3, 0, 0, 0)
130+
self.assertEqual('Europe/Paris', dt.timezone_name)
131+
self.assertEqual(7200, dt.offset)
132+
self.assertTrue(dt.is_dst)
133+
134+
dt = pendulum.create(2013, 3, 10, 1, 59, 59, 999999, 'America/New_York')
135+
136+
self.assertPendulum(dt, 2013, 3, 10, 1, 59, 59, 999999)
137+
self.assertEqual('America/New_York', dt.timezone_name)
138+
self.assertEqual(-5 * 3600, dt.offset)
139+
self.assertFalse(dt.is_dst)
140+
141+
dt = dt.add(microseconds=1)
142+
143+
self.assertPendulum(dt, 2013, 3, 10, 3, 0, 0, 0)
144+
self.assertEqual('America/New_York', dt.timezone_name)
145+
self.assertEqual(- 4 * 3600, dt.offset)
146+
self.assertTrue(dt.is_dst)
147+
148+
dt = pendulum.create(1957, 4, 28, 1, 59, 59, 999999, 'America/New_York')
149+
150+
self.assertPendulum(dt, 1957, 4, 28, 1, 59, 59, 999999)
151+
self.assertEqual('America/New_York', dt.timezone_name)
152+
self.assertEqual(-5 * 3600, dt.offset)
153+
self.assertFalse(dt.is_dst)
154+
155+
dt = dt.add(microseconds=1)
156+
157+
self.assertPendulum(dt, 1957, 4, 28, 3, 0, 0, 0)
158+
self.assertEqual('America/New_York', dt.timezone_name)
159+
self.assertEqual(- 4 * 3600, dt.offset)
160+
self.assertTrue(dt.is_dst)
161+
162+
def test_add_time_to_new_transition_skipped_big(self):
163+
dt = pendulum.create(2013, 3, 31, 1, tz='Europe/Paris')
164+
165+
self.assertPendulum(dt, 2013, 3, 31, 1, 0, 0, 0)
166+
self.assertEqual('Europe/Paris', dt.timezone_name)
167+
self.assertEqual(3600, dt.offset)
168+
self.assertFalse(dt.is_dst)
169+
170+
dt = dt.add(weeks=1)
171+
172+
self.assertPendulum(dt, 2013, 4, 7, 1, 0, 0, 0)
173+
self.assertEqual('Europe/Paris', dt.timezone_name)
174+
self.assertEqual(7200, dt.offset)
175+
self.assertTrue(dt.is_dst)
176+
177+
def test_add_time_to_new_transition_repeated(self):
178+
dt = pendulum.create(2013, 10, 27, 1, 59, 59, 999999, 'Europe/Paris')
179+
dt = dt.add(hours=1)
180+
181+
self.assertPendulum(dt, 2013, 10, 27, 2, 59, 59, 999999)
182+
self.assertEqual('Europe/Paris', dt.timezone_name)
183+
self.assertEqual(7200, dt.offset)
184+
self.assertTrue(dt.is_dst)
185+
186+
dt = dt.add(microseconds=1)
187+
188+
self.assertPendulum(dt, 2013, 10, 27, 2, 0, 0, 0)
189+
self.assertEqual('Europe/Paris', dt.timezone_name)
190+
self.assertEqual(3600, dt.offset)
191+
self.assertFalse(dt.is_dst)
192+
193+
194+
dt = pendulum.create(2013, 11, 3, 0, 59, 59, 999999, 'America/New_York')
195+
dt = dt.add(hours=1)
196+
197+
self.assertPendulum(dt, 2013, 11, 3, 1, 59, 59, 999999)
198+
self.assertEqual('America/New_York', dt.timezone_name)
199+
self.assertEqual(-4 * 3600, dt.offset)
200+
self.assertTrue(dt.is_dst)
201+
202+
dt = dt.add(microseconds=1)
203+
204+
self.assertPendulum(dt, 2013, 11, 3, 1, 0, 0, 0)
205+
self.assertEqual('America/New_York', dt.timezone_name)
206+
self.assertEqual(-5 * 3600, dt.offset)
207+
self.assertFalse(dt.is_dst)
208+
209+
def test_add_time_to_new_transition_repeated_big(self):
210+
dt = pendulum.create(2013, 10, 27, 1, tz='Europe/Paris')
211+
212+
self.assertPendulum(dt, 2013, 10, 27, 1, 0, 0, 0)
213+
self.assertEqual('Europe/Paris', dt.timezone_name)
214+
self.assertEqual(7200, dt.offset)
215+
self.assertTrue(dt.is_dst)
216+
217+
dt = dt.add(weeks=1)
218+
219+
self.assertPendulum(dt, 2013, 11, 3, 1, 0, 0, 0)
220+
self.assertEqual('Europe/Paris', dt.timezone_name)
221+
self.assertEqual(3600, dt.offset)
222+
self.assertFalse(dt.is_dst)
223+
224+
def test_add_time_to_new_transition_does_not_use_transition_rule(self):
225+
pendulum.set_transition_rule(pendulum.TRANSITION_ERROR)
226+
dt = pendulum.create(2013, 3, 31, 1, 59, 59, 999999, 'Europe/Paris')
227+
228+
self.assertPendulum(dt, 2013, 3, 31, 1, 59, 59, 999999)
229+
self.assertEqual('Europe/Paris', dt.timezone_name)
230+
self.assertEqual(3600, dt.offset)
231+
self.assertFalse(dt.is_dst)
232+
233+
dt = dt.add(microseconds=1)
234+
235+
self.assertPendulum(dt, 2013, 3, 31, 3, 0, 0, 0)
236+
self.assertEqual('Europe/Paris', dt.timezone_name)
237+
self.assertEqual(7200, dt.offset)
238+
self.assertTrue(dt.is_dst)

tests/pendulum_tests/test_sub.py

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

3+
import pendulum
34
from datetime import timedelta
45
from pendulum import Pendulum
56

@@ -89,6 +90,108 @@ def test_subtract_timedelta(self):
8990
self.assertEqual(59, d.second)
9091
self.assertEqual(123456, d.microsecond)
9192

93+
def test_subtract_time_to_new_transition_skipped(self):
94+
dt = pendulum.create(2013, 3, 31, 3, 0, 0, 0, 'Europe/Paris')
95+
96+
self.assertPendulum(dt, 2013, 3, 31, 3, 0, 0, 0)
97+
self.assertEqual('Europe/Paris', dt.timezone_name)
98+
self.assertEqual(7200, dt.offset)
99+
self.assertTrue(dt.is_dst)
100+
101+
dt = dt.subtract(microseconds=1)
102+
103+
self.assertPendulum(dt, 2013, 3, 31, 1, 59, 59, 999999)
104+
self.assertEqual('Europe/Paris', dt.timezone_name)
105+
self.assertEqual(3600, dt.offset)
106+
self.assertFalse(dt.is_dst)
107+
108+
dt = pendulum.create(2013, 3, 10, 3, 0, 0, 0, 'America/New_York')
109+
110+
self.assertPendulum(dt, 2013, 3, 10, 3, 0, 0, 0)
111+
self.assertEqual('America/New_York', dt.timezone_name)
112+
self.assertEqual(-4 * 3600, dt.offset)
113+
self.assertTrue(dt.is_dst)
114+
115+
dt = dt.subtract(microseconds=1)
116+
117+
self.assertPendulum(dt, 2013, 3, 10, 1, 59, 59, 999999)
118+
self.assertEqual('America/New_York', dt.timezone_name)
119+
self.assertEqual(-5 * 3600, dt.offset)
120+
self.assertFalse(dt.is_dst)
121+
122+
dt = pendulum.create(1957, 4, 28, 3, 0, 0, 0, 'America/New_York')
123+
124+
self.assertPendulum(dt, 1957, 4, 28, 3, 0, 0, 0)
125+
self.assertEqual('America/New_York', dt.timezone_name)
126+
self.assertEqual(-4 * 3600, dt.offset)
127+
self.assertTrue(dt.is_dst)
128+
129+
dt = dt.subtract(microseconds=1)
130+
131+
self.assertPendulum(dt, 1957, 4, 28, 1, 59, 59, 999999)
132+
self.assertEqual('America/New_York', dt.timezone_name)
133+
self.assertEqual(-5 * 3600, dt.offset)
134+
self.assertFalse(dt.is_dst)
135+
136+
def test_subtract_time_to_new_transition_skipped_big(self):
137+
dt = pendulum.create(2013, 3, 31, 3, 0, 0, 0, 'Europe/Paris')
138+
139+
self.assertPendulum(dt, 2013, 3, 31, 3, 0, 0, 0)
140+
self.assertEqual('Europe/Paris', dt.timezone_name)
141+
self.assertEqual(7200, dt.offset)
142+
self.assertTrue(dt.is_dst)
143+
144+
dt = dt.subtract(days=1)
145+
146+
self.assertPendulum(dt, 2013, 3, 30, 3, 0, 0, 0)
147+
self.assertEqual('Europe/Paris', dt.timezone_name)
148+
self.assertEqual(3600, dt.offset)
149+
self.assertFalse(dt.is_dst)
150+
151+
def test_subtract_time_to_new_transition_repeated(self):
152+
dt = pendulum.create(2013, 10, 27, 2, 0, 0, 0, 'Europe/Paris')
153+
154+
self.assertPendulum(dt, 2013, 10, 27, 2, 0, 0, 0)
155+
self.assertEqual('Europe/Paris', dt.timezone_name)
156+
self.assertEqual(3600, dt.offset)
157+
self.assertFalse(dt.is_dst)
158+
159+
dt = dt.subtract(microseconds=1)
160+
161+
self.assertPendulum(dt, 2013, 10, 27, 2, 59, 59, 999999)
162+
self.assertEqual('Europe/Paris', dt.timezone_name)
163+
self.assertEqual(7200, dt.offset)
164+
self.assertTrue(dt.is_dst)
165+
166+
dt = pendulum.create(2013, 11, 3, 1, 0, 0, 0, 'America/New_York')
167+
168+
self.assertPendulum(dt, 2013, 11, 3, 1, 0, 0, 0)
169+
self.assertEqual('America/New_York', dt.timezone_name)
170+
self.assertEqual(-5 * 3600, dt.offset)
171+
self.assertFalse(dt.is_dst)
172+
173+
dt = dt.subtract(microseconds=1)
174+
175+
self.assertPendulum(dt, 2013, 11, 3, 1, 59, 59, 999999)
176+
self.assertEqual('America/New_York', dt.timezone_name)
177+
self.assertEqual(-4 * 3600, dt.offset)
178+
self.assertTrue(dt.is_dst)
179+
180+
def test_subtract_time_to_new_transition_repeated_big(self):
181+
dt = pendulum.create(2013, 10, 27, 2, 0, 0, 0, 'Europe/Paris')
182+
183+
self.assertPendulum(dt, 2013, 10, 27, 2, 0, 0, 0)
184+
self.assertEqual('Europe/Paris', dt.timezone_name)
185+
self.assertEqual(3600, dt.offset)
186+
self.assertFalse(dt.is_dst)
187+
188+
dt = dt.subtract(days=1)
189+
190+
self.assertPendulum(dt, 2013, 10, 26, 2, 0, 0, 0)
191+
self.assertEqual('Europe/Paris', dt.timezone_name)
192+
self.assertEqual(7200, dt.offset)
193+
self.assertTrue(dt.is_dst)
194+
92195
def test_subtract_invalid_type(self):
93196
d = Pendulum(1975, 5, 21, 0, 0, 0)
94197

0 commit comments

Comments
 (0)