# (c) 2005 Clark C. Evans and contributors # This module is part of the Python Paste Project and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php # Some of this code was funded by: http://prometheusresearch.com """ Date, Time, and Timespan Parsing Utilities This module contains parsing support to create "human friendly" ``datetime`` object parsing. The explicit goal of these routines is to provide a multi-format date/time support not unlike that found in Microsoft Excel. In most approaches, the input is very "strict" to prevent errors -- however, this approach is much more liberal since we are assuming the user-interface is parroting back the normalized value and thus the user has immediate feedback if the data is not typed in correctly. ``parse_date`` and ``normalize_date`` These functions take a value like '9 jan 2007' and returns either an ``date`` object, or an ISO 8601 formatted date value such as '2007-01-09'. There is an option to provide an Oracle database style output as well, ``09 JAN 2007``, but this is not the default. This module always treats '/' delimiters as using US date order (since the author's clients are US based), hence '1/9/2007' is January 9th. Since this module treats the '-' as following European order this supports both modes of data-entry; together with immediate parroting back the result to the screen, the author has found this approach to work well in pratice. ``parse_time`` and ``normalize_time`` These functions take a value like '1 pm' and returns either an ``time`` object, or an ISO 8601 formatted 24h clock time such as '13:00'. There is an option to provide for US style time values, '1:00 PM', however this is not the default. ``parse_datetime`` and ``normalize_datetime`` These functions take a value like '9 jan 2007 at 1 pm' and returns either an ``datetime`` object, or an ISO 8601 formatted return (without the T) such as '2007-01-09 13:00'. There is an option to provide for Oracle / US style, '09 JAN 2007 @ 1:00 PM', however this is not the default. ``parse_delta`` and ``normalize_delta`` These functions take a value like '1h 15m' and returns either an ``timedelta`` object, or an 2-decimal fixed-point numerical value in hours, such as '1.25'. The rationale is to support meeting or time-billing lengths, not to be an accurate representation in mili-seconds. As such not all valid ``timedelta`` values will have a normalized representation. """ from datetime import timedelta, time, date from time import localtime __all__ = ['parse_timedelta', 'normalize_timedelta', 'parse_time', 'normalize_time', 'parse_date', 'normalize_date'] def _number(val): try: return int(val) except: return None # # timedelta # def parse_timedelta(val): """ returns a ``timedelta`` object, or None """ if not val: return None val = val.lower() if "." in val: val = float(val) return timedelta(hours=int(val), minutes=60*(val % 1.0)) fHour = ("h" in val or ":" in val) fMin = ("m" in val or ":" in val) for noise in "minu:teshour()": val = val.replace(noise, ' ') val = val.strip() val = val.split() hr = 0.0 mi = 0 val.reverse() if fHour: hr = int(val.pop()) if fMin: mi = int(val.pop()) if len(val) > 0 and not hr: hr = int(val.pop()) return timedelta(hours=hr, minutes=mi) def normalize_timedelta(val): """ produces a normalized string value of the timedelta This module returns a normalized time span value consisting of the number of hours in fractional form. For example '1h 15min' is formatted as 01.25. """ if type(val) == str: val = parse_timedelta(val) if not val: return '' hr = val.seconds/3600 mn = (val.seconds % 3600)/60 return "%d.%02d" % (hr, mn * 100/60) # # time # def parse_time(val): if not val: return None hr = mi = 0 val = val.lower() amflag = (-1 != val.find('a')) # set if AM is found pmflag = (-1 != val.find('p')) # set if PM is found for noise in ":amp.": val = val.replace(noise, ' ') val = val.split() if len(val) > 1: hr = int(val[0]) mi = int(val[1]) else: val = val[0] if len(val) < 1: pass elif 'now' == val: tm = localtime() hr = tm[3] mi = tm[4] elif 'noon' == val: hr = 12 elif len(val) < 3: hr = int(val) if not amflag and not pmflag and hr < 7: hr += 12 elif len(val) < 5: hr = int(val[:-2]) mi = int(val[-2:]) else: hr = int(val[:1]) if amflag and hr >= 12: hr = hr - 12 if pmflag and hr < 12: hr = hr + 12 return time(hr, mi) def normalize_time(value, ampm): if not value: return '' if type(value) == str: value = parse_time(value) if not ampm: return "%02d:%02d" % (value.hour, value.minute) hr = value.hour am = "AM" if hr < 1 or hr > 23: hr = 12 elif hr >= 12: am = "PM" if hr > 12: hr = hr - 12 return "%02d:%02d %s" % (hr, value.minute, am) # # Date Processing # _one_day = timedelta(days=1) _str2num = {'jan':1, 'feb':2, 'mar':3, 'apr':4, 'may':5, 'jun':6, 'jul':7, 'aug':8, 'sep':9, 'oct':10, 'nov':11, 'dec':12 } def _month(val): for (key, mon) in _str2num.items(): if key in val: return mon raise TypeError("unknown month '%s'" % val) _days_in_month = {1: 31, 2: 28, 3: 31, 4: 30, 5: 31, 6: 30, 7: 31, 8: 31, 9: 30, 10: 31, 11: 30, 12: 31, } _num2str = {1: 'Jan', 2: 'Feb', 3: 'Mar', 4: 'Apr', 5: 'May', 6: 'Jun', 7: 'Jul', 8: 'Aug', 9: 'Sep', 10: 'Oct', 11: 'Nov', 12: 'Dec', } _wkdy = ("mon", "tue", "wed", "thu", "fri", "sat", "sun") def parse_date(val): if not(val): return None val = val.lower() now = None # optimized check for YYYY-MM-DD strict = val.split("-") if len(strict) == 3: (y, m, d) = strict if "+" in d: d = d.split("+")[0] if " " in d: d = d.split(" ")[0] try: now = date(int(y), int(m), int(d)) val = "xxx" + val[10:] except ValueError: pass # allow for 'now', 'mon', 'tue', etc. if not now: chk = val[:3] if chk in ('now','tod'): now = date.today() elif chk in _wkdy: now = date.today() idx = list(_wkdy).index(chk) + 1 while now.isoweekday() != idx: now += _one_day # allow dates to be modified via + or - /w number of days, so # that now+3 is three days from now if now: tail = val[3:].strip() tail = tail.replace("+"," +").replace("-"," -") for item in tail.split(): try: days = int(item) except ValueError: pass else: now += timedelta(days=days) return now # ok, standard parsing yr = mo = dy = None for noise in ('/', '-', ',', '*'): val = val.replace(noise, ' ') for noise in _wkdy: val = val.replace(noise, ' ') out = [] last = False ldig = False for ch in val: if ch.isdigit(): if last and not ldig: out.append(' ') last = ldig = True else: if ldig: out.append(' ') ldig = False last = True out.append(ch) val = "".join(out).split() if 3 == len(val): a = _number(val[0]) b = _number(val[1]) c = _number(val[2]) if len(val[0]) == 4: yr = a if b: # 1999 6 23 mo = b dy = c else: # 1999 Jun 23 mo = _month(val[1]) dy = c elif a is not None and a > 0: yr = c if len(val[2]) < 4: raise TypeError("four digit year required") if b: # 6 23 1999 dy = b mo = a else: # 23 Jun 1999 dy = a mo = _month(val[1]) else: # Jun 23, 2000 dy = b yr = c if len(val[2]) < 4: raise TypeError("four digit year required") mo = _month(val[0]) elif 2 == len(val): a = _number(val[0]) b = _number(val[1]) if a is not None and a > 999: yr = a dy = 1 if b is not None and b > 0: # 1999 6 mo = b else: # 1999 Jun mo = _month(val[1]) elif a is not None and a > 0: if b is not None and b > 999: # 6 1999 mo = a yr = b dy = 1 elif b is not None and b > 0: # 6 23 mo = a dy = b else: # 23 Jun dy = a mo = _month(val[1]) else: if b > 999: # Jun 2001 yr = b dy = 1 else: # Jun 23 dy = b mo = _month(val[0]) elif 1 == len(val): val = val[0] if not val.isdigit(): mo = _month(val) if mo is not None: dy = 1 else: v = _number(val) val = str(v) if 8 == len(val): # 20010623 yr = _number(val[:4]) mo = _number(val[4:6]) dy = _number(val[6:]) elif len(val) in (3,4): if v is not None and v > 1300: # 2004 yr = v mo = 1 dy = 1 else: # 1202 mo = _number(val[:-2]) dy = _number(val[-2:]) elif v < 32: dy = v else: raise TypeError("four digit year required") tm = localtime() if mo is None: mo = tm[1] if dy is None: dy = tm[2] if yr is None: yr = tm[0] return date(yr, mo, dy) def normalize_date(val, iso8601=True): if not val: return '' if type(val) == str: val = parse_date(val) if iso8601: return "%4d-%02d-%02d" % (val.year, val.month, val.day) return "%02d %s %4d" % (val.day, _num2str[val.month], val.year)