| # (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) |