diff options
author | S. Solomon Darnell | 2025-03-28 21:52:21 -0500 |
---|---|---|
committer | S. Solomon Darnell | 2025-03-28 21:52:21 -0500 |
commit | 4a52a71956a8d46fcb7294ac71734504bb09bcc2 (patch) | |
tree | ee3dc5af3b6313e921cd920906356f5d4febc4ed /.venv/lib/python3.12/site-packages/orgparse/date.py | |
parent | cc961e04ba734dd72309fb548a2f97d67d578813 (diff) | |
download | gn-ai-master.tar.gz |
Diffstat (limited to '.venv/lib/python3.12/site-packages/orgparse/date.py')
-rw-r--r-- | .venv/lib/python3.12/site-packages/orgparse/date.py | 717 |
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 |