## timeRange.py ## author: Matthew Marquissee, Intern, NASA GSFC, Code 632 ## date started: July 16, 2003 ## last modified: July 28, 2003 ## purpose: This file performs many time-based operations. First, it ## implements a time range/interval class. Second, it extracts a ## timeRange object from a string (filename) based on a given format. ## Third, it uses a modified "strptime" module to parse simpler strings ## into timeRange objects. Lastly, it defines some conversion functions. from datetime import * # Python 2.3's new time from strptime import * # modified date/time parser import re, string # regular expressions for time strings # beginning strings YYYYDDD = r'^\s*(\d\d\d\d)[-:\s]*(\d\d\d)(.*)' YYYYMMDD = r'^\s*(\d\d\d\d)[-:\s]*(\d\d)[-:\s]*(\d\d)(.*)' BART_FORM = r'\s*b\s*(\d\d\d\d)\s*' # end strings HHNNSS = r'.*(\d\d)[-:\s]*(\d\d)[-:\s]*(\d\d)$' # common time durations durations = {'bartel': timedelta(27), 'day': timedelta(1), 'hour': timedelta(0, 0, 0, 0, 0, 1), 'minute': timedelta(0, 0, 0, 0, 1), 'second': timedelta(0, 1), 'millisecond': timedelta(0, 0, 0, 1), 'zero' : timedelta(0)} # timedelta has parameters (days, seconds, milliseconds, microseconds, # minutes, hours, weeks) for some strange reason. # timeRange class to simplify time range comparisons class timeRange: def __init__(self, start = datetime.min, stop = datetime.max): self.timepair = (start, stop) if start < stop: self.compare = 1 elif start == stop: self.compare = 0 else: self.compare = -1 def start(self): """Get start time for range""" if self.compare == -1: return self.timepair[1] return self.timepair[0] def stop(self): """Get stop time for range""" if self.compare == -1: return self.timepair[0] return self.timepair[1] def changeStart(self, t): """Change starting point of time range""" self.timepair = (t, self.stop()) def changeStop(self, t): """Change stopping point of time range""" self.timepair = (self.start(), t) def duration(self): """Elapsed time between start and stop times""" return self.stop() - self.start() def inRange(self, t_single): """Is t_single within the timeRange?""" if self.start() <= t_single <= self.stop(): return 1 return 0 def overlap(self, t_range): """Does this timeRange overlap t_range?""" if self.inRange(t_range.start()) and self.inRange(t_range.stop()): # <<-------- SELF -------->> # <<--- T_RANGE --->> return t_range elif self.inRange(t_range.start()): # <<-------- SELF -------->> # <<------- T_RANGE ------->> return timeRange(t_range.start(), self.stop()) elif self.inRange(t_range.stop()): # <<-------- SELF -------->> # <<------ T_RANGE ------>> return timeRange(self.start(), t_range.stop()) elif t_range.inRange(self.start()) and t_range.inRange(self.stop()): # <<--- SELF --->> # <<------ T_RANGE ------>> # this works, assuming there are no gaps in data return self else: # <<--- SELF --->> # <<--- T_RANGE --->> return None # helper functions def combine(x, y): """Combine two timeRange objects.""" if y != None and x == None: return y elif y == None: return x mn = min([x.start(), x.stop(), y.start(), y.stop()]) mx = max([x.start(), x.stop(), y.start(), y.stop()]) t = timeRange(mn, mx) return t def timesort(x, y): """Compare two (timeRange, value) pairs by timeRange""" if x[0] == None: return -1 if x[0].start() < y[0].start(): return -1 if x[0].start() == y[0].start(): return 0 return 1 def extract_date(fname, fpattern): """Extract a timeRange from a string with given pattern.""" # fpattern tells you how to parse the date out of a file name # %v matches variable amount of alphanumeric characters # %R bartel rotations # BORROWED FROM strptime module # %a Locale's abbreviated weekday name. # %A Locale's full weekday name. # %b Locale's abbreviated month name. # %B Locale's full month name. # %c Locale's appropriate date and time representation. # %d Day of the month as a decimal number [01,31]. # %H Hour (24-hour clock) as a decimal number [00,23]. # %I Hour (12-hour clock) as a decimal number [01,12]. # %j Day of the year as a decimal number [001,366]. # %m Month as a decimal number [01,12]. # %M Minute as a decimal number [00,59]. # %p Locale's equivalent of either AM or PM. # %S Second as a decimal number [00,61]. (1) # %U Week number of the year (Sunday as the first day of the week) as a decimal number [00,53]. All days in a new year preceding the first Sunday are considered to be in week 0. # %w Weekday as a decimal number [0(Sunday),6]. # %W Week number of the year (Monday as the first day of the week) as a decimal number [00,53]. All days in a new year preceding the first Monday are considered to be in week 0. # %x Locale's appropriate date representation. # %X Locale's appropriate time representation. # %y Year without century as a decimal number [00,99]. # %Y Year with century as a decimal number. # %Z Time zone name (no characters if no time zone exists). # %% A literal "%" character. # # anything else is matched literally # Ex. i8_k0_mag_19950820_v01.cdf # could correspond to *_*_*_%Y%m%d_*.cdf # or *_*_*_%Y%m%d_*.* but not *_*_*_%Y%j_*.cdf # dots (.) are reserved in regular expressions so escape them newpatt = string.replace(fpattern, '.', '[.]') # %v represent variable numbers of characters newpatt = string.replace(newpatt, '%v', '.+') # get all sequences of %_'s formatstrings = re.findall(r'((?:%[aAbBcdHIjmMpRSUwWxXyYZ%])+)', newpatt) # replace the sequences of %_'s with (.+) so we can retrieve the times for x in formatstrings: newpatt = string.replace(newpatt, x, '(.+)') newpatt = '.*/' + newpatt # match the new pattern with file name m = re.match(newpatt, fname) # no match if m == None: return None, fname timestrings = m.groups() # only one time on the file if len(timestrings) == 1 and len(formatstrings) == 1: t1 = get_time(timestrings[0], formatstrings[0]) return t1, fname # timerange on the file (if more than 2, pick only first 2) elif len(timestrings) <= 2 and len(formatstrings) <= 2: t1 = get_time(timestrings[0], formatstrings[0]) t2 = get_time(timestrings[1], formatstrings[1]) return combine(t1, t2), fname # there was problem with file name return None, fname # send in string, get back a range of datetime objects def get_time(str, pattern = None): # a time pattern was specified (usually with file name parsing) if pattern != None: if pattern == '%R': # special case of Bartels rotations try: dt_bartels = bartels_to_dt(int(str)) except ValueError: return None else: try: tmp = timeRange(dt_bartels, dt_bartels) except ValueError: return None else: return tmp try: the_dt = strptime(str, pattern) # send it to time parser except ValueError: # didn't work. try standard formats below. pass else: return timeRange(the_dt, the_dt) # success # Try matching the most common patterns (used with users' input) # ignore space, hyphens, and colons str = string.replace(str, ' ', '') str = string.replace(str, '-', '') str = string.replace(str, ':', '') # Special cases for convenience if string.upper(str) == 'LATEST': return timeRange(datetime.max, datetime.max) elif string.upper(str) == 'BEGINNING': return timeRange(datetime.min, datetime.min) # Check for Bartel rotations (four digits trailing a 'b') if re.match(BART_FORM, str): # Bartel rotations as input match_str = re.sub(BART_FORM, r'\1', str) n_bartels = int(match_str) the_dt = bartels_to_dt(n_bartels) return timeRange(the_dt, the_dt) # YYYYMMDDHHNNSS format if len(str) == 14: try: the_dt = strptime(str, '%Y%m%d%H%M%S') except ValueError: pass else: # these are the same for now, but the end time will be set to # the next file's datetime object (when scanning the list). return timeRange(the_dt, the_dt) # YYYYDDDHHNNSS format if len(str) == 13: try: the_dt = strptime(str, '%Y%j%H%M%S') except ValueError: pass else: return timeRange(the_dt, the_dt) # YYYYMMDD format if len(str) == 8: try: the_dt = strptime(str, '%Y%m%d') except ValueError: pass else: return timeRange(the_dt, the_dt) # YYYYDDD format if len(str) == 7: try: the_dt = strptime(str, '%Y%j') except ValueError: pass else: return timeRange(the_dt, the_dt) # nothing: return max time range possible return timeRange() # the default: min to max # various conversion and pattern checking functions def bartels_to_dt(n_bartels): """Converts from Bartel rotations to a datetime object""" basetime = datetime(1972, 1, 16) basebarts = 1894 n_bartels = n_bartels - basebarts t_delt = timedelta(n_bartels*27) return basetime + t_delt def dt_to_bartels(newtime): """Converts from a datetime object to Bartel rotations""" basetime = datetime(1972, 1, 16) basebarts = 1894 t_delt = newtime - basetime return basebarts + t_delt.days/27 def has_time_params(str): if re.match(HHNNSS, str): return 1 return 0 def is_bartel_form(str): if re.match(BART_FORM, str): return 1 return 0