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