1#    Copyright (C) 2009 Jeremy S. Sanders
2#    Email: Jeremy Sanders <jeremy@jeremysanders.net>
3#
4#    This program is free software; you can redistribute it and/or modify
5#    it under the terms of the GNU General Public License as published by
6#    the Free Software Foundation; either version 2 of the License, or
7#    (at your option) any later version.
8#
9#    This program is distributed in the hope that it will be useful,
10#    but WITHOUT ANY WARRANTY; without even the implied warranty of
11#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12#    GNU General Public License for more details.
13#
14#    You should have received a copy of the GNU General Public License along
15#    with this program; if not, write to the Free Software Foundation, Inc.,
16#    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17###############################################################################
18
19from __future__ import division
20import math
21import datetime
22import re
23
24import numpy as N
25
26from ..compat import crange, citems, cstr
27
28# date format: YYYY-MM-DDTHH:MM:SS.mmmmmm
29# date and time part are optional (check we have at least one!)
30date_re = re.compile( r'''
31^
32# match date YYYY-MM-DD
33([0-9]{4}-[0-9]{1,2}-[0-9]{1,2})?
34[ ,A-Za-z]?
35# match time HH:MM:SS
36([0-9]{1,2}:[0-9]{1,2}:[0-9]{1,2}(\.[0-9]+)?)?
37$
38''', re.VERBOSE )
39
40# we store dates as intervals in sec from this date as a float
41offsetdate = datetime.datetime(2009, 1, 1, 0, 0, 0, 0)
42# this is the numpy version of this
43offsetdate_np = N.datetime64('2009-01-01T00:00')
44
45def isDateTime(datestr):
46    """Check date/time string looks valid."""
47    match = date_re.match(datestr)
48
49    return match and (match.group(1) is not None or match.group(2) is not None)
50
51def _isoDataStringToDate(datestr):
52    """Convert ISO format date time to a datetime object."""
53    match = date_re.match(datestr)
54    val = None
55    if match:
56        try:
57            dategrp, timegrp = match.group(1), match.group(2)
58            if dategrp:
59                # if there is a date part of the string
60                dateval = [int(x) for x in dategrp.split('-')]
61                if len(dateval) != 3:
62                    raise ValueError("Invalid date '%s'" % dategrp)
63            else:
64                dateval = [2009, 1, 1]
65
66            if timegrp:
67                # if there is a time part of the string
68                p = timegrp.split(':')
69                if len(p) != 3:
70                    raise ValueError("Invalid time '%s'" % timegrp)
71                secfrac, sec = math.modf(float(p[2]))
72                timeval = [ int(p[0]), int(p[1]), int(sec), int(secfrac*1e6) ]
73            else:
74                timeval = [0, 0, 0, 0]
75
76            # either worked so return a datetime object
77            if dategrp or timegrp:
78                val = datetime.datetime( *(dateval+timeval) )
79
80        except ValueError:
81            # conversion failed, return nothing
82            pass
83
84    return val
85
86def dateStringToDate(datestr):
87    """Interpret a date string and return a Veusz-format date value."""
88    dt = _isoDataStringToDate(datestr)
89    if dt is None:
90        # try local conversions (time, date and variations)
91        # if ISO formats don't match
92        for fmt in ('%X %x', '%x %X', '%x', '%X'):
93            try:
94                dt = datetime.datetime.strptime(datestr, fmt)
95                break
96            except (ValueError, TypeError):
97                pass
98
99    if dt is not None:
100        delta = dt - offsetdate
101        return (delta.days*24*60*60 + delta.seconds +
102                delta.microseconds*1e-6)
103    else:
104        return N.nan
105
106def floatUnixToVeusz(f):
107    """Convert unix float to veusz float."""
108    delta = datetime.datetime(1970,1,1) - offsetdate
109    return f + delta.total_seconds()
110
111def floatToDateTime(f):
112    """Convert float to datetime."""
113    days = int(f/24/60/60)
114    frac, sec = math.modf(f - days*24*60*60)
115    try:
116        return datetime.timedelta(days,  sec,  frac*1e6) + offsetdate
117    except OverflowError:
118        return datetime.datetime(8000, 1, 1)
119
120def dateFloatToString(f):
121    """Convert date float to string."""
122    if N.isfinite(f):
123        return floatToDateTime(f).isoformat()
124    else:
125        return cstr(f)
126
127def datetimeToTuple(dt):
128    """Return tuple (year,month,day,hour,minute,second,microsecond) from
129    datetime object."""
130    return (dt.year, dt.month, dt.day, dt.hour, dt.minute,
131            dt.second, dt.microsecond)
132
133def datetimeToFloat(dt):
134    """Convert datetime to float"""
135    delta = dt - offsetdate
136    # convert offset into a delta
137    val = (delta.days*24*60*60 + (delta.seconds +
138           delta.microseconds*1e-6))
139    return val
140
141def tupleToFloatTime(t):
142    """Convert a tuple interval to a float style datetime"""
143    dt = datetime.datetime(*t)
144    return datetimeToFloat(dt)
145
146def tupleToDateTime(t):
147    """Convert a tuple to a datetime"""
148    return datetime.datetime(*t)
149
150def addTimeTupleToDateTime(dt,  tt):
151    """Add a time tuple in the form (yr,mn,dy,h,m,s,us) to a datetime.
152    Returns datetime
153    """
154
155    # add on most of the time intervals
156    dt = dt + datetime.timedelta(days=tt[2], hours=tt[3],
157                                 minutes=tt[4], seconds=tt[5],
158                                 microseconds=tt[6])
159
160    # add on years
161    dt = dt.replace(year=dt.year + tt[0])
162
163    # add on months - this could be much simpler
164    if tt[1] > 0:
165        for i in crange(tt[1]):
166            # find interval between this month and next...
167            m, y = dt.month + 1, dt.year
168            if m == 13:
169                m = 1
170                y += 1
171            dt = dt.replace(year=y, month=m)
172    elif tt[1] < 0:
173        for i in crange(abs(tt[1])):
174            # find interval between this month and next...
175            m, y = dt.month - 1, dt.year
176            if m == 0:
177                m = 12
178                y -= 1
179            dt = dt.replace(year=y, month=m)
180
181    return dt
182
183def roundDownToTimeTuple(dt,  tt):
184    """Take a datetime, and round down using the (yr,mn,dy,h,m,s,ms) tuple.
185    Returns a tuple."""
186
187    #print "round down",  dt,  tt
188    timein = list(datetimeToTuple(dt))
189    i = 6
190    while i >= 0 and tt[i] == 0:
191        if i == 1 or i == 2: # month, day
192            timein[i] = 1
193        else:
194            timein[i] = 0
195        i -= 1
196    # round to nearest interval
197    if (i == 1 or i == 2): # month, day
198        timein[i] = ((timein[i]-1) // tt[i])*tt[i] + 1
199    else:
200        timein[i] = (timein[i] // tt[i])*tt[i]
201
202    #print "rounded",  timein
203    return tuple(timein)
204
205def dateStrToRegularExpression(instr):
206    """Convert date-time string to regular expression.
207
208    Converts format yyyy-mm-dd|T|hh:mm:ss to re for date
209    """
210
211    # first rename each special string to a unique string (this is a
212    # unicode character which is in the private use area) then rename
213    # back again to the regular expression. This avoids the regular
214    # expression being remapped.
215    maps = (
216            ('YYYY', u'\ue001', r'(?P<YYYY>[0-9]{4})'),
217            ('YY',   u'\ue002', r'(?P<YY>[0-9]{2})'),
218            ('MM',   u'\ue003', r'(?P<MM>[0-9]{2})'),
219            ('M',    u'\ue004', r'(?P<MM>[0-9]{1,2})'),
220            ('DD',   u'\ue005', r'(?P<DD>[0-9]{2})'),
221            ('D',    u'\ue006', r'(?P<DD>[0-9]{1,2})'),
222            ('hh',   u'\ue007', r'(?P<hh>[0-9]{2})'),
223            ('h',    u'\ue008', r'(?P<hh>[0-9]{1,2})'),
224            ('mm',   u'\ue009', r'(?P<mm>[0-9]{2})'),
225            ('m',    u'\ue00a', r'(?P<mm>[0-9]{1,2})'),
226            ('ss',   u'\ue00b', r'(?P<ss>[0-9]{2}(\.[0-9]*)?)'),
227            ('s',    u'\ue00c', r'(?P<ss>[0-9]{1,2}(\.[0-9]*)?)'),
228        )
229
230    out = []
231    for p in instr.split('|'):
232        # escape special characters (non alpha-num)
233        p = re.escape(p)
234
235        # replace strings with characters
236        for search, char, repl in maps:
237            p = p.replace(search, char, 1)
238        # replace characters with re strings
239        for search, char, repl in maps:
240            p = p.replace(char, repl, 1)
241
242        # save as an optional group
243        out.append( '(?:%s)?' % p )
244
245    # return final expression
246    return r'^\s*%s\s*$' % (''.join(out))
247
248def dateREMatchToDate(match):
249    """Take match object for above regular expression,
250    and convert to float date value."""
251
252    if match is None:
253        raise ValueError("match object is None")
254
255    # remove None matches
256    grps = {}
257    for k, v in citems(match.groupdict()):
258        if v is not None:
259            grps[k] = v
260
261    # bomb out if nothing matches
262    if len(grps) == 0:
263        raise ValueError("no groups matched")
264
265    # get values of offset
266    oyear = offsetdate.year
267    omon = offsetdate.month
268    oday = offsetdate.day
269    ohour = offsetdate.hour
270    omin = offsetdate.minute
271    osec = offsetdate.second
272    omicrosec = offsetdate.microsecond
273
274    # now convert each element from the re
275    if 'YYYY' in grps:
276        oyear = int(grps['YYYY'])
277    if 'YY' in grps:
278        y = int(grps['YY'])
279        if y >= 70:
280            oyear = int('19' + grps['YY'])
281        else:
282            oyear = int('20' + grps['YY'])
283    if 'MM' in grps:
284        omon = int(grps['MM'])
285    if 'DD' in grps:
286        oday = int(grps['DD'])
287    if 'hh' in grps:
288        ohour = int(grps['hh'])
289    if 'mm' in grps:
290        omin = int(grps['mm'])
291    if 'ss' in grps:
292        s = float(grps['ss'])
293        osec = int(s)
294        omicrosec = int(1e6*(s-osec))
295
296    # convert to python datetime object
297    d = datetime.datetime(
298        oyear, omon, oday, ohour, omin, osec, omicrosec)
299
300    # return to veusz float time
301    return datetimeToFloat(d)
302