From 1c05bed1c6da68ea12a80bd6a4973be3bd654569 Mon Sep 17 00:00:00 2001 From: Mathieu Le Marec - Pasquet Date: Wed, 15 Mar 2017 23:34:45 +0100 Subject: [PATCH] Implement DST support This fixes https://github.com/taichino/croniter/issues/82 --- README.rst | 6 ++++ src/croniter/croniter.py | 27 ++++++++++++++++-- src/croniter/tests/test_croniter.py | 43 ++++++++++++++++++++++++++++- 3 files changed, 72 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 9eddcc8..f7f8e27 100644 --- a/README.rst +++ b/README.rst @@ -80,6 +80,12 @@ Supported added for ``get_prev`` method. (>= 0.2.0):: >>> print itr.get_prev(datetime) # 2010-07-01 00:00:00 >>> print itr.get_prev(datetime) # 2010-06-01 00:00:00 +About DST +========= +Be sure to init your croniter instance with a TZ aware datetime for this to work !:: + + >>> local_date = tz.localize(datetime(2017, 3, 26)) + >>> val = croniter('0 0 * * *', local_date).get_next(datetime) Develop this package ==================== diff --git a/src/croniter/croniter.py b/src/croniter/croniter.py index 5dce8eb..2952b3c 100644 --- a/src/croniter/croniter.py +++ b/src/croniter/croniter.py @@ -75,6 +75,7 @@ class croniter(object): self.tzinfo = start_time.tzinfo start_time = self._datetime_to_timestamp(start_time) + self.start_time = start_time self.cur = start_time self.exprs = expr_format.split() @@ -257,11 +258,31 @@ class croniter(object): result = t1 if t1 > t2 else t2 else: result = self._calc(self.cur, expanded, is_prev) + + # DST Handling for cron job spanning accross days + dtstarttime = self._timestamp_to_datetime(self.start_time) + dtresult = self._timestamp_to_datetime(result) + dtresult_utcoffset = dtresult.utcoffset() or datetime.timedelta(0) + dtstarttime_utcoffset = ( + dtstarttime.utcoffset() or datetime.timedelta(0)) + hours_before_midnight = 24 - dtstarttime.hour + lag_hours = ( + self._timedelta_to_seconds(dtresult - dtstarttime) / (60*60) + ) + if ( + lag_hours >= hours_before_midnight and + (dtresult_utcoffset or dtstarttime_utcoffset) and + (dtresult_utcoffset != dtstarttime_utcoffset) + ): + lag = self._timedelta_to_seconds( + dtresult_utcoffset - dtstarttime_utcoffset + ) + dtresult = dtresult - datetime.timedelta(seconds=lag) + result = self._datetime_to_timestamp(dtresult) + self.cur = result - if issubclass(ret_type, datetime.datetime): - result = self._timestamp_to_datetime(result) - + result = dtresult return result def _calc(self, now, expanded, is_prev): diff --git a/src/croniter/tests/test_croniter.py b/src/croniter/tests/test_croniter.py index 4bc5d21..fb9feef 100755 --- a/src/croniter/tests/test_croniter.py +++ b/src/croniter/tests/test_croniter.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- import unittest -from datetime import datetime +from datetime import datetime, timedelta from time import sleep import pytz from croniter import croniter, CroniterBadDateError @@ -640,6 +640,47 @@ class CroniterTest(base.TestCase): ct = croniter('*/30 * * * *', tz.localize(start)) self.assertScheduleTimezone(lambda: ct.get_prev(datetime), reversed(expected_schedule)) + def test_std_dst(self): + """ + DST tests + + This fixes https://github.com/taichino/croniter/issues/82 + + """ + tz = pytz.timezone('Europe/Warsaw') + # -> 2017-03-26 01:59+1:00 -> 03:00+2:00 + local_date = tz.localize(datetime(2017, 3, 26)) + val = croniter('0 0 * * *', local_date).get_next(datetime) + self.assertEqual(val, tz.localize(datetime(2017, 3, 27))) + # + local_date = tz.localize(datetime(2017, 3, 26, 1)) + cr = croniter('0 * * * *', local_date) + val = cr.get_next(datetime) + self.assertEqual(val, tz.localize(datetime(2017, 3, 26, 3))) + val = cr.get_current(datetime) + self.assertEqual(val, tz.localize(datetime(2017, 3, 26, 3))) + + # -> 2017-10-29 02:59+2:00 -> 02:00+1:00 + local_date = tz.localize(datetime(2017, 10, 29)) + val = croniter('0 0 * * *', local_date).get_next(datetime) + self.assertEqual(val, tz.localize(datetime(2017, 10, 30))) + local_date = tz.localize(datetime(2017, 10, 29, 1, 59)) + val = croniter('0 * * * *', local_date).get_next(datetime) + self.assertEqual( + val.replace(tzinfo=None), + tz.localize(datetime(2017, 10, 29, 2)).replace(tzinfo=None)) + local_date = tz.localize(datetime(2017, 10, 29, 2)) + val = croniter('0 * * * *', local_date).get_next(datetime) + self.assertEqual(val, tz.localize(datetime(2017, 10, 29, 3))) + local_date = tz.localize(datetime(2017, 10, 29, 3)) + val = croniter('0 * * * *', local_date).get_next(datetime) + self.assertEqual(val, tz.localize(datetime(2017, 10, 29, 4))) + local_date = tz.localize(datetime(2017, 10, 29, 4)) + val = croniter('0 * * * *', local_date).get_next(datetime) + self.assertEqual(val, tz.localize(datetime(2017, 10, 29, 5))) + local_date = tz.localize(datetime(2017, 10, 29, 5)) + val = croniter('0 * * * *', local_date).get_next(datetime) + self.assertEqual(val, tz.localize(datetime(2017, 10, 29, 6))) if __name__ == '__main__': unittest.main()