forked from anitagraser/TimeManager
-
Notifications
You must be signed in to change notification settings - Fork 0
/
time_util.py
402 lines (321 loc) · 11.9 KB
/
time_util.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
import time
import re # for hacking strftime
import abc
from datetime import datetime, timedelta
from dateutil.relativedelta import relativedelta
from PyQt4.QtCore import QDateTime
import PyQt4.QtCore as QtCore
import bcdate_util
""" A module to have time related functionality """
__author__ = "Karolina Alexiou"
__email__ = "[email protected]"
OGR_DATE_FORMAT = "%Y/%m/%d"
OGR_DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%S"
# TODO: There is also an OGR format with milliseconds
DEFAULT_FORMAT = "%Y-%m-%d %H:%M:%S"
SAVE_STRING_FORMAT = DEFAULT_FORMAT # Used to be: "%Y-%m-%d %H:%M:%S.%f",
# but this format is not portable in Windows because of the %f directive
PENDING = "WILL BE INFERRED"
UTC = "SECONDS FROM EPOCH"
UTC_FLOAT = "SECONDS FROM EPOCH (float)"
NETCDF_BAND = "NetCDF Time Dimension"
NORMAL_MODE = "Normal Mode"
ARCHAELOGY_MODE = "Archaeology Mode"
DINOSAURS_MODE = "Paleontology Mode"
_mode = NORMAL_MODE
def setCurrentMode(new_mode):
global _mode
_mode = new_mode
def getCurrentMode():
return _mode
def setArchDigits(digits):
bcdate_util.setGlobalDigitSetting(digits)
def getArchDigits():
return bcdate_util.getGlobalDigitSetting()
def is_archaelogical():
return _mode == ARCHAELOGY_MODE
class UnsupportedFormatException(Exception):
pass
class DateTypes:
IntegerTimestamps = "IntegerTimestamps"
DatesAsStrings = "DatesAsStrings"
DatesAsStringsArchaelogical = "DatesAsStringsArchaelogical"
DatesAsQDates = "DatesAsQDates"
DatesAsQDateTimes = "DatesAsQDateTimes"
nonQDateTypes = [IntegerTimestamps, DatesAsStrings, DatesAsStringsArchaelogical]
QDateTypes = [DatesAsQDates, DatesAsQDateTimes]
@classmethod
def determine_type(cls, val):
if is_archaelogical():
return cls.DatesAsStringsArchaelogical
try:
float(val)
return cls.IntegerTimestamps
except:
pass
try:
int(val)
return cls.IntegerTimestamps
except:
if type(val) is QtCore.QDate:
return cls.DatesAsQDates
if type(val) is QtCore.QDateTime:
return cls.DatesAsQDateTimes
return cls.DatesAsStrings
@classmethod
def get_type_format(cls, typ):
if typ in cls.nonQDateTypes:
raise Exception
else:
if typ == cls.DatesAsQDates:
return OGR_DATE_FORMAT
if typ == cls.DatesAsQDateTimes:
return OGR_DATETIME_FORMAT
raise Exception
def _cast_to_int_or_float(val):
if int(val) == float(val):
return int(val)
else:
return float(val)
def _str_switch(str, substr1, substr2):
"""Switch the location in a string of two substrings"""
i1 = str.find(substr1)
i2 = str.find(substr2)
if i1 < 0 or i2 < 0:
return str
if i1 < i2:
return str[:i1] + substr2 + str[i1 + len(substr1):i2] + substr1 + str[i2 + len(substr2):]
if i1 == i2:
return str
if i1 > i2:
return str[:i2] + substr1 + str[i2 + len(substr2):i1] + substr2 + str[i1 + len(substr1):]
def generate_all_timezones(fmt):
l = [fmt+" UTC"]
for i in range(-12, 15):
l.append(fmt + ("-" if i < 0 else "+") + str(abs(i)).zfill(2))
return l
BASIC_SUPPORTED_FORMATS = [
"%Y-%m-%d %H:%M:%S.%f",
"%Y-%m-%d %H:%M:%S",
"%Y-%m-%d %H:%M",
"%Y-%m-%dT%H:%M:%S",
"%Y-%m-%dT%H:%M:%SZ",
"%Y-%m-%dT%H:%M",
"%Y-%m-%dT%H:%MZ",
"%Y-%m-%d",
"%Y/%m/%d %H:%M:%S.%f",
"%Y/%m/%d %H:%M:%S",
"%Y/%m/%d %H:%M",
"%Y/%m/%d",
"%H:%M:%S",
"%H:%M:%S.%f",
"%Y.%m.%d %H:%M:%S.%f",
"%Y.%m.%d %H:%M:%S",
"%Y.%m.%d %H:%M",
"%Y.%m.%d",
"%Y%m%d%H%M%SED"
]
YMD_SUPPORTED_FORMATS = BASIC_SUPPORTED_FORMATS + generate_all_timezones("%Y-%m-%d %H:%M:%S")
DMY_SUPPORTED_FORMATS = map(lambda x: _str_switch(x, "%Y", "%d"), YMD_SUPPORTED_FORMATS)
MDY_SUPPORTED_FORMATS = map(lambda x: _str_switch(x, "%m", "%d"), DMY_SUPPORTED_FORMATS)
OTHER_FORMATS = ["%Y-%m"]
SUPPORTED_FORMATS = list(
YMD_SUPPORTED_FORMATS + MDY_SUPPORTED_FORMATS + DMY_SUPPORTED_FORMATS + OTHER_FORMATS)
def is_date_object(val):
return isinstance(val, datetime) or isinstance(val, bcdate_util.BCDate)
def updateUi(ui, val):
if is_archaelogical():
ui.setText(str(val))
return
else:
ui.setDateTime(val)
def get_max_dt():
if is_archaelogical():
return bcdate_util.get_max_dt()
return datetime(9999, 12, 31, 23, 59, 59, 999999)
def get_min_dt():
if is_archaelogical():
return bcdate_util.get_min_dt()
return datetime(1, 1, 1, 0, 0, 0, 0)
def timeval_to_epoch(val, fmt):
"""Converts any string, number, datetime or Qdate or QDatetime to epoch"""
if is_archaelogical():
return bcdate_util.timeval_to_epoch(val)
try:
return int(val)
except:
try:
return float(val)
except:
if type(val) in [QtCore.QDate, QtCore.QDateTime]:
val = QDateTime_to_datetime(val)
if type(val) in [str, basestring, unicode]:
val = str_to_datetime(val, fmt)
return datetime_to_epoch(val)
def timeval_to_datetime(val, fmt):
if is_archaelogical():
return bcdate_util.timeval_to_bcdate(val)
epoch = timeval_to_epoch(val, fmt)
return epoch_to_datetime(epoch)
def QDateTime_to_datetime(date):
try:
return date.toPyDateTime()
except:
return datetime_at_start_of_day(date.toPyDate())
def datetime_at_start_of_day(dt):
return datetime.combine(dt, datetime.min.time())
def datetime_at_end_of_day(dt):
return datetime.combine(dt, datetime.max.time())
def epoch_to_datetime(seconds_from_epoch):
"""Convert seconds since 1970-1-1 (UNIX epoch) to a datetime"""
# This doesnt work on windows for negative timestamps
# http://stackoverflow.com/questions/22082103/on-windows-how-to-convert-a-timestamps-before-1970-into-something-manageable # nopep8
# return datetime.utcfromtimestamp(seconds_from_epoch)
# but this should:
if is_archaelogical():
return bcdate_util.epoch_to_bcdate(seconds_from_epoch)
else:
return datetime(1970, 1, 1) + timedelta(seconds=seconds_from_epoch)
def epoch_to_str(seconds_from_epoch, fmt):
return datetime_to_str(epoch_to_datetime(seconds_from_epoch), fmt)
def datetime_to_epoch(dt):
""" convert a datetime to seconds after (or possibly before) 1970-1-1 """
if is_archaelogical():
return bcdate_util.bcdate_to_epoch(dt)
res = ((dt - datetime(1970, 1, 1)).total_seconds())
return _cast_to_int_or_float(res)
def datetime_to_str(dt, fmt):
""" strftime has a bug for years<1900, so fixing it as well as we can """
if is_archaelogical():
return str(dt)
if "%" not in fmt:
raise Exception(
"{} does not look like a time format for val {} of type {}".format(fmt, dt, type(dt)))
if dt.year >= 1900:
return datetime.strftime(dt, fmt)
else:
return _fixed_strftime(dt, fmt)
# Based on code submitted to comp.lang.python by Andrew Dalke and subsequently used on the django project
# https://github.com/django/django/
# This fix does not support strftime's "%s" or "%y" format strings.
# Allowed if there's an even number of "%"s because they are escaped.
_illegal_formatting = re.compile(r"((^|[^%])(%%)*%[sy])")
def _findall(text, substr):
# Also finds overlaps
sites = []
i = 0
while 1:
j = text.find(substr, i)
if j == -1:
break
sites.append(j)
i = j + 1
return sites
def _fixed_strftime(dt, fmt):
illegal_formatting = _illegal_formatting.search(fmt)
if illegal_formatting:
raise TypeError(
"strftime of dates before 1900 does not handle" + illegal_formatting.group(0))
year = dt.year
# For every non-leap year century, advance by
# 6 years to get into the 28-year repeat cycle
delta = 2000 - year
off = 6 * (delta // 100 + delta // 400)
year = year + off
# Move to around the year 2000
year = year + ((2000 - year) // 28) * 28
timetuple = dt.timetuple()
s1 = time.strftime(fmt, (year,) + timetuple[1:])
sites1 = _findall(s1, str(year))
s2 = time.strftime(fmt, (year + 28,) + timetuple[1:])
sites2 = _findall(s2, str(year + 28))
sites = []
for site in sites1:
if site in sites2:
sites.append(site)
s = s1
syear = "%04d" % (dt.year,)
for site in sites:
s = s[:site] + syear + s[site + 4:]
return s
def get_format_of_timeval(datetimeValue):
typ = DateTypes.determine_type(datetimeValue)
if typ == DateTypes.DatesAsStringsArchaelogical:
return bcdate_util.BC_FORMAT
if typ in DateTypes.QDateTypes:
return DateTypes.get_type_format(typ)
datetimeValue = str(datetimeValue)
# is it an integer representing seconds?
try:
seconds = int(datetimeValue)
return UTC
except:
pass
# is it a float representing seconds and milliseconds after the floating point?
try:
seconds = float(datetimeValue)
return UTC_FLOAT
except:
pass
for format in SUPPORTED_FORMATS:
try:
datetime.strptime(datetimeValue, format)
return format
except:
pass
# If all fail, raise an exception
raise UnsupportedFormatException(
"Could not find a suitable time format for value {}".format(datetimeValue))
def createNiceMessage(dateStr, specified_fmt, is_arch, e):
if is_arch:
return "Data with value {} is not a valid archaelogical format. Cause: {}".format(dateStr,
e) + \
"If you are trying to use a regular date, please disable archaelogical mode by clicking on the" + \
"button next to Settings"
if specified_fmt == PENDING:
return "Could not match value {} with any of the supported formats. Tried: <br> {}".format(
dateStr, '<br>'.join(OTHER_FORMATS + BASIC_SUPPORTED_FORMATS))
else:
return "You specified that the format of {} is {}, but this did not succeed. Please check again".format(
dateStr, specified_fmt)
def str_to_datetime(datetimeString, fmt=PENDING):
"""convert a date/time string into a Python datetime object"""
datetimeString = str(datetimeString)
specified_fmt = fmt
try:
if is_archaelogical():
return bcdate_util.str_to_bcdate(datetimeString)
if fmt == PENDING:
fmt = get_format_of_timeval(datetimeString)
if fmt == UTC:
return epoch_to_datetime(int(datetimeString))
if fmt == UTC_FLOAT:
return epoch_to_datetime(float(datetimeString))
return datetime.strptime(datetimeString, fmt)
except Exception, e:
raise UnsupportedFormatException(
createNiceMessage(datetimeString, specified_fmt, is_archaelogical(), e))
def get_frame_count(start, end, td):
if not is_archaelogical():
try:
td1 = end - start
except: # hope this fixes #17 which I still cannot reproduce
return 0
td2 = td
if isinstance(td2, relativedelta):
# convert back to timedelta
# approximately (it makes the interval <= the actual interval but this way around it doesn't matter)
# for the frame count
td2 = timedelta(weeks=4 * td2.months, days=365 * td2.years)
# this is how you can devide two timedeltas (not supported by default):
us1 = td1.total_seconds()
us2 = td2.total_seconds()
if us2 == 0:
raise Exception("Cannot have zero length timeFrame") # this should never happen
# it's forbidden at UI level
return int(us1 * 1.0 / us2)
else:
years = bcdate_util.BCDate.dist(end, start)
return int(years / td.years)
def is_archaeological_layer(layer):
return layer.getTimeFormat() in [bcdate_util.BC_FORMAT]