aboutsummaryrefslogtreecommitdiff
path: root/.venv/lib/python3.12/site-packages/orgparse/date.py
diff options
context:
space:
mode:
authorS. Solomon Darnell2025-03-28 21:52:21 -0500
committerS. Solomon Darnell2025-03-28 21:52:21 -0500
commit4a52a71956a8d46fcb7294ac71734504bb09bcc2 (patch)
treeee3dc5af3b6313e921cd920906356f5d4febc4ed /.venv/lib/python3.12/site-packages/orgparse/date.py
parentcc961e04ba734dd72309fb548a2f97d67d578813 (diff)
downloadgn-ai-master.tar.gz
two version of R2R are hereHEADmaster
Diffstat (limited to '.venv/lib/python3.12/site-packages/orgparse/date.py')
-rw-r--r--.venv/lib/python3.12/site-packages/orgparse/date.py717
1 files changed, 717 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/orgparse/date.py b/.venv/lib/python3.12/site-packages/orgparse/date.py
new file mode 100644
index 00000000..dd407b78
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/orgparse/date.py
@@ -0,0 +1,717 @@
+import datetime
+import re
+from typing import Union, Tuple, Optional, List
+
+DateIsh = Union[datetime.date, datetime.datetime]
+
+
+def total_seconds(td):
+ """Equivalent to `datetime.timedelta.total_seconds`."""
+ return float(td.microseconds +
+ (td.seconds + td.days * 24 * 3600) * 10 ** 6) / 10 ** 6
+
+
+def total_minutes(td):
+ """Alias for ``total_seconds(td) / 60``."""
+ return total_seconds(td) / 60
+
+
+def gene_timestamp_regex(brtype, prefix=None, nocookie=False):
+ """
+ Generate timestamp regex for active/inactive/nobrace brace type
+
+ :type brtype: {'active', 'inactive', 'nobrace'}
+ :arg brtype:
+ It specifies a type of brace.
+ active: <>-type; inactive: []-type; nobrace: no braces.
+
+ :type prefix: str or None
+ :arg prefix:
+ It will be appended to the head of keys of the "groupdict".
+ For example, if prefix is ``'active_'`` the groupdict has
+ keys such as ``'active_year'``, ``'active_month'``, and so on.
+ If it is None it will be set to ``brtype`` + ``'_'``.
+
+ :type nocookie: bool
+ :arg nocookie:
+ Cookie part (e.g., ``'-3d'`` or ``'+6m'``) is not included if
+ it is ``True``. Default value is ``False``.
+
+ >>> timestamp_re = re.compile(
+ ... gene_timestamp_regex('active', prefix=''),
+ ... re.VERBOSE)
+ >>> timestamp_re.match('no match') # returns None
+ >>> m = timestamp_re.match('<2010-06-21 Mon>')
+ >>> m.group()
+ '<2010-06-21 Mon>'
+ >>> '{year}-{month}-{day}'.format(**m.groupdict())
+ '2010-06-21'
+ >>> m = timestamp_re.match('<2005-10-01 Sat 12:30 +7m -3d>')
+ >>> from collections import OrderedDict
+ >>> sorted(m.groupdict().items())
+ ... # doctest: +NORMALIZE_WHITESPACE
+ [('day', '01'),
+ ('end_hour', None), ('end_min', None),
+ ('hour', '12'), ('min', '30'),
+ ('month', '10'),
+ ('repeatdwmy', 'm'), ('repeatnum', '7'), ('repeatpre', '+'),
+ ('warndwmy', 'd'), ('warnnum', '3'), ('warnpre', '-'), ('year', '2005')]
+
+ When ``brtype = 'nobrace'``, cookie part cannot be retrieved.
+
+ >>> timestamp_re = re.compile(
+ ... gene_timestamp_regex('nobrace', prefix=''),
+ ... re.VERBOSE)
+ >>> timestamp_re.match('no match') # returns None
+ >>> m = timestamp_re.match('2010-06-21 Mon')
+ >>> m.group()
+ '2010-06-21'
+ >>> '{year}-{month}-{day}'.format(**m.groupdict())
+ '2010-06-21'
+ >>> m = timestamp_re.match('2005-10-01 Sat 12:30 +7m -3d')
+ >>> sorted(m.groupdict().items())
+ ... # doctest: +NORMALIZE_WHITESPACE
+ [('day', '01'),
+ ('end_hour', None), ('end_min', None),
+ ('hour', '12'), ('min', '30'),
+ ('month', '10'), ('year', '2005')]
+ """
+
+ if brtype == 'active':
+ (bo, bc) = ('<', '>')
+ elif brtype == 'inactive':
+ (bo, bc) = (r'\[', r'\]')
+ elif brtype == 'nobrace':
+ (bo, bc) = ('', '')
+ else:
+ raise ValueError("brtype='{0!r}' is invalid".format(brtype))
+
+ if brtype == 'nobrace':
+ ignore = r'[\s\w]'
+ else:
+ ignore = '[^{bc}]'.format(bc=bc)
+
+ if prefix is None:
+ prefix = '{0}_'.format(brtype)
+
+ regex_date_time = r"""
+ (?P<{prefix}year>\d{{4}}) -
+ (?P<{prefix}month>\d{{2}}) -
+ (?P<{prefix}day>\d{{2}})
+ ( # optional time field
+ ({ignore}+?)
+ (?P<{prefix}hour>\d{{2}}) :
+ (?P<{prefix}min>\d{{2}})
+ ( # optional end time range
+ --?
+ (?P<{prefix}end_hour>\d{{2}}) :
+ (?P<{prefix}end_min>\d{{2}})
+ )?
+ )?
+ """
+ regex_cookie = r"""
+ ( # optional repeater
+ ({ignore}+?)
+ (?P<{prefix}repeatpre> [\.\+]{{1,2}})
+ (?P<{prefix}repeatnum> \d+)
+ (?P<{prefix}repeatdwmy> [hdwmy])
+ )?
+ ( # optional warning
+ ({ignore}+?)
+ (?P<{prefix}warnpre> \-)
+ (?P<{prefix}warnnum> \d+)
+ (?P<{prefix}warndwmy> [hdwmy])
+ )?
+ """
+ # http://www.pythonregex.com/
+ regex = ''.join([
+ bo,
+ regex_date_time,
+ regex_cookie if nocookie or brtype != 'nobrace' else '',
+ '({ignore}*?)',
+ bc])
+ return regex.format(prefix=prefix, ignore=ignore)
+
+
+def date_time_format(date) -> str:
+ """
+ Format a date or datetime in default org format
+
+ @param date The date
+
+ @return Formatted date(time)
+ """
+ default_format_date = "%Y-%m-%d %a"
+ default_format_datetime = "%Y-%m-%d %a %H:%M"
+ is_datetime = isinstance(date, datetime.datetime)
+
+ return date.strftime(default_format_datetime if is_datetime else default_format_date)
+
+
+def is_same_day(date0, date1) -> bool:
+ """
+ Check if two dates or datetimes are on the same day
+ """
+ return (OrgDate._date_to_tuple(date0)[:3] == OrgDate._date_to_tuple(date1)[:3])
+
+
+TIMESTAMP_NOBRACE_RE = re.compile(
+ gene_timestamp_regex('nobrace', prefix=''),
+ re.VERBOSE)
+
+TIMESTAMP_RE = re.compile(
+ '|'.join((gene_timestamp_regex('active'),
+ gene_timestamp_regex('inactive'))),
+ re.VERBOSE)
+
+
+class OrgDate(object):
+
+ _active_default = True
+ """
+ The default active value.
+
+ When the `active` argument to ``__init__`` is ``None``,
+ This value will be used.
+
+ """
+
+ """
+ When formatting the date to string via __str__, and there is an end date on
+ the same day as the start date, allow formatting in the short syntax
+ <2021-09-03 Fri 16:01--17:30>? Otherwise the string represenation would be
+ <2021-09-03 Fri 16:01>--<2021-09-03 Fri 17:30>
+ """
+ _allow_short_range = True
+
+ def __init__(self, start, end=None, active=None, repeater=None,
+ warning=None):
+ """
+ Create :class:`OrgDate` object
+
+ :type start: datetime, date, tuple, int, float or None
+ :type end: datetime, date, tuple, int, float or None
+ :arg start: Starting date.
+ :arg end: Ending date.
+
+ :type active: bool or None
+ :arg active: Active/inactive flag.
+ None means using its default value, which
+ may be different for different subclasses.
+ :type repeater: tuple or None
+ :arg repeater: Repeater interval.
+ :type warning: tuple or None
+ :arg warning: Deadline warning interval.
+
+ >>> OrgDate(datetime.date(2012, 2, 10))
+ OrgDate((2012, 2, 10))
+ >>> OrgDate((2012, 2, 10))
+ OrgDate((2012, 2, 10))
+ >>> OrgDate((2012, 2)) #doctest: +NORMALIZE_WHITESPACE
+ Traceback (most recent call last):
+ ...
+ ValueError: Automatic conversion to the datetime object
+ requires at least 3 elements in the tuple.
+ Only 2 elements are in the given tuple '(2012, 2)'.
+ >>> OrgDate((2012, 2, 10, 12, 20, 30))
+ OrgDate((2012, 2, 10, 12, 20, 30))
+ >>> OrgDate((2012, 2, 10), (2012, 2, 15), active=False)
+ OrgDate((2012, 2, 10), (2012, 2, 15), False)
+
+ OrgDate can be created using unix timestamp:
+
+ >>> OrgDate(datetime.datetime.fromtimestamp(0)) == OrgDate(0)
+ True
+
+ """
+ self._start = self._to_date(start)
+ self._end = self._to_date(end)
+ self._active = self._active_default if active is None else active
+ # repeater and warning are tuples of (prefix, number, interval)
+ self._repeater = repeater
+ self._warning = warning
+
+ @staticmethod
+ def _to_date(date) -> DateIsh:
+ if isinstance(date, (tuple, list)):
+ if len(date) == 3:
+ return datetime.date(*date)
+ elif len(date) > 3:
+ return datetime.datetime(*date)
+ else:
+ raise ValueError(
+ "Automatic conversion to the datetime object "
+ "requires at least 3 elements in the tuple. "
+ "Only {0} elements are in the given tuple '{1}'."
+ .format(len(date), date))
+ elif isinstance(date, (int, float)):
+ return datetime.datetime.fromtimestamp(date)
+ else:
+ return date
+
+ @staticmethod
+ def _date_to_tuple(date):
+ if isinstance(date, datetime.datetime):
+ return tuple(date.timetuple()[:6])
+ elif isinstance(date, datetime.date):
+ return tuple(date.timetuple()[:3])
+
+ def __repr__(self):
+ args = [
+ self.__class__.__name__,
+ self._date_to_tuple(self.start),
+ self._date_to_tuple(self.end) if self.has_end() else None,
+ None if self._active is self._active_default else self._active,
+ self._repeater,
+ self._warning,
+ ]
+ while args[-1] is None:
+ args.pop()
+ if len(args) > 3 and args[3] is None:
+ args[3] = self._active_default
+ return '{0}({1})'.format(args[0], ', '.join(map(repr, args[1:])))
+
+ def __str__(self):
+ fence = ("<", ">") if self.is_active() else ("[", "]")
+
+ start = date_time_format(self.start)
+ end = None
+
+ if self.has_end():
+ if self._allow_short_range and is_same_day(self.start, self.end):
+ start += "--%s" % self.end.strftime("%H:%M")
+ else:
+ end = date_time_format(self.end)
+
+ if self._repeater:
+ start += " %s%d%s" % self._repeater
+ if self._warning:
+ start += " %s%d%s" % self._warning
+ ret = "%s%s%s" % (fence[0], start, fence[1])
+ if end:
+ ret += "--%s%s%s" % (fence[0], end, fence[1])
+
+ return ret
+
+ def __nonzero__(self):
+ return bool(self._start)
+
+ __bool__ = __nonzero__ # PY3
+
+ def __eq__(self, other):
+ if (isinstance(other, OrgDate) and
+ self._start is None and
+ other._start is None):
+ return True
+ return (isinstance(other, self.__class__) and
+ self._start == other._start and
+ self._end == other._end and
+ self._active == other._active)
+
+ @property
+ def start(self):
+ """
+ Get date or datetime object
+
+ >>> OrgDate((2012, 2, 10)).start
+ datetime.date(2012, 2, 10)
+ >>> OrgDate((2012, 2, 10, 12, 10)).start
+ datetime.datetime(2012, 2, 10, 12, 10)
+
+ """
+ return self._start
+
+ @property
+ def end(self):
+ """
+ Get date or datetime object
+
+ >>> OrgDate((2012, 2, 10), (2012, 2, 15)).end
+ datetime.date(2012, 2, 15)
+ >>> OrgDate((2012, 2, 10, 12, 10), (2012, 2, 15, 12, 10)).end
+ datetime.datetime(2012, 2, 15, 12, 10)
+
+ """
+ return self._end
+
+ def is_active(self) -> bool:
+ """Return true if the date is active"""
+ return self._active
+
+ def has_end(self) -> bool:
+ """Return true if it has the end date"""
+ return bool(self._end)
+
+ def has_time(self) -> bool:
+ """
+ Return true if the start date has time field
+
+ >>> OrgDate((2012, 2, 10)).has_time()
+ False
+ >>> OrgDate((2012, 2, 10, 12, 10)).has_time()
+ True
+
+ """
+ return isinstance(self._start, datetime.datetime)
+
+ def has_overlap(self, other) -> bool:
+ """
+ Test if it has overlap with other :class:`OrgDate` instance
+
+ If the argument is not an instance of :class:`OrgDate`, it is
+ converted to :class:`OrgDate` instance by ``OrgDate(other)``
+ first.
+
+ >>> od = OrgDate((2012, 2, 10), (2012, 2, 15))
+ >>> od.has_overlap(OrgDate((2012, 2, 11)))
+ True
+ >>> od.has_overlap(OrgDate((2012, 2, 20)))
+ False
+ >>> od.has_overlap(OrgDate((2012, 2, 11), (2012, 2, 20)))
+ True
+ >>> od.has_overlap((2012, 2, 11))
+ True
+
+ """
+ if not isinstance(other, OrgDate):
+ other = OrgDate(other)
+ if self.has_end():
+ return (self._datetime_in_range(other.start) or
+ self._datetime_in_range(other.end))
+ elif other.has_end():
+ return other._datetime_in_range(self.start)
+ elif self.start == other.get_start:
+ return True
+ else:
+ return False
+
+ def _datetime_in_range(self, date):
+ if not isinstance(date, (datetime.datetime, datetime.date)):
+ return False
+ asdt = self._as_datetime
+ if asdt(self.start) <= asdt(date) <= asdt(self.end):
+ return True
+ return False
+
+ @staticmethod
+ def _as_datetime(date) -> datetime.datetime:
+ """
+ Convert the given date into datetime (if it already is, return it
+ unmodified
+ """
+ if not isinstance(date, datetime.datetime):
+ return datetime.datetime(*date.timetuple()[:3])
+ return date
+
+ @staticmethod
+ def _daterange_from_groupdict(dct, prefix='') -> Tuple[List, Optional[List]]:
+ start_keys = ['year', 'month', 'day', 'hour' , 'min']
+ end_keys = ['year', 'month', 'day', 'end_hour', 'end_min']
+ start_range = list(map(int, filter(None, (dct[prefix + k] for k in start_keys))))
+ end_range: Optional[List]
+ end_range = list(map(int, filter(None, (dct[prefix + k] for k in end_keys))))
+ if len(end_range) < len(end_keys):
+ end_range = None
+ return (start_range, end_range)
+
+ @classmethod
+ def _datetuple_from_groupdict(cls, dct, prefix=''):
+ return cls._daterange_from_groupdict(dct, prefix=prefix)[0]
+
+ @classmethod
+ def list_from_str(cls, string: str) -> List['OrgDate']:
+ """
+ Parse string and return a list of :class:`OrgDate` objects
+
+ >>> OrgDate.list_from_str("... <2012-02-10 Fri> and <2012-02-12 Sun>")
+ [OrgDate((2012, 2, 10)), OrgDate((2012, 2, 12))]
+ >>> OrgDate.list_from_str("<2012-02-10 Fri>--<2012-02-12 Sun>")
+ [OrgDate((2012, 2, 10), (2012, 2, 12))]
+ >>> OrgDate.list_from_str("<2012-02-10 Fri>--[2012-02-12 Sun]")
+ [OrgDate((2012, 2, 10)), OrgDate((2012, 2, 12), None, False)]
+ >>> OrgDate.list_from_str("this is not timestamp")
+ []
+ >>> OrgDate.list_from_str("<2012-02-11 Sat 10:11--11:20>")
+ [OrgDate((2012, 2, 11, 10, 11, 0), (2012, 2, 11, 11, 20, 0))]
+ """
+ cookie_suffix = ['pre', 'num', 'dwmy']
+ match = TIMESTAMP_RE.search(string)
+ if match:
+ rest = string[match.end():]
+ mdict = match.groupdict()
+ if mdict['active_year']:
+ prefix = 'active_'
+ active = True
+ rangedash = '--<'
+ else:
+ prefix = 'inactive_'
+ active = False
+ rangedash = '--['
+ repeater: Optional[Tuple[str, int, str]] = None
+ warning: Optional[Tuple[str, int, str]] = None
+ if mdict[prefix + 'repeatpre'] is not None:
+ keys = [prefix + 'repeat' + suffix for suffix in cookie_suffix]
+ values = [mdict[k] for k in keys]
+ repeater = (values[0], int(values[1]), values[2])
+ if mdict[prefix + 'warnpre'] is not None:
+ keys = [prefix + 'warn' + suffix for suffix in cookie_suffix]
+ values = [mdict[k] for k in keys]
+ warning = (values[0], int(values[1]), values[2])
+ has_rangedash = rest.startswith(rangedash)
+ match2 = TIMESTAMP_RE.search(rest) if has_rangedash else None
+ if has_rangedash and match2:
+ rest = rest[match2.end():]
+ # no need for check activeness here because of the rangedash
+ mdict2 = match2.groupdict()
+ odate = cls(
+ cls._datetuple_from_groupdict(mdict, prefix),
+ cls._datetuple_from_groupdict(mdict2, prefix),
+ active=active, repeater=repeater, warning=warning)
+ else:
+ odate = cls(
+ *cls._daterange_from_groupdict(mdict, prefix),
+ active=active, repeater=repeater, warning=warning)
+ return [odate] + cls.list_from_str(rest)
+ else:
+ return []
+
+ @classmethod
+ def from_str(cls, string):
+ """
+ Parse string and return an :class:`OrgDate` objects.
+
+ >>> OrgDate.from_str('2012-02-10 Fri')
+ OrgDate((2012, 2, 10))
+ >>> OrgDate.from_str('2012-02-10 Fri 12:05')
+ OrgDate((2012, 2, 10, 12, 5, 0))
+
+ """
+ match = cls._from_str_re.match(string)
+ if match:
+ mdict = match.groupdict()
+ return cls(cls._datetuple_from_groupdict(mdict),
+ active=cls._active_default)
+ else:
+ return cls(None)
+
+ _from_str_re = TIMESTAMP_NOBRACE_RE
+
+
+def compile_sdc_re(sdctype):
+ brtype = 'inactive' if sdctype == 'CLOSED' else 'active'
+ return re.compile(
+ r'^(?!\#).*{0}:\s+{1}'.format(
+ sdctype,
+ gene_timestamp_regex(brtype, prefix='', nocookie=True)),
+ re.VERBOSE)
+
+
+class OrgDateSDCBase(OrgDate):
+
+ _re = None # override this!
+
+ # FIXME: use OrgDate.from_str
+ @classmethod
+ def from_str(cls, string):
+ rgx = cls._re
+ assert rgx is not None
+ match = rgx.search(string)
+ if match:
+ mdict = match.groupdict()
+ start = cls._datetuple_from_groupdict(mdict)
+ end = None
+ end_hour = mdict['end_hour']
+ end_min = mdict['end_min']
+ if end_hour is not None and end_min is not None:
+ end_dict = {}
+ end_dict.update(mdict)
+ end_dict.update({'hour': end_hour, 'min': end_min})
+ end = cls._datetuple_from_groupdict(end_dict)
+ cookie_suffix = ['pre', 'num', 'dwmy']
+ repeater: Optional[Tuple[str, int, str]] = None
+ warning: Optional[Tuple[str, int, str]] = None
+ prefix = ''
+ if mdict[prefix + 'repeatpre'] is not None:
+ keys = [prefix + 'repeat' + suffix for suffix in cookie_suffix]
+ values = [mdict[k] for k in keys]
+ repeater = (values[0], int(values[1]), values[2])
+ if mdict[prefix + 'warnpre'] is not None:
+ keys = [prefix + 'warn' + suffix for suffix in cookie_suffix]
+ values = [mdict[k] for k in keys]
+ warning = (values[0], int(values[1]), values[2])
+ return cls(start, end, active=cls._active_default,
+ repeater=repeater, warning=warning)
+ else:
+ return cls(None)
+
+
+class OrgDateScheduled(OrgDateSDCBase):
+ """Date object to represent SCHEDULED attribute."""
+ _re = compile_sdc_re('SCHEDULED')
+ _active_default = True
+
+
+class OrgDateDeadline(OrgDateSDCBase):
+ """Date object to represent DEADLINE attribute."""
+ _re = compile_sdc_re('DEADLINE')
+ _active_default = True
+
+
+class OrgDateClosed(OrgDateSDCBase):
+ """Date object to represent CLOSED attribute."""
+ _re = compile_sdc_re('CLOSED')
+ _active_default = False
+
+
+def parse_sdc(string):
+ return (OrgDateScheduled.from_str(string),
+ OrgDateDeadline.from_str(string),
+ OrgDateClosed.from_str(string))
+
+
+class OrgDateClock(OrgDate):
+
+ """
+ Date object to represent CLOCK attributes.
+
+ >>> OrgDateClock.from_str(
+ ... 'CLOCK: [2010-08-08 Sun 17:00]--[2010-08-08 Sun 17:30] => 0:30')
+ OrgDateClock((2010, 8, 8, 17, 0, 0), (2010, 8, 8, 17, 30, 0))
+
+ """
+
+ _active_default = False
+
+ _allow_short_range = False
+
+ def __init__(self, start, end=None, duration=None, active=None):
+ """
+ Create OrgDateClock object
+ """
+ super(OrgDateClock, self).__init__(start, end, active=active)
+ self._duration = duration
+
+ @property
+ def duration(self):
+ """
+ Get duration of CLOCK.
+
+ >>> duration = OrgDateClock.from_str(
+ ... 'CLOCK: [2010-08-08 Sun 17:00]--[2010-08-08 Sun 17:30] => 0:30'
+ ... ).duration
+ >>> duration.seconds
+ 1800
+ >>> total_minutes(duration)
+ 30.0
+
+ """
+ return self.end - self.start
+
+ def is_duration_consistent(self):
+ """
+ Check duration value of CLOCK line.
+
+ >>> OrgDateClock.from_str(
+ ... 'CLOCK: [2010-08-08 Sun 17:00]--[2010-08-08 Sun 17:30] => 0:30'
+ ... ).is_duration_consistent()
+ True
+ >>> OrgDateClock.from_str(
+ ... 'CLOCK: [2010-08-08 Sun 17:00]--[2010-08-08 Sun 17:30] => 0:15'
+ ... ).is_duration_consistent()
+ False
+
+ """
+ return (self._duration is None or
+ self._duration == total_minutes(self.duration))
+
+ @classmethod
+ def from_str(cls, line: str) -> 'OrgDateClock':
+ """
+ Get CLOCK from given string.
+
+ Return three tuple (start, stop, length) which is datetime object
+ of start time, datetime object of stop time and length in minute.
+
+ """
+ match = cls._re.search(line)
+ if not match:
+ return cls(None, None)
+
+ ymdhm1 = [int(d) for d in match.groups()[:5]]
+
+ # second part starting with "--", does not exist for open clock dates
+ has_end = bool(match.group(6))
+ ymdhm2_dt: Optional[datetime.datetime]
+ len_min: Optional[int]
+ if has_end:
+ ymdhm2 = [int(d) for d in match.groups()[6:11]]
+ hm3 = [int(d) for d in match.groups()[11:]]
+
+ ymdhm2_dt = datetime.datetime(*ymdhm2) # type: ignore[arg-type]
+ len_min = hm3[0] * 60 + hm3[1]
+ else:
+ ymdhm2_dt = None
+ len_min = None
+
+ return cls(
+ datetime.datetime(*ymdhm1), # type: ignore[arg-type]
+ ymdhm2_dt,
+ len_min,
+ )
+
+ _re = re.compile(
+ r'^(?!#).*CLOCK:\s+'
+ r'\[(\d+)\-(\d+)\-(\d+)[^\]\d]*(\d+)\:(\d+)\]'
+ r'(--\[(\d+)\-(\d+)\-(\d+)[^\]\d]*(\d+)\:(\d+)\]\s+=>\s+(\d+)\:(\d+))?'
+ )
+
+
+class OrgDateRepeatedTask(OrgDate):
+
+ """
+ Date object to represent repeated tasks.
+ """
+
+ _active_default = False
+
+ def __init__(self, start, before, after, active=None):
+ super(OrgDateRepeatedTask, self).__init__(start, active=active)
+ self._before = before
+ self._after = after
+
+ def __repr__(self):
+ args = [self._date_to_tuple(self.start), self.before, self.after]
+ if self._active is not self._active_default:
+ args.append(self._active)
+ return '{0}({1})'.format(
+ self.__class__.__name__, ', '.join(map(repr, args)))
+
+ def __eq__(self, other):
+ return super(OrgDateRepeatedTask, self).__eq__(other) and \
+ isinstance(other, self.__class__) and \
+ self._before == other._before and \
+ self._after == other._after
+
+ @property
+ def before(self):
+ """
+ The state of task before marked as done.
+
+ >>> od = OrgDateRepeatedTask((2005, 9, 1, 16, 10, 0), 'TODO', 'DONE')
+ >>> od.before
+ 'TODO'
+
+ """
+ return self._before
+
+ @property
+ def after(self):
+ """
+ The state of task after marked as done.
+
+ >>> od = OrgDateRepeatedTask((2005, 9, 1, 16, 10, 0), 'TODO', 'DONE')
+ >>> od.after
+ 'DONE'
+
+ """
+ return self._after