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