Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multiple exclusion points syntax addition #2244

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 5 additions & 2 deletions doc/src/cylc-user-guide/cug.tex
Original file line number Diff line number Diff line change
Expand Up @@ -2624,8 +2624,11 @@ \subsubsection{Graph Section Headings}
\lstinline=[[[ PT1D!20000101 ]]]= means run daily except on the
first of January 2000.

This syntax can only be used to exclude one datetime from a recurrence. Note
that the \lstinline=^= and \lstinline=$= symbols (shorthand for the initial
This syntax can be used to exclude one or multiple datetimes from a recurrence.
Multiple datetimes are excluded using the syntax
\lstinline=[[[ PT1D!(20000101,20000102,...) ]]]=. All datetimes listed within
the parentheses after the exclamation mark will be excluded. Note that the
\lstinline=^= and \lstinline=$= symbols (shorthand for the initial
and final cycle points) are both datetimes so \lstinline=[[[ T12!$-PT1D ]]]=
is valid.

Expand Down
10 changes: 6 additions & 4 deletions lib/cylc/cycling/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,16 @@ def parse_exclusion(expr):
if count == 0:
return expr, None
elif count > 1:
raise Exception("'%s': only one exclusion per expression "
raise Exception("'%s': only one set of exclusions per expression "
"permitted" % expr)
else:
remainder, exclusion = expr.split('!')
if '/' in exclusion:
remainder, exclusions = expr.split('!')
if '/' in exclusions:
raise Exception("'%s': exclusion must be at the end of the "
"expression" % expr)
return remainder.strip(), exclusion.strip()
exclusions = exclusions.translate(None, ' ()')
exclusions = exclusions.split(',')
return remainder.strip(), exclusions


class CyclerTypeError(TypeError):
Expand Down
74 changes: 57 additions & 17 deletions lib/cylc/cycling/integer.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,8 +263,17 @@ def __init__(self, dep_section, p_context_start, p_context_stop=None):
self.i_offset = IntegerInterval('P0')

matched_recurrence = False

expression, exclusion = parse_exclusion(dep_section)
expression, excl_points = parse_exclusion(dep_section)
# Create a list of multiple exclusion points, if there are any.
if excl_points:
self.exclusions = set()
for excl in excl_points:
self.exclusions.add(get_point_from_expression(
excl,
None,
is_required=False))
else:
self.exclusions = None

for rec, format_num in RECURRENCE_FORMAT_RECS:
results = rec.match(expression)
Expand Down Expand Up @@ -297,12 +306,6 @@ def __init__(self, dep_section, p_context_start, p_context_stop=None):
stop, self.p_context_stop, is_required=end_required)
if intv:
self.i_step = IntegerInterval(intv)
if exclusion:
self.exclusion = get_point_from_expression(exclusion, None,
is_required=False)
else:
self.exclusion = None

if format_num == 3:
# REPEAT/START/PERIOD
if not intv or reps is not None and reps <= 1:
Expand Down Expand Up @@ -417,7 +420,7 @@ def set_offset(self, i_offset):

def is_on_sequence(self, point):
"""Is point on-sequence, disregarding bounds?"""
if self.exclusion and point == self.exclusion:
if self.exclusions and point in self.exclusions:
return False
if self.i_step:
return int(point - self.p_start) % int(self.i_step) == 0
Expand Down Expand Up @@ -452,7 +455,7 @@ def get_prev_point(self, point):
else:
prev_point = point - self.i_step
ret = self._get_point_in_bounds(prev_point)
if self.exclusion and ret == self.exclusion:
if self.exclusions and ret in self.exclusions:
return self.get_prev_point(ret)
return ret

Expand All @@ -468,7 +471,7 @@ def get_nearest_prev_point(self, point):
break
prev_point = sequence_point
sequence_point = self.get_next_point(sequence_point)
if self.exclusion and prev_point == self.exclusion:
if self.exclusions and prev_point in self.exclusions:
return self.get_nearest_prev_point(prev_point)
return prev_point

Expand All @@ -484,7 +487,7 @@ def get_next_point(self, point):
i = int(point - self.p_start) % int(self.i_step)
next_point = point + self.i_step - IntegerInterval.from_integer(i)
ret = self._get_point_in_bounds(next_point)
if self.exclusion and ret and ret == self.exclusion:
if self.exclusions and ret and ret in self.exclusions:
return self.get_next_point(ret)
return ret

Expand All @@ -496,7 +499,7 @@ def get_next_point_on_sequence(self, point):
return None
next_point = point + self.i_step
ret = self._get_point_in_bounds(next_point)
if self.exclusion and ret and ret == self.exclusion:
if self.exclusions and ret and ret in self.exclusions:
return self.get_next_point_on_sequence(ret)
return ret

Expand All @@ -509,19 +512,19 @@ def get_first_point(self, point):
point = self._get_point_in_bounds(point)
else:
point = self.get_next_point(point)
if self.exclusion and point == self.exclusion:
if self.exclusions and point in self.exclusions:
return self.get_next_point_on_sequence(point)
return point

def get_start_point(self):
"""Return the first point in this sequence, or None."""
if self.exclusion and self.p_start == self.exclusion:
if self.exclusions and self.p_start in self.exclusions:
return self.get_next_point_on_sequence(self.p_start)
return self.p_start

def get_stop_point(self):
"""Return the last point in this sequence, or None if unbounded."""
if self.exclusion and self.p_stop == self.exclusion:
if self.exclusions and self.p_stop in self.exclusions:
return self.get_prev_point(self.p_stop)
return self.p_stop

Expand All @@ -534,7 +537,7 @@ def __eq__(self, other):
return self.i_step == other.i_step and \
self.p_start == other.p_start and \
self.p_stop == other.p_stop and \
self.exclusion == other.exclusion
self.exclusions == other.exclusions


def init_from_cfg(cfg):
Expand Down Expand Up @@ -583,6 +586,43 @@ def test_exclusions_simple(self):
point = sequence.get_next_point(point)
self.assertEqual([int(out) for out in output], [1, 2, 4, 5])

def test_multiple_exclusions_simple(self):
"""Tests the multiple exclusion syntax for integer notation"""
sequence = IntegerSequence('R/P1!(2,3,7)', 1, 10)
output = []
point = sequence.get_start_point()
while point:
output.append(point)
point = sequence.get_next_point(point)
self.assertEqual([int(out) for out in output], [1, 4, 5, 6, 8, 9, 10])

def test_multiple_exclusions_extensive(self):
"""Tests IntegerSequence methods for sequences with multi-exclusions"""
points = [IntegerPoint(i) for i in range(10)]
sequence = IntegerSequence('R/P1!(2,3,7)', 1, 10)
self.assertFalse(sequence.is_on_sequence(points[3]))
self.assertFalse(sequence.is_valid(points[3]))
self.assertEqual(sequence.get_prev_point(points[3]), points[1])
self.assertEqual(sequence.get_prev_point(points[4]), points[1])
self.assertEqual(sequence.get_nearest_prev_point(points[3]), points[1])
self.assertEqual(sequence.get_nearest_prev_point(points[4]), points[1])
self.assertEqual(sequence.get_next_point(points[3]), points[4])
self.assertEqual(sequence.get_next_point(points[2]), points[4])
self.assertEqual(sequence.get_next_point_on_sequence(
points[3]),
points[4])
self.assertEqual(sequence.get_next_point_on_sequence(
points[6]),
points[8])

sequence = IntegerSequence('R/P1!(1,3,4)', 1, 10)
self.assertEqual(sequence.get_first_point(points[1]), points[2])
self.assertEqual(sequence.get_first_point(points[0]), points[2])
self.assertEqual(sequence.get_start_point(), points[2])

sequence = IntegerSequence('R/P1!(8,9,10)', 1, 10)
self.assertEqual(sequence.get_stop_point(), points[7])

def test_exclusions_extensive(self):
"""Test IntegerSequence methods for sequences with exclusions."""
point_0 = IntegerPoint(0)
Expand Down