summaryrefslogtreecommitdiff
path: root/src/croniter/croniter.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/croniter/croniter.py')
-rw-r--r--src/croniter/croniter.py158
1 files changed, 118 insertions, 40 deletions
diff --git a/src/croniter/croniter.py b/src/croniter/croniter.py
index 96dec79..21e70ba 100644
--- a/src/croniter/croniter.py
+++ b/src/croniter/croniter.py
@@ -40,6 +40,72 @@ class CroniterNotAlphaError(CroniterBadCronError):
pass
+def zerodate(d):
+ return d.replace(hour=0, minute=0, second=0, microsecond=0)
+
+
+def timedelta_to_seconds(td):
+ """
+ Converts a 'datetime.timedelta' object `td` into seconds contained in
+ the duration.
+ Note: We cannot use `timedelta.total_seconds()` because this is not
+ supported by Python 2.6.
+ """
+ return (td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6) \
+ / 10**6
+
+
+def datetime_to_timestamp(d):
+ """
+ Converts a `datetime` object `d` into a UNIX timestamp.
+ """
+ if d.tzinfo is not None:
+ d = d.replace(tzinfo=None) - d.utcoffset()
+ return timedelta_to_seconds(d - datetime.datetime(1970, 1, 1))
+
+
+def timestamp_to_datetime(timestamp, tzinfo=None):
+ """
+ Converts a UNIX timestamp `timestamp` into a `datetime` object.
+ """
+ result = datetime.datetime.utcfromtimestamp(timestamp)
+ if tzinfo:
+ result = result.replace(tzinfo=tzutc()).astimezone(tzinfo)
+ return result
+
+
+def get_next_dst_window(dt1, dt2=None):
+ '''
+ Return the lower, upper bound of the first DST transition between
+ the two dates, and if it is the summer, or winter one.
+ '''
+ if dt2 is None:
+ dt2 = dt1 + relativedelta(days=3)
+ minh = min(zerodate(dt1), zerodate(dt2))
+ minhdst = timedelta_to_seconds(minh.utcoffset())
+ is_summer = True
+ window = None, None, is_summer
+ for hour in range(
+ int(
+ round(
+ croniter._timedelta_to_seconds(abs(dt1 - dt2)) / 60)
+ )
+ ):
+ curh = minh + relativedelta(hours=hour)
+ upb = timestamp_to_datetime(datetime_to_timestamp(curh), tzinfo=curh.tzinfo)
+ coffset = timedelta_to_seconds(upb.utcoffset())
+ if coffset != minhdst:
+ ilowb = upb - relativedelta(hours=1)
+ lowb = timestamp_to_datetime(datetime_to_timestamp(ilowb), tzinfo=curh.tzinfo)
+ lowboffset = timedelta_to_seconds(lowb.utcoffset())
+ # impossible to happen for now
+ if upb is None:
+ raise CroniterError('DST window error')
+ window = lowb, upb, coffset > lowboffset
+ break
+ return window
+
+
class croniter(object):
MONTHS_IN_YEAR = 12
RANGES = (
@@ -149,20 +215,13 @@ class croniter(object):
"""
Converts a `datetime` object `d` into a UNIX timestamp.
"""
- if d.tzinfo is not None:
- d = d.replace(tzinfo=None) - d.utcoffset()
-
- return cls._timedelta_to_seconds(d - datetime.datetime(1970, 1, 1))
+ return datetime_to_timestamp(d)
def _timestamp_to_datetime(self, timestamp):
"""
Converts a UNIX timestamp `timestamp` into a `datetime` object.
"""
- result = datetime.datetime.utcfromtimestamp(timestamp)
- if self.tzinfo:
- result = result.replace(tzinfo=tzutc()).astimezone(self.tzinfo)
-
- return result
+ return timestamp_to_datetime(timestamp, tzinfo=self.tzinfo)
@classmethod
def _timedelta_to_seconds(cls, td):
@@ -172,8 +231,7 @@ class croniter(object):
Note: We cannot use `timedelta.total_seconds()` because this is not
supported by Python 2.6.
"""
- return (td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6) \
- / 10**6
+ return timedelta_to_seconds(td)
def _get_next(self, ret_type=None, is_prev=None):
if is_prev is None:
@@ -204,36 +262,9 @@ class croniter(object):
else:
result = self._calc(self.cur, expanded,
nth_weekday_of_month, is_prev)
-
- # DST Handling for cron job spanning accross days
- dtstarttime = self._timestamp_to_datetime(self.dst_start_time)
- dtstarttime_utcoffset = (
- dtstarttime.utcoffset() or datetime.timedelta(0))
- dtresult = self._timestamp_to_datetime(result)
- lag = lag_hours = 0
- # do we trigger DST on next crontab (handle backward changes)
- dtresult_utcoffset = dtstarttime_utcoffset
- if dtresult and self.tzinfo:
- dtresult_utcoffset = dtresult.utcoffset()
- lag_hours = (
- self._timedelta_to_seconds(dtresult - dtstarttime) / (60 * 60)
- )
- lag = self._timedelta_to_seconds(
- dtresult_utcoffset - dtstarttime_utcoffset
- )
- hours_before_midnight = 24 - dtstarttime.hour
- if dtresult_utcoffset != dtstarttime_utcoffset:
- if (
- (lag > 0 and abs(lag_hours) >= hours_before_midnight)
- or (lag < 0 and
- ((3600 * abs(lag_hours) + abs(lag)) >= hours_before_midnight * 3600))
- ):
- dtresult = dtresult - datetime.timedelta(seconds=lag)
- result = self._datetime_to_timestamp(dtresult)
- self.dst_start_time = result
self.cur = result
if issubclass(ret_type, datetime.datetime):
- result = dtresult
+ result = timestamp_to_datetime(self.cur, tzinfo=self.tzinfo)
return result
# iterator protocol, to enable direct use of croniter
@@ -455,9 +486,56 @@ class croniter(object):
month, year = dst.month, dst.year
next = True
break
+ dst = dst.replace(microsecond=0)
+ # if a DST occurs during the 24 hours between result candidate
+ # and selected original date:
+ # we check if final candidate is occuring during
+ # the one-hour DST transition.
+ # In this case:
+ # Summer time (+1h delta): 2(-3)h -> 3h
+ # the algo is NEXTing: we add one hour for candidates between the window
+ # 0 * * * * -> xxxx 0100
+ # 0 * * * * -> xxxx 0300
+ # 0 * * * * -> xxxx 0400
+ # 0 * * * * -> xxxx 0500
+ # the algo is PREVing: we remove one hour for candidates between the window
+ # 0 * * * * -> xxxx 0400
+ # 0 * * * * -> xxxx 0300
+ # 0 * * * * -> xxxx 0100
+ # 0 * * * * -> xxxx 0000
+ # Winter time (+1h delta): 2(-3)h -> 1h
+ # the algo is NEXTing: we add 2 hours
+ # 0 * * * * -> xxxx 0300
+ # 0 * * * * -> xxxx 0600
+ # 0 * * * * -> xxxx 0700
+ # the algo is PREVing: we remove two hour
+ # 0 * * * * -> xxxx 0300
+ # 0 * * * * -> xxxx 0100
+ # 0 * * * * -> xxxx 0000
+ if dst.tzinfo is not None:
+ curdt = self._timestamp_to_datetime(self.cur)
+ lowb, upb, is_summer = get_next_dst_window(curdt)
+ ohl = dst + relativedelta(hours=1)
+ if lowb is not None:
+ if (
+ is_summer and
+ ((curdt <= lowb and dst > lowb) or
+ (curdt < lowb and dst >= lowb))
+ ):
+ sign = is_prev and -1 or 1
+ dst += sign * relativedelta(hours=1)
+ next = False
+ elif (
+ not is_summer and
+ (curdt >= upb) and
+ (dst < lowb)
+ ):
+ sign = is_prev and -1 or 1
+ dst += sign * relativedelta(hours=1)
+ next = False
if next:
continue
- return self._datetime_to_timestamp(dst.replace(microsecond=0))
+ return self._datetime_to_timestamp(dst)
if is_prev:
raise CroniterBadDateError("failed to find prev date")