#!/usr/bin/python

# tz_parse_time.py
#       --copyright--                   Copyright 2007 (C) Tranzoa, Co. All rights reserved.    Warranty: You're free and on your own here. This code is not necessarily up-to-date or of public quality.
#       --url--                         http://www.tranzoa.net/tzpython/
#       --email--                       pycode is the name to send to. tranzoa.com is the place to send to.
#       --bodstamps--
#       July 7, 2003            bar
#       October 27, 2006        bar     parse another format
#       July 21, 2007           bar     nist ascii format
#       July 22, 2007           bar     .gpx format
#       July 24, 2007           bar     now that nist is back on line, let's subtract rather than add the msADV value
#       July 27, 2007           bar     exif and gps/exif timezone parsing
#                                       parse_time_zone
#       November 18, 2007       bar     turn on doxygen
#       November 20, 2007       bar     comments
#       November 27, 2007       bar     add fractional seconds to gpx and exif times (the latter 'cause they must have the same fields for the cheap code to work)
#       November 27, 2007       bar     insert boilerplate copyright
#       December 1, 2007        bar     time_diff_str()
#       December 3, 2007        bar     comment
#       December 9, 2007        bar     comments and allow 1900 years in the month-name forms 1 and 2
#                                       allow fractional seconds in parse_time_zone
#       May 17, 2008            bar     email adr
#       November 18, 2008       bar     hmmss_str()
#       May 21, 2011            bar     another time parse type MM dd, yyyy hh:mm:ss
#       May 27, 2012            bar     doxygen namespace
#       September 12, 2012      bar     svn format
#                                       Month day, year
#       January 3, 2013         bar     mm/dd/(yy)yy and dd/mm/(yy)yy
#       October 15, 2013        bar     parse_datetime()
#       October 30, 2013        bar     got the wrong code copied in the newest regx use for datetime
#                                       make commas/maybe-space and spaces equivalent where they can be
#                                       move staight mmddyy(yy) and ddmm(yy)yy to here from an app
#                                       beef up the regression test a bit
#       July 2, 2014            bar     parse_date() and age_from_dob()
#                                       able to parse YYYY MM DD  and YYYY DD MM
#       October 24, 2014        bar     is_time_not_date param to parse_time()
#                                       case insensitive parse_time_zone and make it work for [+-]HHMM
#                                       and make parse_time_zone able to handle HH:MM:.ddd
#       October 25, 2014        bar     more time and time zone parsing - spaces after +- - seconds and fractional seconds
#       August 30, 2015         bar     allow forms 1, 2, 3, and 4 to optionally have the century (19 or 20)
#                                       form_9 dd month_name year - and allow now spaces within
#                                       allow month name to not have spaces after it, with or without the comma
#                                       allow more varients of month names
#                                       form_10 - (yy)yy month_name dd?
#                                       form_11 - (yy)yy dd? month_name
#                                       and all the others up thru 23
#       November 4, 2015        bar     parse int or float unix time values
#                                       put a \b at the end of forms 7, 21, 22, and 23
#                                       strip the string going in to parse_datetime() and get rid of trailing \s* from forms with them
#                                       for beginning of string for froms 5, 6, 7, 8, 14, 21, 22, and 23
#                                       explicitly ignore the English season names
#       March 21, 2016          bar     parse_time_of_day()
#       March 27, 2016          bar     yyyy:mm:dd for EXIF GPS date stamps
#       April 22, 2016          bar     allow HH:MM:SS:mills... along with HH:MM:SS.mills...
#       October 25, 2016        bar     be a bit tougher on dates with slashes - make sure they don't end in slashes
#       November 7, 2017        bar     maxint->maxsize
#       May 19, 2018            bar     image file format YYYYMMDD_HHMMSS and parse_image_file_date_time() to make life easy
#       January 11, 2020        bar     non-recent year clicked over
#       May 2, 2020             bar     sorta handle st|nd|rd|th after the day number
#       May 21, 2020            bar     handle dates of YYYY-MM-DD or YYYY-month name-dd or YYYY:month name:DD
#       February 8, 2021        bar     fix the last July 7, 2015 to 2016 to make it recent
#       --eodstamps--
##      \file
#       \namespace              tzpython.tz_parse_time
#
#
#       Do some ASCII time parsing.
#
#


import  datetime
import  re
import  sys
import  time



NON_RECENT_YEARS_AGO    = 5     #: how mamy years ago is "non-recent"


def fixed_year(s) :
    """ Return the year from the given digit string, fixing as best we can whether two digit years are in the 1900's or the 2000's. """
    y   = int(s)
    if  38 <= y <= 99 :
        y  += 1900
    elif 0 <= y <= 38 :     # note <= 38 in case someone gets the right idea and changes the if above
        y  += 2000
    return(y)


##      Days-in-months table
nyda    = [ 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 ]

def make_utime(mo, d, y, h, mi, s) :
    """
        Given month, day, year, hour, minute, second,
        return the Unix (1/1/1970 based) time as a float.

        mo is 1..12 (or 0 if d has the day-of-year)
        d  is 1..31 or day-of-year 1..365)
        y  is 1938..2037 or 0..99
        h  is 0..23
        mi is 0..59
        s  is 0..59 or 0.0 .. 59.9999999....

        This routine accepts odd-ball values for parameters.
    """

    if  not 0 < mo <= len(nyda) :                               # we have a day-of-year and no month?
        mo  = 1

    if  y      >=  100 :
        y      -= 1970                                          # way-off years will get weird 'cause 100 year and 400 year and 1000 year logic isn't here, really !!!!
    else :
        y      %= 100                                           # yes, bad data to this routine gives bad results, not exceptions
        if  y  >= 38 :
            y  -= 70
        else :
            y  += 30
        pass


    t = float(s) + (
        (mi * 60) +
        (h * 3600) +
        ((d - 1) * 86400) +
        (nyda[mo - 1] * 86400) +
        ((365 * 86400) * y) +
        (86400 * ((y + 1) / 4))
        )

    if  ((y % 4) == 2) and (mo >= 3) :
        t  += 86400                                             #  add Feb 29th of this year if we're past it

    return(t)


def mktime(tm) :
    """ Return the GMT (absulute) time for the given mdyhms). Not to be confused with time.mktime() which returns localtime and must have time.timezone subtracted from it. """

    return(make_utime(tm[1], tm[2], tm[0], tm[3], tm[4], tm[5]))



def hmmss_str(t) :
    """ Return an hour:minute:second string for the given time. """

    t   = int(round(t or 0.0))
    return("%u:%02u:%02u" % ( t / 3600, (t % 3600) / 60, t % 60 ))




def time_diff_str(gmt, ot) :
    """ Return a string telling how far off a GMT time is to the "correct" 'gmt' time. """

    s   = ""

    d   = gmt - ot

    a       = " slow"
    if  d < 0 :
        a   = " fast"
        d   = -d

    d   = int(d)

    dd  = d / (24 * 3600)
    d  %=     (24 * 3600)
    if  dd :
        ss                  = ""
        if  dd > 1 :    ss  = "s"
        s  += " %u day%s" % ( dd, ss )

    dd  = d / 3600
    d  %=     3600
    if  dd :
        ss                  = ""
        if  dd > 1 :    ss  = "s"
        s  += " %u hour%s" % ( dd, ss )

    dd  = d / 60
    d  %=     60
    if  dd :
        ss                  = ""
        if  dd > 1 :    ss  = "s"
        s  += " %u minute%s" % ( dd, ss )

    dd  = int(d)
    if  dd :
        ss                  = ""
        if  dd > 1 :    ss  = "s"
        s  += " %u second%s" % ( dd, ss )

    s      += a

    return(s.strip())







##      regx to detect a time zone.
tz_re   = re.compile(r"(?:Z|GMT|UTC|([\+\-]?(?:\d+(?:\.\d*)?|\.\d+))(?:\:(\d+)(?:\:(\d+(?:\.\d*)?|\.\d+))?)?)", re.IGNORECASE)


def parse_time_zone(s) :
    """
        Given a string, see if we can find the time zone in it.

        Return None if we cannot.

        Return the time zone offset from GMT in seconds if we can.
    """

    g   = tz_re.search(s.replace(' ', ''))
    if  not g :
        return(None)

    neg =   False
    t   =   0
    if  g.lastindex > 0 :
        ts  = g.group(1)
        neg = (ts[0] == '-')
        ts  = ts.lstrip("+-")
        if  g.lastindex > 1 :                                           # if they have colons, just make the first number hours
            t   = float(ts) * 3600.0
        if len(ts) == 3 :
            t   = ((int(ts[0], 10) * 60) + int(ts[1:], 10)) * 60        # figure it for HMM
        elif len(ts) < 2 :
            t   = float(ts) * 3600.0                                    # figure it for HH or H
        else    :
            t   = (((int(ts[:2], 10) * 60) + int(ts[2:4], 10)) * 60) + float(ts[4:] or 0)       # HHMM(SS.fraction)
        pass
    if  g.lastindex > 1 :
        t  += (float(g.group(2)) * 60)
    if  g.lastindex > 2 :
        t  += float(g.group(3))

    if  neg :
        t   = -t

    return(t)



##                      Months in 3-char, lower-case English form
month_str           =   "janfebmaraprmayjunjulaugsepoctnovdec"

##                      regx to sense a month in 3-char or full name English
# mon_re_str        =   r"(Jan(?:uary)?|Feb(?:ruary)?|Mar(?:ch)?|Apr(?:il)?|May|Jun(?:e)?|Jul(?:y)?|Aug(?:ust)?|Sep(?:tember)?|Oct(?:ober)?|Nov(?:ember)?|Dec(?:ember)?)(?:,|\s+)\s*"
mon_re_str          =   r"(Jan(?:uary|u)?|Feb(?:ruary|r)?|Mar(?:ch)?|Apr(?:il?)?|May|June?|July?|Aug(?:ust|u)?|Sep(?:tember|tem|t)?|Oct(?:ober|ob?)?|Nov(?:ember|emb|em?)?|Dec(?:ember|emb|em?)?)\s*"
mon_comma_re_str    =   mon_re_str + r",?\s*"

day_re_str          =   r"([12]\d|3[01]|0?[1-9])"
#   day_re_str      =   r"(1\d(?:\s*th)?|21(?:\s*st)?|22(?:\s*nd)?|23(?:\s*rd)?|2[0456789](?:\s*th)?|30(?:\s*th)?|31(?:\s*st)?]|0?(?:1(?:\s*st)?|2(?:\s*nd)?|3(?:\s*rd)?|[4-9](?:\s*th)?))"     # note: we can't include the nd|st|rd|th in the string that's found so this doesn't work
if  False :
    x    =  re.compile(day_re_str + '\s*,')
    for i in range(1, 32) :
        print i, x.search(' %02u nd, ' % i)
    sys.exit(1)

day_comma_re_str    =   day_re_str + r"\s*(?:th|st|nd|rd)?\s*,?\s*"

mon_num_re_str      =   r"(1[0-2]|0?[1-9])"

year_re_str         =   r"(19\d\d|20\d\d|\d\d)"

strict_year_re_str  =   r"(19\d\d|20\d\d|3[2-9]|[4-9]\d)"
strict_mon_re_str   =   r"(1[0-2]|0[1-9])"
strict_day_re_str   =   r"([12]\d|3[01]|0[1-9])"
strict_hour_re_str  =   r"([01]\d|2[0-3])"
strict_min_re_str   =   r"([0-5]\d)"                 # we'll ignore that it's actually legit to have 60 seconds at the end of the day
strict_sec_re_str   =   r"([0-5]\d|60)"              # it's actually legit to have 60 seconds at the end of the day
strict_time_re_str  =   strict_hour_re_str + strict_min_re_str + strict_sec_re_str


winter_re           =   re.compile(r"\bwin(?:ter)?\b",  re.IGNORECASE + re.DOTALL)
spring_re           =   re.compile(r"\bspr(?:ing)?\b",  re.IGNORECASE + re.DOTALL)
summer_re           =   re.compile(r"\bsum(?:mer)?\b",  re.IGNORECASE + re.DOTALL)
fall_re             =   re.compile(r"\bfall\b",         re.IGNORECASE + re.DOTALL)
season_days         =   [
                            "Jan 31,",                  # I like this better than 1/30
                            "May  5,",                  # I like this better than 5/6
                            "Aug  6,",
                            "Nov  5,",
                        ]                               #: mid-season days



##                                   (DOW)                       DD                    Month              Year               HH     MM     SS                   e.g. "Tue 23 Jan 2007 03:56:45" "Tuesday 23 January 2007 03:56:45"
form_1_re   =   re.compile(r"\s*(?:\b[a-z]+(?:\s*,\s*|\s+))?"  + day_re_str + r"\s+" + mon_comma_re_str + year_re_str + r"\s+(\d\d):(\d\d):(\d\d)\s+GMT\b",     re.IGNORECASE + re.DOTALL)

##                                   (DOW)                       Month              DD                HH     MM     SS           Year                           e.g. "Tue Jan 23 2007 03:56:45" "Tuesday January 23 2007 03:56:45"
form_2_re   =   re.compile(r"\s*(?:\b[a-z]+(?:\s*,\s*|\s+))?"  + mon_comma_re_str + day_re_str + r"\s+(\d\d):(\d\d):(\d\d)\s+" + year_re_str + r"\b",           re.IGNORECASE + re.DOTALL)

##                                   (DOW)                       Month              DD                 Year               HH      MM          SS                e.g. "Jan 23 2007 03:56:45" "Tuesday January 23, 2007 03:56"
form_3_re   =   re.compile(r"\s*(?:\b[a-z]+(?:\s*,\s*|\s+))?"  + mon_comma_re_str + day_comma_re_str + year_re_str + r"\s+(\d?\d):(\d?\d)(?:\:(\d?\d))?\b",     re.IGNORECASE + re.DOTALL)


##                                                                    Month              DD                    Year                                             e.g. "January 23, 2007"
form_4_re   =   re.compile(r"\s*"                           + r"\b" + mon_comma_re_str + day_comma_re_str    + year_re_str  + r"\b",                            re.IGNORECASE + re.DOTALL)

##                                                                    DD                     Month              Year                                            e.g. "23 January, 2007"
form_9_re   =   re.compile(r"\s*"                           + r"\b" + day_re_str  + r"\s*" + mon_comma_re_str + year_re_str + r"\b",                            re.IGNORECASE + re.DOTALL)

##                                                                    Year                   Month              DD                                              e.g. "2007 January 23"
form_10_re  =   re.compile(r"\s*"                           + r"\b" + year_re_str + r"\s*" + mon_comma_re_str + day_re_str  + r"\b",                            re.IGNORECASE + re.DOTALL)

##                                                                    Year                   DD                     Month                                       e.g. "2007 23 January"
form_11_re  =   re.compile(r"\s*"                           + r"\b" + year_re_str + r"\s*" + day_re_str  + r"\s*" + mon_re_str + r"\b",                         re.IGNORECASE + re.DOTALL)

##                                                                    Year                   Month                                                              e.g. "2007 January"
form_12_re  =   re.compile(r"\s*"                           + r"\b" + year_re_str + r"\s*" + mon_re_str + r"\b",                                                re.IGNORECASE + re.DOTALL)

##                                                                    Month              Year                                                                   e.g. "2007 January"
form_13_re  =   re.compile(r"\s*"                           + r"\b" + mon_comma_re_str + year_re_str,                                                           re.IGNORECASE + re.DOTALL)

##                                                                    Year                                                                                      e.g. "2007 January"
form_14_re  =   re.compile(r"\s*"                           + r"^"  + strict_year_re_str    + r"\b",                                                            re.IGNORECASE + re.DOTALL)

##                                                                    Month              DD                    Year                                             e.g. "January 23, 2007"
form_15_re  =   re.compile(r"\s*"                           + r"\b" + mon_comma_re_str + day_comma_re_str    + strict_year_re_str  + r"\b",                     re.IGNORECASE + re.DOTALL)

##                                                                    DD                     Month              Year                                            e.g. "23 January, 2007"
form_16_re  =   re.compile(r"\s*"                           + r"\b" + day_re_str  + r"\s*" + mon_comma_re_str + strict_year_re_str + r"\b",                     re.IGNORECASE + re.DOTALL)

##                                                                    Year                          Month              DD                                       e.g. "2007 January 23"
form_17_re  =   re.compile(r"\s*"                           + r"\b" + strict_year_re_str + r"\s*" + mon_comma_re_str + day_re_str  + r"\b",                     re.IGNORECASE + re.DOTALL)

##                                                                    Year                          DD                     Month                                e.g. "2007 23 January"
form_18_re  =   re.compile(r"\s*"                           + r"\b" + strict_year_re_str + r"\s*" + day_re_str  + r"\s*" + mon_re_str + r"\b",                  re.IGNORECASE + re.DOTALL)

##                                                                    Year                          Month                                                       e.g. "2007 January"
form_19_re  =   re.compile(r"\s*"                           + r"\b" + strict_year_re_str + r"\s*" + mon_re_str + r"\b",                                         re.IGNORECASE + re.DOTALL)

##                                                                    Month              Year                                                                   e.g. "January 2007 "
form_20_re  =   re.compile(r"\s*"                           + r"\b" + mon_comma_re_str + strict_year_re_str + r"\b",                                            re.IGNORECASE + re.DOTALL)


##                                                                    Year                               Month                                                  e.g. "2007 01"
form_21_re  =   re.compile(r"\s*"                           + r"^"  + strict_year_re_str + r"\s*/?\s*" + mon_num_re_str     + r"(?!/)\b",                       re.IGNORECASE + re.DOTALL)

##                                                                    Month                              Year                                                   e.g. "01 2007"
form_22_re  =   re.compile(r"\s*"                           + r"^"  + mon_num_re_str     + r"\s*/?\s*" + strict_year_re_str + r"(?!/)\b",                       re.IGNORECASE + re.DOTALL)


##                                                                    Month                              Year                                                   e.g. "3 2007"
form_23_re  =   re.compile(r"\s*"                           + r"^"  + mon_num_re_str     + r"\s*/?\s*" + year_re_str        + r"(?!/)\b",                       re.IGNORECASE + re.DOTALL)


##                                                                    Monthnum                       DD                             Year                            e.g. "12/23/2007"
form_5_re   =   re.compile(r"\s*"                           + r"^"  + mon_num_re_str + r"\s*/?\s*" + day_re_str     + r"\s*/?\s*" + year_re_str    + r"(?!/)\b",    re.IGNORECASE + re.DOTALL)

##                                                                    DD                             Monthnum                       Year                            e.g. "23/12/2007"
form_6_re   =   re.compile(r"\s*"                           + r"^"  + day_re_str     + r"\s*/?\s*" + mon_num_re_str + r"\s*/?\s*" + year_re_str    + r"(?!/)\b",    re.IGNORECASE + re.DOTALL)


##                                                                    Year                           Monthnum                       DD                              e.g. "2007/12/23" "20071212"
form_7_re   =   re.compile(r"\s*"                           + r"^"  + year_re_str    + r"\s*/?\s*" + mon_num_re_str + r"\s*/?\s*" + day_re_str     + r"(?!/)\b",    re.IGNORECASE + re.DOTALL)

##                                                                    Year                           DD                             Monthnum                        e.g. "2007/23/12" "20072312"
form_8_re   =   re.compile(r"\s*"                           + r"^"  + year_re_str    + r"\s*/?\s*" + day_re_str     + r"\s*/?\s*" + mon_num_re_str + r"(?!/)\b",    re.IGNORECASE + re.DOTALL)

##                                                                    Year                               Monthnum                        DD                         e.g. "2007:12:23"
form_24_re  =   re.compile(r"\s*"                           + r"^"  + strict_year_re_str + r"\s*:\s*"  + mon_num_re_str + r"\s*:\s*"   + day_re_str + r"\b",        re.IGNORECASE + re.DOTALL)

##                                                                    Year                               Monthnum                        DD                         e.g. "2007-12-23"
form_25_re  =   re.compile(r"\s*"                           + r"^"  + strict_year_re_str + r"\s*-\s*"  + mon_num_re_str + r"\s*-\s*"   + day_re_str + r"\b",        re.IGNORECASE + re.DOTALL)

##                                                                    Year                               Month                       DD                             e.g. "2007-12-23"
form_26_re  =   re.compile(r"\s*"                           + r"^"  + strict_year_re_str + r"\s*:\s*"  + mon_re_str + r"\s*:\s*"   + day_re_str + r"\b",            re.IGNORECASE + re.DOTALL)

##                                                                    Year                               Month                       DD                             e.g. "2007-12-23"
form_27_re  =   re.compile(r"\s*"                           + r"^"  + strict_year_re_str + r"\s*-\s*"  + mon_re_str + r"\s*-\s*"   + day_re_str + r"\b",            re.IGNORECASE + re.DOTALL)

##                                                                    YYYY                 Monthnum            DD                      HHMMSS                       e.g. "20071223_132705"
img_file_re =   re.compile(r"\s*"                           + r"^"  + strict_year_re_str + strict_mon_re_str + day_re_str + r"[_-]?" + strict_time_re_str + r"\b",  re.IGNORECASE + re.DOTALL)



##              regx to extract time/date from a nist time server response                                          e.g. "54444 07-12-10 07:00:12 00 0 0  32.5 UTC(NIST) *"
nist_re     =   re.compile(r"(\d+) (\d\d)-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d) (\d+) (\d+) (\d+) (\d+\.\d+) UTC\(NIST\) \*")

##              regx to extract time/date from a .gpx file (and ISO 8601 - http://www.w3.org/TR/NOTE-datetime)      e.g. "2007-12-08T17:36:03Z" "2007-12-08T17:36:03-08:00" "2007-12-08T17:36:03.25-08:00"
gpx_re      =   re.compile(r"(\d\d\d\d)-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d(?:\.\d+)?)(?:Z|([+-]\d\d):(\d\d))")

##              regx to extract time/date from EXIF format and svn log format                                       e.g. EXIF "2007:12:08 18:19:11"     svn "2012-07-18 02:47:15 -0700"
exif_re     =   re.compile(r"(\d\d\d\d)[:-](\d\d)[:-](\d\d)\s+(\d\d):(\d\d):(\d\d(?:[\.\:]\d*)?)\s*(?:Z|([+-]\d\d):?(\d\d))?")


def parse_datetime(s) :
    """
        Parse date/time in many forms.

        Return a datetime.datetime().

    """

    # tzone   = 0         # time.timezone

    s       = s.strip()

    seastr  = ""
    season  = -1
    for i,  sr in enumerate([ winter_re, spring_re, summer_re, fall_re, ]) :
        if  sr.match(s) :
            s       = sr.sub('', s).strip()
            season  = i
            seastr  = season_days[season]
            break
        pass
    seastr                          # make pyflakes happy


    #
    #   Note: Win boxes don't have time.strptime() and it doesn't seem to work, anyway, on Gina/Python22, at any rate.
    #
    g       = form_1_re.search(s)
    if  g   :
        dc  = g.group(2)[0:3].lower()
        mon = int(month_str.find(dc) / 3) + 1
        y   = fixed_year(g.group(3))

        try :
            return(datetime.datetime(y, mon, int(g.group(1)), int(g.group(4)), int(g.group(5)), int(g.group(6))))

        except ( ValueError, OverflowError, ) :
            pass
        pass

    g       = form_2_re.search(s)
    if  g   :
        dc  = g.group(1)[0:3].lower()
        mon = int(month_str.find(dc) / 3) + 1
        y   = fixed_year(g.group(6))

        try :
            return(datetime.datetime(y, mon, int(g.group(2)), int(g.group(3)), int(g.group(4)), int(((g.lastindex >= 5) and g.group(5)) or 0)))

        except ( ValueError, OverflowError, ) :
            pass
        pass


    g       = form_3_re.search(s)
    if  g   :
        dc  = g.group(1)[0:3].lower()
        mon = int(month_str.find(dc) / 3) + 1
        y   = fixed_year(g.group(3))

        try :
            return(datetime.datetime(y, mon, int(g.group(2)), int(g.group(4)), int(g.group(5)), int(((g.lastindex >= 6) and g.group(6)) or 0)))

        except ( ValueError, OverflowError, ) :
            pass
        pass


    g       = nist_re.search(s)
    if  g   :
        try :
            y   = fixed_year(g.group(2))
            d   = datetime.datetime(y, int(g.group(3)), int(g.group(4)), int(g.group(5)), int(g.group(6)), int(g.group(7)))
            d  -= datetime.timedelta(milliseconds = float(g.group(11)))         # why is this negative????

            return(d)

        except ( ValueError, OverflowError, ) :
            pass
        pass


    g       = gpx_re.search(s)
    if  not g :
        g   = exif_re.search(s)
    if  not g :
        g   = img_file_re.search(s)
    if  g   :
        try :
            sec = float(g.group(6).replace(':', '.'))
            ms  = int(round((sec - int(sec)) * 1000000))
            sec = int(sec)
            y   = fixed_year(g.group(1))
            d   = datetime.datetime(y, int(g.group(2)), int(g.group(3)), int(g.group(4)), int(g.group(5)), sec, microsecond = ms)

            if  g.lastindex == 8 :
                h   =   int(g.group(7))
                m   =   int(g.group(8))
                if  h < 0 :
                    m   = -m
                d  -=   datetime.timedelta(minutes = m, hours = h)

            return(d)

        except ( ValueError, OverflowError, ) :
            pass
        pass


    ll  = 0
    ls  = sys.maxsize
    ld  = None
    for form, gns in [
                        [ form_19_re,   [ 0, 2, 0, 1 ] ],
                        [ form_20_re,   [ 0, 1, 0, 2 ] ],
                        [ form_15_re,   [ 0, 1, 2, 3 ] ],
                        [ form_16_re,   [ 0, 2, 1, 3 ] ],
                        [ form_17_re,   [ 0, 2, 3, 1 ] ],
                        [ form_18_re,   [ 0, 3, 2, 1 ] ],
                        [ form_21_re,   [ 0, 2, 0, 1 ] ],
                        [ form_22_re,   [ 0, 1, 0, 2 ] ],
                        [ form_14_re,   [ 0, 0, 0, 1 ] ],
                        [ form_12_re,   [ 0, 2, 0, 1 ] ],
                        [ form_13_re,   [ 0, 1, 0, 2 ] ],
                        [ form_4_re,    [ 0, 1, 2, 3 ] ],
                        [ form_9_re,    [ 0, 2, 1, 3 ] ],
                        [ form_10_re,   [ 0, 2, 3, 1 ] ],
                        [ form_11_re,   [ 0, 3, 2, 1 ] ],
                        [ form_5_re,    [ 0, 1, 2, 3 ] ],
                        [ form_6_re,    [ 0, 2, 1, 3 ] ],
                        [ form_7_re,    [ 0, 2, 3, 1 ] ],
                        [ form_8_re,    [ 0, 3, 2, 1 ] ],
                        [ form_24_re,   [ 0, 2, 3, 1 ] ],
                        [ form_25_re,   [ 0, 2, 3, 1 ] ],
                        [ form_26_re,   [ 0, 2, 3, 1 ] ],
                        [ form_27_re,   [ 0, 2, 3, 1 ] ],
                        [ form_23_re,   [ 0, 1, 0, 2 ] ],
                        [ img_file_re,  [ 0, 1, 0, 2 ] ],
                     ] :
        g           = form.search(s)
        if  g       :
            ln      = g.end() - g.start()
            if  (ll  < ln) or ((ll == ln) and (g.start() < ls)) :
                try :
                    if  gns[1] :
                        dc  = g.group(gns[1])[0:3].lower()
                        dci = month_str.find(dc)
                        if  dci >= 0 :
                            mon = int(dci / 3) + 1
                        else    :
                            mon = int(g.group(gns[1]))
                        pass
                    else    :
                        mon = 1

                    y       = fixed_year(g.group(gns[3]))

                    if  gns[2] :
                        day = int(g.group(gns[2]))
                    else    :
                        day = 1

                    d       = datetime.datetime(y, mon, day)
                    if  (d.month == mon) and (d.day == day) :
                        ld  = d
                        ll  = ln
                        ls  = g.start()
                        # print "@@@@", ld, ll, ls, gns, form.pattern
                    pass
                except ( ValueError, OverflowError, ) :
                    pass
                pass
            pass
        pass
    if  ld  :
        return(ld)


    ss  = re.sub(r'\s+', '', s)
    if  re.match(r"^\d+$", ss) and (len(ss) in [ 6, 8 ]) :
        try :
            y   = fixed_year(ss[4:])
            #                       MMDDYY(YY)
            d   = datetime.datetime(y, int(ss[0:2]), int(ss[2:4]))
            if  (d.month == int(ss[0:2])) and (d.day == int(ss[2:4])) :
                return(d)

            pass
        except (  ValueError, OverflowError, ) :
            try :
                y   = fixed_year(ss[4:])
                #                       DDMMYY(YY)
                d   = datetime.datetime(y, int(ss[2:4]), int(ss[0:2]))
                if  (d.month == int(ss[2:4])) and (d.day == int(ss[0:2])) :
                    return(d)

                pass
            except (  ValueError, OverflowError, ) :
                pass
            pass
        pass

    ss  = s.strip()
    if  re.match(r"^(?:[\+\-]?(?:\d+(?:\.\d*)?|\.\d+))$", ss) :         # note: the code won't get here for numbers that pass the tests earlier for things like YYYYMMDD, etc.
        return(datetime.datetime.fromtimestamp(float(ss)))

    return(None)


#                                             HH  or H                 MM                       SS                 am or PM
time_1_re   =   re.compile(r"^\s*(?:at\s+)?([0-1][0-9]|2[0-3]|[0-9])(?:\s*:?\s*([0-5][0-9]))?(?:\s*:?\s*([0-5][0-9](?:\.\d*)?))?(?:\s*((?:at\s+)?am|pm|(?:Z|GMT|UTC|(?:(?:[\+\-]\s*)?(?:\d+(?:\.\d*)?|\.\d+))(?:\:(?:\d+)(?:\:(?:\d+(?:\.\d*)?|\.\d+))?)?)))?\s*$", re.IGNORECASE + re.DOTALL)
sssss_re    =   re.compile(r'(\d+(?:\.\d*)?|\.\d+)', re.DOTALL)


def parse_time_of_day(s) :
    """
        Return the number of seconds after midnight given by the string, 's'.

        Return None if no time of day can be parsed.

    """
    if  s   :
        s   = s.strip()
        g   = time_1_re.search(s)
        if  g :
            t   = float(g.group(1)) * 3600
            if  g.lastindex > 1 :
                t  += float(g.group(2) or "0") * 60
                if  g.lastindex > 2 :
                    t  += float(g.group(3) or "0")
                    if  g.lastindex == 4 :
                        if  g.group(4).lower() == 'pm' :
                            t  += 12 * 3600
                            if  t  >= 24 * 3600 :
                                t  -= 12 * 3600                         # ignore their PM on a 24 hour clock time
                            pass
                        elif g.group(4).lower() != 'am' :
                            t      -=  parse_time_zone(g.group(4))      # convert to GMT
                        pass
                    pass
                pass
            if  t >= 0 :

                return(t)                                               # return the time of day

            pass

        g   = sssss_re.search(s)
        if  g :

            return(float(g.group(1)))

        pass

    return(None)


def parse_time(s, is_time_not_date = False) :
    """
        Parse time in many forms.

        Return a time.time() value, which can easily overflow time.time() if the value is outside 32-bit bounds.

        Also handle stricly time values if is_time_not_date is true, returning 0..86399.

    """

    if  is_time_not_date :
        t   = parse_time_of_day(s)
        if  t != None :

            return(t)

        pass

    t       = None
    tzone   = 0         # time.timezone
    d       = parse_datetime(s)
    if  d  != None :
        try :
            t   = mktime(d.timetuple()) - tzone
            t  += (d.microsecond / 1000000.0)
        except ( ValueError, OverflowError, ) :
            t   = None
        pass
    return(t)


def parse_date(s, min_age = None) :
    """
        Return a date/time from a date string.
        Return it in datetime.datetime form so that it works on a
        32-bit system.

        Return None if the string doesn't pass muster.

    """
    s   = s.strip()
    d   = parse_datetime(s)
    if  d and (min_age != None) and (d.year >= time.localtime(time.time()).tm_year - min_age) :
        d   = None
    return(d)


def parse_non_recent_date(s) :
    """
        Return a date/time from a date string.
        Return it in datetime.datetime form so that it works on a
        32-bit system.

        Return None if the string doesn't pass muster.

    """
    d   = parse_date(s, NON_RECENT_YEARS_AGO)
    return(d)


def age_from_dob(dob, now = None) :
    """ Return the age in years, given the dob in datetime form. """
    now = ((now is None) and time.time()) or now
    now = time.gmtime(now)
    age = now.tm_year - dob.year
    if  now.tm_mon < dob.month :
        age    -= 1
    elif (now.tm_mon == dob.month) and (now.tm_mday < dob.day) :
        age    -= 1
    return(age)


def parse_image_file_date_time(s) :
    """ Return a date/time or None for the given image file name. """
    g       = img_file_re.search(s)
    if  g   :
        try :
            return(datetime.datetime(int(g.group(1)), int(g.group(2)), int(g.group(3)), int(g.group(4)), int(g.group(5)), int(g.group(6))))
        except ( ValueError, OverflowError, ) :
            pass
        pass
    return(None)




#
#
#
if __name__ == '__main__' :

    sys.argv.pop(0)

    dob_test        = []
    if  not len(sys.argv) :             # no cmd line args - put the test values in the command line

        dob_test    = [
                        '  19230405  ',
                        '  19232305  ',
                        '01011965',
                        '01231965',
                        ' 01/01/1991 ',
                        ' 01/23/1970 ',
                        ' 23/1/1970 ',
                        ' 07 / 04/ 2003 ',
                        '07/01/2016 ',                              # January 11, 2020  apparently, this is a "recent" year - to test the NON_RECENT_YEARS_AGO value - so will need updating every year
                      ]

        sys.argv.append("From - Fri Oct 27 16:57:16 2006")
        sys.argv.append("Fri 27 ocT 2006 16:57:16 GMT")
        sys.argv.append("From - Fri Oct 27 16:57:16 1996")
        sys.argv.append("Fri 27 ocT 1996 16:57:16 GMT")
        sys.argv.append("    27 ocT,1996 16:57:16 GMT")
        sys.argv.append("2006-10-27T16:57:16Z")
        sys.argv.append("2006-10-27T15:50:16-01:07")
        sys.argv.append("2006:10:27 16:57:16")
        sys.argv.append("2006:10:27 16:57:16Z")
        sys.argv.append("2006:10:27 15:50:16-01:07")
        sys.argv.append("2006:10:27 17:59:16+01:02")
        sys.argv.append("October  27, 2006 17:59")
        sys.argv.append("October  27  2006 17:59:00")
        sys.argv.append("October, 27  2006 17:59:01")

        sys.argv.append("1234567890")
        sys.argv.append("1234567890.4")
        sys.argv.append("1234567890.6")

        #
        #   test fractions of seconds
        #
        sys.argv.append("2006:10:27 16:57:16.25Z")
        sys.argv.append("2006:10:27 17:59:16.5+01:02")
        sys.argv.append("2006-10-27T16:57:16.25Z")
        sys.argv.append("2006-10-27T15:50:16.5-01:07")

        sys.argv.append("54303 06-10-27 16:57:16 50 0 0 500.0 UTC(NIST) *")

        sys.argv.append("1970-01-01T00:00:00.0000Z")
        sys.argv.append("1970-12-31T12:13:14.1516Z")
        sys.argv.append("1969-12-31T23:59:59.99999Z")
        sys.argv.append("1938-01-01T00:00:00.0000Z")
        sys.argv.append("2037-12-31T23:59:59.99Z")
        sys.argv.append("1993-02-01T00:01:02.03Z")
        sys.argv.append("1993-03-01T00:01:02.03Z")
        sys.argv.append("1994-02-01T00:01:02.03Z")
        sys.argv.append("1994-03-01T00:01:02.03Z")
        sys.argv.append("1995-02-01T00:01:02.03Z")
        sys.argv.append("1995-03-01T00:01:02.03Z")
        sys.argv.append("1996-02-01T00:01:02.03Z")
        sys.argv.append("1996-03-01T00:01:02.03Z")
        sys.argv.append("1997-02-01T00:01:02.03Z")
        sys.argv.append("1997-03-01T00:01:02.03Z")
        sys.argv.append("1999-02-01T00:01:02.03Z")
        sys.argv.append("1999-03-01T00:01:02.03Z")
        sys.argv.append("2000-02-01T00:01:02.03Z")
        sys.argv.append("2000-03-01T00:01:02.03Z")
        sys.argv.append("2001-02-01T00:01:02.03Z")
        sys.argv.append("2001-03-01T00:01:02.03Z")
        sys.argv.append("2000-01-01T00:01:02.03Z")
        sys.argv.append("2000-12-01T23:22:21.20Z")
        sys.argv.append("2038-01-02T03:04:05.06Z")
        sys.argv.append("2038-01-19T03:14:07Z")
        sys.argv.append("2038-01-19T03:14:08Z")
        sys.argv.append("2038-12-11T10:09:08.07Z")
        sys.argv.append("2039-12-11T10:09:08.07Z")

        sys.argv.append("bada October 27,2006 blah")

        sys.argv.append("10 /27/ 2006")
        sys.argv.append("27/ 10 /2006")
        sys.argv.append("10272006")
        sys.argv.append("27102006")
        sys.argv.append("10/27/06")
        sys.argv.append("27 / 10/06")
        sys.argv.append("1027 06")
        sys.argv.append("27 1006")
        sys.argv.append("10/27/1993")
        sys.argv.append("27/10/1993")
        sys.argv.append("10 27 1993")
        sys.argv.append("27101993")
        sys.argv.append(" 10/27/ 93 ")
        sys.argv.append("27/  10  /93")
        sys.argv.append(" 102793 ")
        sys.argv.append("   27   1093  ")

        sys.argv.append("bada 27 October , 2006 blah")
        sys.argv.append("bada 27 Oct 06 blah")
        sys.argv.append("bada 9Oct71 blah")
        sys.argv.append("bada 1Oct71 blah")
        sys.argv.append("bada 10Oct71 blah")
        sys.argv.append("bada 29Oct71 blah")
        sys.argv.append("bada 27 Oct,95 blah")

        sys.argv.append("bada 2006 27 October blah")
        sys.argv.append("bada 1996 27 Oct blah")
        sys.argv.append("bada 71 Oct9 blah")
        sys.argv.append("bada 71Oct1 blah")
        sys.argv.append("bada 7110Oct blah")
        sys.argv.append("bada 71 28Oct ")
        sys.argv.append("bada 95 Oct 27")

        for s in [
                    "01 Dec 69",
                    "01 Feb 70 01",
                    "01 Feb 70 02",
                    "01 Feb 73",
                    "01 May 71",
                    "01 Nov 70 01",
                    "01 Nov 70 02",
                    "02 May 70",
                    "2 May 70",
                    "02 May 70 02",
                    "02 Nov 70 01",
                    "02 Nov 70 02",
                    "2 nov 70 01",
                    "03 12 80",
                    "03 28 80",
                    "03 30 80",
                    "03 80",
                    "03 Feb 70",
                    "03 June 70",
                    "04 02 80",
                    "04 06 80",
                    "04 26 80",
                    "04 30 80",
                    "04 80",
                    "04 Apr 71",
                    "04 Nov 70",
                    "05 08 80",
                    "5 April 70",
                    "05 Apr 70",
                    "05 June 71",
                    "05 Nov 70 01",
                    "05 Nov 70 02",
                    "06 Dec 69",
                    "06 June 70",
                    "06 March 70",
                    "07 Jan 70",
                    "07 July 70 01",
                    "07 July 70 02",
                    "07 May 71",
                    "08 80",
                    "08 July 70 01",
                    "08 July 70 02",
                    "08 June 70",
                    "09 May 70",
                    "9 May 70",
                    "12 March 70",
                    "12 March 70 01",
                    "12 March 70 02",
                    "13 Feb 71 01",
                    "13 June 70",
                    "13 March 70",
                    "14 Dec 69",
                    "14 July 70",
                    "14 Oct 70",
                    "14 Sept 71",
                    "15 Aug 71",
                    "15 Dec 69",
                    "15 Feb 71",
                    "15 Jan 70",
                    "15 June 70",
                    "15 Oct 70",
                    "16 Jan 70",
                    "16 June 70 01",
                    "16 June 70 02",
                    "16 Nov 03",
                    "16 Nov 69",
                    "16 Nov 69 02",
                    "16 Oct 70",
                    "17 June 70",
                    "17 May 70",
                    "18 June 70",
                    "19 Aug 70",
                    "19 Aug 71",
                    "19 Jan 70",
                    "19 Nov 69 01",
                    "19 Nov 69 02",
                    "19 Nov 69 03",
                    "19 Sept 71",
                    "20 June 70 01",
                    "20 June 70 02",
                    "20 June 70 03",
                    "20 June 70 04",
                    "20 Oct 73",
                    "21 July 70 01",
                    "21 July 70 02",
                    "22 Aug 71",
                    "22 Feb 70",
                    "22 Nov 69",
                    "23 May 70",
                    "23 Sept 70",
                    "24 Dec 70",
                    "24 Jun 71",
                    "24 March 70 01",
                    "24 March 70 02",
                    "24 March 70 03",
                    "24 Oct 69",
                    "25 Jan 70 01",
                    "25 Jan 70 02",
                    "25 Jan 70 03",
                    "25 July 70",
                    "26 Dec 69",
                    "26 July 70",
                    "26 June 70 01",
                    "26 June 70 02",
                    "26 Nov 69",
                    "26 Oct 70",
                    "26 Sept 70",
                    "26 Sept 70 02",
                    "26 Sept 70 03",
                    "27 Nov 69",
                    "27 Oct 73",
                    "27 Sept 70 01",
                    "27 Sept 70 02",
                    "29 Aug 71",
                    "29 July 71",
                    "29 Sept 70",
                    "31 Jan 70",
                    "31 Oct 70",
                    "70 July 08 02",
                    "70 July 14",
                    "70 June 12",
                    "70 June 16 02",
                    "70 June 18",
                    "70 June 20 02",
                    "70 June 20 03",
                    "77 78",
                    "79 80",
                    "1986",
                    "April 25 1975",
                    "Aug 02 74",
                    "Aug 11 74",
                    "Dec 28 1970",
                    "Dec 73",
                    "Dec 74",
                    "Dec 75",
                    "Feb 79",
                    "Jan 76",
                    "June 1979",
                    "Nov 70 01",
                    "Nov 70 02",
                    "Nov 75",
                    "Oct 20 1973",
                    "Oct 69 01",
                    "Oct 69 02",
                    "Oct 69 03",
                    "Oct 69 04",
                    "Oct 75",
                    "Oct 85",
                    "Sep 74",
                    "Sep 78",
                    "Sep 80",
                    " Spring 1976",
                    " 2016:5:21 ",
                    " 2016:12:1 ",
                    " 2016:1:1 ",
                    " 1973:5:21 ",
                    " 1973:12:1 ",
                    " 1973:1:1 ",
                    "20100301_100406",
                    "20100203-110507",
                 ]  :
            sys.argv.append(s)

        ts  = ' 10 11  12 am  '
        if  parse_time(ts, is_time_not_date = True) != (((10 * 60) + 11) * 60) + 12.0 :
            print "Bad", ts
            sys.exit(119)
        ts  = ' 10 11  12. am  '
        if  parse_time(ts, is_time_not_date = True) != (((10 * 60) + 11) * 60) + 12.0 :
            print "Bad", ts
            sys.exit(119)
        ts  = ' 10 11  12.4 am  '
        if  parse_time(ts, is_time_not_date = True) != (((10 * 60) + 11) * 60) + 12.4 :
            print "Bad", ts
            sys.exit(119)
        ts  = ' 10 11  12 Gmt  '
        if  parse_time(ts, is_time_not_date = True) != (((10 * 60) + 11) * 60) + 12.0 :
            print "Bad", ts
            sys.exit(119)
        ts  = ' 10 11  12 -0123  '
        if  parse_time(ts, is_time_not_date = True) != (((11 * 60) + 34) * 60) + 12.0 :
            print "Bad", ts, parse_time(ts, is_time_not_date = True), (((11 * 60) + 34) * 60) + 12.0
            sys.exit(119)
        ts  = ' 10 11  12 - 012345.6  '
        if  parse_time(ts, is_time_not_date = True) != (((11 * 60) + 34) * 60) + 57.6 :
            print "Bad", ts, parse_time(ts, is_time_not_date = True), (((11 * 60) + 34) * 60) + 57.6
            sys.exit(119)
        ts  = ' 10 11  12 +0123  '
        if  parse_time(ts, is_time_not_date = True) != ((( 8 * 60) + 48) * 60) + 12.0 :
            print "Bad", ts, parse_time(ts, is_time_not_date = True), (((11 * 60) + 34) * 60) + 12.0
            sys.exit(119)
        ts  = '101159Pm'
        if  parse_time(ts, is_time_not_date = True) != (((22 * 60) + 11) * 60) + 59.0 :
            print "Bad", ts
            sys.exit(119)
        ts  = '10:11:59Pm'
        if  parse_time(ts, is_time_not_date = True) != (((22 * 60) + 11) * 60) + 59.0 :
            print "Bad", ts
            sys.exit(119)
        ts  = '23:11:59'
        if  parse_time(ts, is_time_not_date = True) != (((23 * 60) + 11) * 60) + 59.0 :
            print "Bad", ts
            sys.exit(119)
        ts  = ' 23 :  11  : 59 '
        if  parse_time(ts, is_time_not_date = True) != (((23 * 60) + 11) * 60) + 59.0 :
            print "Bad", ts
            sys.exit(119)
        ts  = '3645'
        if  parse_time(ts, is_time_not_date = True) != ((( 1 * 60) +  0) * 60) + 45.0 :
            print "Bad", ts
            sys.exit(119)
        ts  = '1045'
        if  parse_time(ts, is_time_not_date = True) != (((10 * 60) + 45) * 60) +  0.0 :
            print "Bad", ts
            sys.exit(119)

        ts  = "Z"
        if  parse_time_zone(ts) != 0 :
            print "Bad", ts
            sys.exit(119)
        ts  = "GMT"
        if  parse_time_zone(ts) != 0 :
            print "Bad", ts
            sys.exit(119)
        ts  = "utc"
        if  parse_time_zone(ts) != 0 :
            print "Bad", ts
            sys.exit(119)
        ts  = "-1234"
        if  parse_time_zone(ts) != -((((12 * 60) + 34) * 60) + 0.0) :
            print "Bad", ts, parse_time_zone(ts)
            sys.exit(119)
        ts  = "- 123456.78"
        if  parse_time_zone(ts) != -((((12 * 60) + 34) * 60) + 56.78) :
            print "Bad", ts, parse_time_zone(ts)
            sys.exit(119)
        ts  = "+123456.78"
        if  parse_time_zone(ts) != ((((12 * 60) + 34) * 60) + 56.78) :
            print "Bad", ts, parse_time_zone(ts)
            sys.exit(119)

        print "All times should be 1161968236.0 - except NIST and fractional second times, which should be .5 less - and time tests after NIST."

    tod = False

    for a in xrange(len(sys.argv)) :
        p = sys.argv[a]
        if  p in [ '--time', '--tod', '--time_of_day', '--time-of-day', '--timeofday', ] :
            tod = True
        elif p in [ '--date', '--date_time', '--date-time', '--datetime', '--dt', ] :
            tod = False
        else    :
            print "%50s" % ( p ),

            t = parse_time(p, is_time_not_date = tod)

            if  t == None :
                print "----"
            else :
                try :
                    ts  = time.asctime(time.gmtime(t))
                except ValueError :
                    ts  = ""
                print "%18.6f %s" % ( t, ts )

            pass
        pass


    if  len(dob_test) :
        print   "Non-recent dates:"
        for p in dob_test :
            print "%50s" % ( p ),

            t = parse_non_recent_date(p)

            if  t  == None :
                print "----"
            else :
                ts  = t.ctime()
                print "%s %s" % ( t, ts )

            pass
        pass
    pass



##      Public things.
__ALL__ = [
            'parse_time',
          ]


#
#
#
# eof
