about summary refs log tree commit diff
path: root/.venv/lib/python3.12/site-packages/orgparse/date.py
diff options
context:
space:
mode:
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