""" Interval functions. Supported intervals: LENGTH DATETIME DATETIME..[DATETIME] DATETIME+LENGTH DATETIME-LENGTH DATETIME+-LENGTH If DATETIME is not specified, then NOW and - is implied: NOW-LENGTH LENGTH has the following format: {NUMBER INTERVAL_SPECIFICATOR}+ INTERVAL_SPECIFICATOR is one of the following: s Second m Minute h Hour d Day (24 hours) w week M month (30 days) y year (365 days) Length examples: 10m 1h30m 10d 3w DATETIME can be specified in any parsable format (parsed by dateparser). If DATETIME specifies some long interval (say day or month), then this interval is considered as the resulting interval. """ import calendar import datetime import re from dateutil.relativedelta import relativedelta import dateparser INTERVAL_LENGTH = { 's': 1, 'm': 60, 'h': 3600, 'd': 24*3600, 'w': 7*24*3600, 'M': 30*24*3600, 'y': 365*24*3600, } def _epoch(dt_tuple): return calendar.timegm(dt_tuple.timetuple()) def _parse_datetime(date_time): if date_time == "": return datetime.datetime.now() return dateparser.parse(date_time) def parse_datetime(date_time, now=None): """ Parse ``date_time``, return time in seconds since EPOCH. """ if date_time == "" and now is not None: return now parsed = _parse_datetime(date_time) if parsed: return _epoch(parsed) return None def from_secs(secs): """ Convert number of seconds ``secs`` into interval description. >>> from_secs(10) '10s' >>> from_secs(60) '1m' >>> from_secs(3600) '1h' >>> from_secs(3660) '1h1m' """ result = "" for name, size in sorted(INTERVAL_LENGTH.items(), key=lambda x: -x[1]): number_of_subintervals = secs / size secs = secs % size if number_of_subintervals: result += "%s%s" % (number_of_subintervals, name) return result def parse_length(length): """ Parse ``length``` and return parsed length interval (in seconds) or None if length can't be parsed. >>> parse_length('1m') 60 >>> parse_length('1h1m') 3660 >>> parse_length('1') >>> parse_length('1hX1m') >>> parse_length('1d') 86400 >>> parse_length('2M') 5184000 """ sum_ = 0 joined = "" letters = "".join(INTERVAL_LENGTH.keys()) for number, int_spec in re.findall('([0-9]+)([%s])' % letters, length): joined += number + int_spec try: sum_ += int(number)*INTERVAL_LENGTH[int_spec] except KeyError: return None # if there were some skipped characters, # it was not a correct interval specification, # return None if joined != length: return None return sum_ def parse_interval(interval_string, now=None): # pylint: disable=too-many-branches,too-many-return-statements """ Parse ``interval_string`` and return a pair of timestamps in seconds since EPOCH >>> parse_interval('1h', now=3600) (0, 3600) >>> a, b = parse_interval('2018-01-10+1h'); b-a 3600 >>> a, b = parse_interval('2018-01-10+-1h'); b-a 7200 >>> a, _ = parse_interval('2018-01-10-1h'); _, b = parse_interval('2018-01-10+1h'); b-a 7200 >>> a, b = parse_interval('2018-01-01'); b-a 86400 >>> a, b = parse_interval('Jan,01'); b-a 86400 >>> a, b = parse_interval('2018-01'); b-a 2678400 >>> a, b = parse_interval('Jan'); b-a 2678400 """ # # Parsing DATETIME..[DATETIME] # separator_index = interval_string.rfind('..') if separator_index != -1: first = interval_string[:separator_index] second = interval_string[separator_index+2:] return parse_datetime(first), parse_datetime(second) # # Parsing DATETIME+-[LENGTH] # DATETIME+[LENGTH] # DATETIME-[LENGTH] # operator = None separator_index = interval_string.rfind('+-') if separator_index != -1: operator = '+-' else: separator_index = interval_string.rfind('+') if separator_index != -1: operator = '+' else: separator_index = interval_string.rfind('-') if separator_index != -1: # we accept this string only if LENGTH can be parsed as LENGTH if parse_length(interval_string[separator_index+1:]): operator = '-' if operator is not None: first = interval_string[:separator_index] second = interval_string[separator_index+len(operator):] length = parse_length(second) if length is None: raise SyntaxError("Can't parse interval: %s" % second) given_date = parse_datetime(first, now=now) if given_date is None: raise SyntaxError("Can't parse date/time: %s" % first) if operator == '-': return given_date - length, given_date if operator == '+': return given_date, given_date + length if operator == '+-': return given_date - length, given_date + length # # Parsing LENGTH # length = parse_length(interval_string) if length is not None: now = parse_datetime("", now=now) return now-length, now # # Parsing DATETIME # parsed = _parse_datetime(interval_string) if parsed is not None: if dateparser.parse(interval_string, settings={'RELATIVE_BASE':datetime.datetime(1, 1, 1)}) != \ dateparser.parse(interval_string, settings={'RELATIVE_BASE':datetime.datetime(1, 1, 2)}): delta = relativedelta(months=1) elif parsed.hour == 0: delta = relativedelta(days=1) elif parsed.minute == 0: delta = relativedelta(hours=1) return _epoch(parsed), _epoch(parsed+delta) return None, None