Skip to content

Commit

Permalink
Reallow DOW=7, & rework range calculations.
Browse files Browse the repository at this point in the history
Related to #90.
  • Loading branch information
kiorky committed Oct 29, 2024
1 parent 0ec55d4 commit 79c0505
Show file tree
Hide file tree
Showing 4 changed files with 205 additions and 133 deletions.
5 changes: 2 additions & 3 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
Changelog
==============

4.0.1 (unreleased)
5.0.1 (unreleased)
------------------

- Nothing changed yet.

- Community wanted: Reintroduce 7 as DayOfWeek in deviation from standard cron (#90). [kiorky]

4.0.0 (2024-10-28)
------------------
Expand Down
19 changes: 18 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,24 @@ Random "R" definition keywords are supported, and remain consistent only within
datetime.datetime(2021, 4, 11, 4, 19)


Note about Ranges
=================
Note that as a deviation from cron standard, croniter is somehow laxist with ranges and will allow ranges of **Jan-Dec**, & **Sun-Sat** in reverse way and interpret them as following examples:
- **Apr-Jan**: from April to january
- **Sat-Sun**: Saturday, Sunday
- **Wed-Sun**: Wednesday to Saturday, Sunday

Please note that if a /step is given, it will be respected.

Note about Sunday
=================
Note that as a deviation from cron standard, croniter like numerous cron implementations supports SUNDAY to be expressed as DAY 7, allowing such expressions::

0 0 * * 7
0 0 * * 6-7
0 0 * * 6,7


Keyword expressions
===================

Expand All @@ -303,7 +321,6 @@ What they evaluate to depends on whether you supply hash_id: no hash_id correspo
@annually 0 0 1 1 * H H H H * H
============ ============ ================


Upgrading
==========

Expand Down
94 changes: 58 additions & 36 deletions src/croniter/croniter.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ class croniter(object):
{},
{0: 1},
{0: 1},
{},
{7: 0},
{},
{}
)
Expand Down Expand Up @@ -791,6 +791,21 @@ def is_leap(self, year):
else:
return False

@classmethod
def value_alias(cls, val, field, len_expressions=UNIX_CRON_LEN):
if isinstance(len_expressions, (list, dict, tuple, set)):
len_expressions = len(len_expressions)
if val in cls.LOWMAP[field] and not (
# do not support 0 as a month either for classical 5 fields cron,
# 6fields second repeat form or 7 fields year form
# but still let conversion happen if day field is shifted
(field in [DAY_FIELD, MONTH_FIELD] and len_expressions == UNIX_CRON_LEN) or
(field in [MONTH_FIELD, DOW_FIELD] and len_expressions == SECOND_CRON_LEN) or
(field in [DAY_FIELD, MONTH_FIELD, DOW_FIELD] and len_expressions == YEAR_CRON_LEN)
):
val = cls.LOWMAP[field][val]
return val

@classmethod
def _expand(cls, expr_format, hash_id=None, second_at_beginning=False, from_timestamp=None):
# Split the expression in components, and normalize L -> l, MON -> mon,
Expand Down Expand Up @@ -886,8 +901,6 @@ def _expand(cls, expr_format, hash_id=None, second_at_beginning=False, from_time

if m:
# early abort if low/high are out of bounds
add_sunday = False

(low, high, step) = m.group(1), m.group(2), m.group(4) or 1
if i == DAY_FIELD and high == 'l':
high = '31'
Expand All @@ -898,19 +911,21 @@ def _expand(cls, expr_format, hash_id=None, second_at_beginning=False, from_time
if not only_int_re.search(high):
high = "{0}".format(cls._alphaconv(i, high, expressions))

if (
not low or not high or int(low) > int(high)
or not only_int_re.search(str(step))
):
# handle -Sun notation as Sunday is DOW-0
if i == DOW_FIELD and high == '0':
add_sunday = True
high = '6'
else:
# normally, it's already guarded by the RE that should not accept not-int values.
if not only_int_re.search(str(step)):
raise CroniterBadCronError(
"[{0}] step '{2}' in field {1} is not acceptable".format(
expr_format, i, step))
step = int(step)

for band in low, high:
if not only_int_re.search(str(band)):
raise CroniterBadCronError(
"[{0}] is not acceptable".format(expr_format))
"[{0}] bands '{2}-{3}' in field {1} are not acceptable".format(
expr_format, i, low, high))

low, high = [cls.value_alias(int(_val), i, expressions) for _val in (low, high)]

low, high, step = map(int, [low, high, step])
if (
max(low, high) > max(cls.RANGES[i][0], cls.RANGES[i][1])
):
Expand All @@ -920,19 +935,34 @@ def _expand(cls, expr_format, hash_id=None, second_at_beginning=False, from_time
if from_timestamp:
low = cls._get_low_from_current_date_number(i, int(step), int(from_timestamp))

try:
rng = range(low, high + 1, step)
except ValueError as exc:
raise CroniterBadCronError(
'invalid range: {0}'.format(exc))

e_list += (["{0}#{1}".format(item, nth) for item in rng]
if i == DOW_FIELD and nth and nth != "l" else rng)
# if low == high, this means all week
if (i == DOW_FIELD) and low == high and not (add_sunday and low == 6):
_ = [e_list.append(dow) for dow in range(7) if dow not in e_list]
if (i == DOW_FIELD) and add_sunday and (0 not in e_list):
e_list.insert(0, 0)
# Handle when the second bound of the range is in backtracking order:
# eg: X-Sun or X-7 (Sat-Sun) in DOW, or X-Jan (Apr-Jan) in MONTH
if low > high:
whole_field_range = list(range(cls.RANGES[i][0], cls.RANGES[i][1] + 1, 1))
# Add FirstBound -> ENDRANGE, respecting step
rng = list(range(low, cls.RANGES[i][1] + 1, step))
# Then 0 -> SecondBound, but skipping n first occurences according to step
# EG to respect such expressions : Apr-Jan/3
to_skip = 0
if rng:
already_skipped = list(reversed(whole_field_range)).index(rng[-1])
curpos = whole_field_range.index(rng[-1])
if ((curpos + step) > len(whole_field_range)) and (already_skipped < step):
to_skip = step - already_skipped
rng += list(range(cls.RANGES[i][0] + to_skip, high + 1, step))
# if we include a range type: Jan-Jan, or Sun-Sun,
# it means the whole cycle (all days of week, # all monthes of year, etc)
elif low == high:
rng = list(range(cls.RANGES[i][0], cls.RANGES[i][1] + 1, step))
else:
try:
rng = list(range(low, high + 1, step))
except ValueError as exc:
raise CroniterBadCronError('invalid range: {0}'.format(exc))

rng = (["{0}#{1}".format(item, nth) for item in rng]
if i == DOW_FIELD and nth and nth != "l" else rng)
e_list += [a for a in rng if a not in e_list]
else:
if t.startswith('-'):
raise CroniterBadCronError((
Expand All @@ -947,15 +977,7 @@ def _expand(cls, expr_format, hash_id=None, second_at_beginning=False, from_time
except ValueError:
pass

if t in cls.LOWMAP[i] and not (
# do not support 0 as a month either for classical 5 fields cron,
# 6fields second repeat form or 7 fields year form
# but still let conversion happen if day field is shifted
(i in [DAY_FIELD, MONTH_FIELD] and len(expressions) == UNIX_CRON_LEN) or
(i in [MONTH_FIELD, DOW_FIELD] and len(expressions) == SECOND_CRON_LEN) or
(i in [DAY_FIELD, MONTH_FIELD, DOW_FIELD] and len(expressions) == YEAR_CRON_LEN)
):
t = cls.LOWMAP[i][t]
t = cls.value_alias(t, i, expressions)

if (
t not in ["*", "l"]
Expand Down
Loading

0 comments on commit 79c0505

Please sign in to comment.