Skip to content

Commit

Permalink
cylc#887: update isodatetime
Browse files Browse the repository at this point in the history
  • Loading branch information
benfitzpatrick committed Feb 20, 2014
1 parent 4065c6a commit 40fc122
Show file tree
Hide file tree
Showing 10 changed files with 3,299 additions and 2,538 deletions.
171 changes: 78 additions & 93 deletions lib/cylc/time_parser.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env python

# -*- coding: utf-8 -*-
#C: THIS FILE IS PART OF THE CYLC SUITE ENGINE.
#C: Copyright (C) 2008-2014 Hilary Oliver, NIWA
#C:
Expand Down Expand Up @@ -35,7 +35,8 @@
import unittest

import cylc.CylcError
import isodatetime
import isodatetime.data
import isodatetime.parsers


class CylcTimeSyntaxError(cylc.CylcError.CylcError):
Expand All @@ -51,7 +52,7 @@ class CylcTimeParser(object):
context_start_point describes the beginning date/time used for
extrapolating incomplete date/time syntax (usually initial
cycle time). It is either a date/time string in full ISO 8601
syntax or an isodatetime.TimePoint object.
syntax or an isodatetime.data.TimePoint object.
context_end_point is the same as context_start_point, but describes
a final time - e.g. the final cycle time.
Expand Down Expand Up @@ -80,12 +81,18 @@ class CylcTimeParser(object):
re.compile("W-\dT")]}

def __init__(self, context_start_point,
context_end_point):
self.timepoint_parser = isodatetime.TimePointParser(
allow_only_basic=True,
allow_truncated=True,
num_expanded_year_digits=2,
format_function=self.custom_format_timepoint)
context_end_point, num_expanded_year_digits=0,
timezone_format="Z"):
if num_expanded_year_digits == 0:
dump_format = "CCYYMMDDThhmm" + timezone_format
else:
dump_format = u"±XCCYYMMDDThhmm" + timezone_format
self.num_expanded_year_digits = num_expanded_year_digits
self.timepoint_parser = isodatetime.parsers.TimePointParser(
allow_only_basic=True,
allow_truncated=True,
num_expanded_year_digits=num_expanded_year_digits,
dump_format=dump_format)
if isinstance(context_start_point, basestring):
context_start_point = self.timepoint_parser.parse(
context_start_point)
Expand All @@ -98,8 +105,8 @@ def __init__(self, context_start_point,
for regex, format_num in self.RECURRENCE_FORMAT_REGEXES:
self._recur_format_recs.append((re.compile(regex), format_num))
self._offset_rec = re.compile(self.OFFSET_REGEX)
self.timeinterval_parser = isodatetime.TimeIntervalParser()
self.recurrence_parser = isodatetime.TimeRecurrenceParser(
self.timeinterval_parser = isodatetime.parsers.TimeIntervalParser()
self.recurrence_parser = isodatetime.parsers.TimeRecurrenceParser(
timepoint_parser=self.timepoint_parser)

def parse_interval(self, expr):
Expand All @@ -111,11 +118,11 @@ def parse_timepoint(self, expr, context_point=None):
expression should be a string such as 20010205T00Z, or a
truncated/abbreviated format string such as T06 or -P2Y.
context_point should be an isodatetime.TimePoint object that
supplies the missing information for truncated expressions. For
example, context_point should be the task cycle time for
inter-cycle dependency expressions. If context_point is None,
self.context_start_point is used.
context_point should be an isodatetime.data.TimePoint object
that supplies the missing information for truncated
expressions. For example, context_point should be the task
cycle time for inter-cycle dependency expressions. If
context_point is None, self.context_start_point is used.
"""
if context_point is None:
Expand Down Expand Up @@ -181,7 +188,7 @@ def parse_recurrence(self, expression,
end_point += context_end_point
if end_offset is not None:
end_point += end_offset
return isodatetime.TimeRecurrence(
return isodatetime.data.TimeRecurrence(
repetitions=repetitions,
start_point=start_point,
interval=interval,
Expand All @@ -190,44 +197,6 @@ def parse_recurrence(self, expression,
max_point=context_end_point)
raise CylcTimeSyntaxError("Could not parse %s" % expression)

def custom_format_timepoint(self, timepoint):
"""Return a custom-format string representation of timepoint."""
if timepoint.truncated:
return timepoint.__str__(override_custom=True)
date_info = timepoint.get_calendar_date()
if date_info is None:
date_string = "-"
else:
year, month, day_of_month = date_info
if timepoint.expanded_year_digits:
year_string = "%06d" % abs(year)
if year < 0:
year_string = "-" + year_string
else:
year_string = "+" + year_string
elif year < 0:
raise ValueError(("Year %s " % year) + "cannot be represented " +
"unless expanded format is used")
else:
year_string = "%04d" % year
date_string = year_string + "%02d%02d" % (month, day_of_month)

hour, minute, second = timepoint.get_hour_minute_second()
# We ignore seconds.
time_string = "%02d%02d" % (hour, minute)
time_zone = timepoint.get_time_zone()
if time_zone.hours == 0 and time_zone.minutes == 0:
time_zone_string = "Z"
else:
sign_string = "+"
if time_zone.hours < 0:
sign_string = "-"
if time_zone.hours == 0 and time_zone.minutes < 0:
sign_string = "-"
time_zone_string = sign_string + "%02d%02d"
time_zone_string %= (abs(time_zone.hours), abs(time_zone.minutes))
return date_string + "T" + time_string + time_zone_string

def _get_interval_from_expression(self, expr, context=None):
if expr is None:
if context is None or not context.truncated:
Expand All @@ -252,7 +221,7 @@ def _get_interval_from_expression(self, expr, context=None):
kwargs = {"minutes": 1}
if not kwargs:
return None
return isodatetime.TimeInterval(**kwargs)
return isodatetime.data.TimeInterval(**kwargs)
return self.timeinterval_parser.parse(expr)

def _get_point_from_expression(self, expr, context, is_required=False):
Expand All @@ -276,19 +245,25 @@ def _get_point_from_expression(self, expr, context, is_required=False):
if expr.endswith("T"):
expr_to_parse = expr + "00"
try:
print "try to parse as timepoint:", expr_to_parse
expr_point = self.timepoint_parser.parse(expr_to_parse)
except isodatetime.TimeSyntaxError:
except isodatetime.parsers.ISOSyntaxError:
print "... no go"
pass
else:
print " go"
return expr_point, expr_offset
for truncation_string, recs in self.TRUNCATED_REC_MAP.items():
for rec in recs:
if rec.search(expr):
try:
print "try to parse as timepoint:", truncation_string + expr_to_parse
expr_point = self.timepoint_parser.parse(
truncation_string + expr_to_parse)
except isodatetime.TimeSyntaxError:
except isodatetime.parsers.ISOSyntaxError:
print "... no go"
continue
print " go"
return expr_point, expr_offset
raise CylcTimeSyntaxError(
("'%s': not a valid cylc-shorthand or full " % expr) +
Expand All @@ -302,32 +277,40 @@ class TestRecurrenceSuite(unittest.TestCase):
def setUp(self):
self._start_point = "19991226T0930Z"
self._end_point = "20010506T1200+0200"
self._parser = CylcTimeParser(self._start_point,
self._end_point)
self._parsers = {0: CylcTimeParser(self._start_point,
self._end_point),
2: CylcTimeParser(self._start_point,
self._end_point,
num_expanded_year_digits=2)}

def test_first_recurrence_format(self):
"""Test the first ISO 8601 recurrence format."""
tests = [("R5/T06/04T1230-0300",
0,
"R5/19991227T0600Z/20010604T1530Z",
["19991227T0600Z", "20000506T1422Z",
"20000914T2245Z", "20010124T0707Z"]),
("R2/-0450000101T04Z/+0020630405T2200-05",
2,
"R2/-0450000101T0400Z/+0020630406T0300Z",
[]),
("R10/T12+P10D/-P1Y",
0,
"R10/20000105T1200Z/20000506T1000Z",
["20000105T1200Z", "20000119T0106Z",
"20000201T1413Z", "20000215T0320Z",
"20000228T1626Z", "20000313T0533Z",
"20000326T1840Z", "20000409T0746Z",
"20000422T2053Z", "20000506T1000Z"])]
for test in tests:
if len(test) == 2:
expression, ctrl_data = test
if len(test) == 3:
expression, num_expanded_year_digits, ctrl_data = test
ctrl_results = None
else:
expression, ctrl_data, ctrl_results = test
recurrence = self._parser.parse_recurrence(expression)
expression, num_expanded_year_digits, ctrl_data = test[:3]
ctrl_results = test[3]
parser = self._parsers[num_expanded_year_digits]
recurrence = parser.parse_recurrence(expression)
test_data = str(recurrence)
self.assertEqual(test_data, ctrl_data)
if ctrl_results is None:
Expand Down Expand Up @@ -379,7 +362,7 @@ def test_third_recurrence_format(self):
ctrl_results = None
else:
expression, ctrl_data, ctrl_results = test
recurrence = self._parser.parse_recurrence(expression)
recurrence = self._parsers[0].parse_recurrence(expression)
test_data = str(recurrence)
self.assertEqual(test_data, ctrl_data)
if ctrl_results is None:
Expand All @@ -393,27 +376,28 @@ def test_third_recurrence_format(self):
def test_fourth_recurrence_format(self):
"""Test the fourth ISO 8601 recurrence format."""
tests = [("PT6H/20000101T0500Z", "R/PT6H/20000101T0500Z"),
("P12D/+P2W", "R/P12D/20010520T1200+0200"),
("P1W/-P1M1D", "R/P1W/20010405T1200+0200"),
("P6D/T12", "R/P6D/20010506T1200+0200"),
("P6DT12H/01T", "R/P6DT12H/20010601T0000+0200"),
("R/P1D/20010506T1200+0200", "R/P1D/20010506T1200+0200"),
("R/PT5M/+PT2M", "R/PT5M/20010506T1202+0200"),
("R/P20Y/-P20Y", "R/P20Y/19810506T1200+0200"),
("R/P3YT2H/T18-02", "R/P3YT2H/20010506T2200+0200"),
("R/PT3H/31T", "R/PT3H/20010531T0000+0200"),
("R5/P1Y/", "R5/P1Y/20010506T1200+0200"),
("R3/P2Y/02T", "R3/P2Y/20010602T0000+0200"),
("R/P2Y", "R/P2Y/20010506T1200+0200"),
("R48/PT2H", "R48/PT2H/20010506T1200+0200"),
("R/P21Y/", "R/P21Y/20010506T1200+0200")]
("P12D/+P2W", "R/P12D/20010520T1000Z"),
("P1W/-P1M1D", "R/P1W/20010405T1000Z"),
("P6D/T12", "R/P6D/20010506T1000Z"),
("P6DT12H/01T", "R/P6DT12H/20010531T2200Z"),
("R/P1D/20010506T1200+0200", "R/P1D/20010506T1000Z"),
("R/PT5M/+PT2M", "R/PT5M/20010506T1002Z"),
("R/P20Y/-P20Y", "R/P20Y/19810506T1000Z"),
("R/P3YT2H/T18-02", "R/P3YT2H/20010506T2000Z"),
("R/PT3H/31T", "R/PT3H/20010530T2200Z"),
("R5/P1Y/", "R5/P1Y/20010506T1000Z"),
("R3/P2Y/02T", "R3/P2Y/20010601T2200Z"),
("R/P2Y", "R/P2Y/20010506T1000Z"),
("R48/PT2H", "R48/PT2H/20010506T1000Z"),
("R/P21Y/", "R/P21Y/20010506T1000Z")]
for test in tests:
if len(test) == 2:
expression, ctrl_data = test
ctrl_results = None
else:
expression, ctrl_data, ctrl_results = test
recurrence = self._parser.parse_recurrence(expression)
recurrence = self._parsers[0].parse_recurrence(expression)
print expression
test_data = str(recurrence)
self.assertEqual(test_data, ctrl_data)
if ctrl_results is None:
Expand All @@ -426,19 +410,20 @@ def test_fourth_recurrence_format(self):

def test_inter_cycle_timepoints(self):
"""Test the inter-cycle timepoint parsing."""
task_cycle_time = self._parser.parse_timepoint(
task_cycle_time = self._parsers[0].parse_timepoint(
"20000101T00Z")
tests = [("T06", "20000101T0600Z"),
("-PT6H", "19991231T1800Z"),
("+P5Y2M", "20050301T0000Z"),
("0229T", "20000229T0000Z"),
("+P54D", "20000224T0000Z"),
("T12+P5W", "20000205T1200Z"),
("-P1Y", "19990101T0000Z"),
("-9999990101T00Z", "-9999990101T0000Z"),
("20050601T2359+0200", "20050601T2359+0200")]
for expression, ctrl_data in tests:
test_data = str(self._parser.parse_timepoint(
tests = [("T06", "20000101T0600Z", 0),
("-PT6H", "19991231T1800Z", 0),
("+P5Y2M", "20050301T0000Z", 0),
("0229T", "20000229T0000Z", 0),
("+P54D", "20000224T0000Z", 0),
("T12+P5W", "20000205T1200Z", 0),
("-P1Y", "19990101T0000Z", 0),
("-9999990101T00Z", "-9999990101T0000Z", 2),
("20050601T2359+0200", "20050601T2159Z", 0)]
for expression, ctrl_data, num_expanded_year_digits in tests:
parser = self._parsers[num_expanded_year_digits]
test_data = str(parser.parse_timepoint(
expression,
context_point=task_cycle_time))
self.assertEqual(test_data, ctrl_data)
Expand All @@ -449,7 +434,7 @@ def test_interval(self):
"P5W", "PT12M2S", "PT65S",
"PT2M", "P1YT567,4M"]
for expression in tests:
test_data = str(self._parser.parse_interval(expression))
test_data = str(self._parsers[0].parse_interval(expression))
self.assertEqual(test_data, expression)


Expand Down
Loading

0 comments on commit 40fc122

Please sign in to comment.