#!/usr/bin/env python2.3 ''' $ python when.py [-h|--help] [-f "format"] [date] Example dates: today ToDaY yesterday tomorrow 1 day 3 weeks 4 hours ago now+ 42 minutes 1 second 5 years ago monday NoW now 3 January last thursDaY neXt sunday jan 3 4:20 jan 3 4:20pm next friday 4:20 p/m/ next friday 4:20 p.m. Output format is ISO [%Y-%m-%d %H:%M:%S] by default. You can specify your own with the -f flag. I'm just a program. I may be 200 lines of python, but I can still be confused very easily. When I do get confused, I usually calmly tell you through sys.stderr, then return a useless date in case you actually wanted me to ignore the error. ''' import sys, string, re, time def chunk(L, n=2): return [L[i : (i + n)] for i in xrange(0, len(L), n)] ISO_DATETIME_FORMAT = '%Y-%m-%d %H:%M:%S' MONTH_DAYS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] def is_leap_year(y): ''' http://en.wikipedia.org/wiki/Leap_year ''' if 0 == (y % 400): return True elif 0 == (y % 100): return False elif 0 == (y % 4): return True else: return False def get_days_in_month(year, month): if (month == 1) and is_leap_year(year): return 29 else: return MONTH_DAYS[month-1] def rectify(year, month, day=None): ''' if day is None: return (year, month) such that 1 <= month <= 12 else: return (year, month, day) such that 1 <= month <= 12 and 1 <= day <= get_days_in_month(year, month) ''' if day is None: year_delta, month = divmod(month, 12) return (year + year_delta), month else: year, month = rectify(year, month) while day < 0: try: day += get_days_in_month(year, month) except: year, month = rectify(year, month) else: month -= 1 while day > get_days_in_month(year, month): try: day -= get_days_in_month(year, month) except: year, month = rectify(year, month) else: month += 1 return year, month, day def when(s, in_US=True): ''' Take a string supposedly representing a date and/or time in no particular format, and return the date and time in ISO format. 'yesterday', and 'tomorrow' [capitalized or not] are special days which can only be accompanied by times. today is assumed if you only specify a time. 'ago' is a keyword which must be preceded by alternations of numbers and strings in ['days', 'day', 'months', 'month', 'years', 'year', 'weeks', 'week', 'hour', 'hours', 'minutes', 'minute', 'seconds', 'second'] eg. '1 month 2 weeks 1 day ago', '2 years 1 day ago 2:10:40', '20 minutes ago' 'now+' is similar to 'ago', but it must come before the alternations of numbers and strings. 'next' and 'last' are keywords for absolute mode which add or subtract 7 to/from the date: 'next tuesday' 'last thursday' I tried to read your mind, so pinch of salt and all that. If you do NOT want me to assume that the month comes before the day before the year, specify in_US=False. that variable is only used if s does not contain a month spelled out. >>> assert when('23:23 3 jun 2008') == '2008-6-3 23:23:00' ''' s = re.sub('[^\w\s\:\+]+', ' ', s).lower() # replace with a space everything but alphanum, colons, and plus signs if s.endswith('ago') or s.startswith('now+') or s.endswith('from now') or s.endswith('fromnow'): # relative time tokens = s.split(' ') while '' in tokens: tokens.remove('') now = time.time() levels = { 'second': 1, 'minute': 60, 'hour': 3600, 'day': 86400, 'week': 604800, 'month': 2678400, # approximation worth a couple dozen lines of nasty code 'year': 31556926, 'decade': 315569260, 'century': 3155692600 } direction = 1 if s.endswith('ago'): tokens = tokens[:-1] direction = -1 elif s.endswith('from now'): tokens = tokens[:-2] elif s.endswith('fromnow'): tokens = tokens[:-1] elif s.startswith('now+'): tokens = tokens[1:] for n, level in chunk(tokens, 2): if level.endswith('s'): level = level[:-1] if n.isdigit() and (level in levels): now += levels[level] * int(n) * direction return time.strftime(ISO_DATETIME_FORMAT, time.localtime(now)) elif s.strip() in ['now', '']: return time.strftime(ISO_DATETIME_FORMAT) else: # absolute time date = { 'Y': time.strftime('%Y'), 'm': time.strftime('%m'), 'd': time.strftime('%d'), 'H': '00', 'M': '00', 'S': '00' } # Nones prevent 1 offs long_months = [ None, 'january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december' ] short_months = [None] + [m[:3] for m in long_months[1:]] long_days = [None, 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'] short_days = [None] + [d[:3] for d in long_days[1:]] pm = False if 'am' in s: s = s.replace('am', '') elif 'pm' in s: pm = True s = s.replace('pm', '') tokens = re.split('[^\w\:]+', s) day = None month = None uneaten_tokens = [] bad_tokens = [] for i, token in enumerate(tokens): if token in long_months + short_months: try: date['m'] = str(long_months.index(token)).zfill(2) except: date['m'] = str(short_months.index(token)).zfill(2) month = token elif token in long_days + short_days: day = token if token in long_days: days_list, pat = long_days, '%A' else: days_list, pat = short_days, '%a' date['d'] = str( long_days.index(token) + int(date['d']) - days_list.index(time.strftime(pat).lower()) ).zfill(2) elif token == 'next': date['d'] = str(int(date['d']) + 7) elif token == 'last': date['d'] = str(int(date['d']) - 7) elif token == 'today': pass elif token == 'yesterday': date['d'] = str(int(date['d']) - 1).zfill(2) elif token == 'tomorrow': date['d'] = str(int(date['d']) + 1).zfill(2) elif (len(token) == 4) and token.isdigit(): date['Y'] = token elif re.match('(\d{1,2}\:){1,2}\d{1,2}', token): token_parts = token.split(':') date['H'] = token_parts[0].zfill(2) date['M'] = token_parts[1].zfill(2) if len(token_parts) > 2: date['S'] = token_parts[2].zfill(2) elif token.isdigit() and (len(token) < 3): uneaten_tokens.append(token) else: bad_tokens.append(token) # look for day, month in case specified numerically; # they're probably next to each other, and the only 2-digit tokens in uneaten_tokens if len(uneaten_tokens) == 1: token = uneaten_tokens[0] if (month is None) and (1 <= int(token) <= 12): date['m'] = token elif (day is None) and (1 <= int(token) <= 31): date['d'] = token elif (len(uneaten_tokens) == 2) and (month is None) and (day is None): if 12 < int(uneaten_tokens[0]) < 32: date['d'] = uneaten_tokens[0] date['m'] = uneaten_tokens[1] elif 12 < int(uneaten_tokens[1]) < 32: date['m'] = uneaten_tokens[0] date['d'] = uneaten_tokens[1] elif in_US: date['m'] = uneaten_tokens[0] date['d'] = uneaten_tokens[1] else: date['d'] = uneaten_tokens[0] date['m'] = uneaten_tokens[1] if pm and (int(date['H']) < 13): date['H'] = str(int(date['H']) + 12) date['Y'], date['m'], date['d'] = map(str, rectify(int(date['Y']), int(date['m']), int(date['d']))) if bad_tokens: print >> sys.stderr, 'bad tokens:', bad_tokens result = ISO_DATETIME_FORMAT for k, v in date.iteritems(): result = result.replace('%' + k, v) return result def main(argv): argv = list(argv) if '-f' in argv: format = argv[argv.index('-f') + 1] argv.remove('-f') argv.remove(format) print time.strftime(format, time.strptime(when(' '.join(argv)), ISO_DATETIME_FORMAT)) elif ('-h' in argv) or ('--help' in argv): print __doc__ elif not argv: import doctest exec doctest.script_from_examples(when.__doc__) print when('now') else: print when(' '.join(argv)) if __name__ == '__main__': sys.exit(main(sys.argv[1:]))