-
Notifications
You must be signed in to change notification settings - Fork 2k
/
field_types.py
279 lines (258 loc) · 12.1 KB
/
field_types.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
import re
import time
import datetime
import warnings
with warnings.catch_warnings():
warnings.filterwarnings('ignore', '.*compile_mappers.*')
import formalchemy
from ckan.common import OrderedDict
months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']
class DateConvertError(Exception):
pass
class DateType(object):
'''Utils for handling dates in forms.
* Full or partial dates
* User inputs in form DD/MM/YYYY and it is stored in db as YYYY-MM-DD.
'''
format_types = ('form', 'db')
datetime_fields = OrderedDict([('year', (1000, 2100, 4, 'YYYY')),
('month', (1, 12, 2, 'MM')),
('day', (1, 31, 2, 'DD')),
('hour', (0, 23, 2, 'HH')),
('minute', (0, 59, 2, 'MM')),
])
datetime_fields_indexes = {'min':0, 'max':1, 'digits':2, 'format_code':3}
date_fields_order = {'db':('year', 'month', 'day'),
'form':('day', 'month', 'year')}
parsing_separators = {'date':'-/',
'time':':\.'}
default_separators = {'db':{'date':'-',
'time':':'},
'form':{'date':'/',
'time':':'},}
field_code_map = {'year':'YYYY', 'month':'MM', 'day':'DD',
'hour':'HH', 'minute':'MM'}
word_match = re.compile('[A-Za-z]+')
timezone_match = re.compile('(\s[A-Z]{3})|(\s[+-]\d\d:?\d\d)')
months_abbreviated = [month[:3] for month in months]
@classmethod
def parse_timedate(cls, timedate_str, format_type):
'''Takes a timedate and returns a dictionary of the fields.
* Little validation is done.
* If it can\'t understand the layout it raises DateConvertError
'''
assert format_type in cls.format_types
if not hasattr(cls, 'matchers'):
# build up a list of re matches for the different
# acceptable ways of expressing the time and date
cls.matchers = {}
cls.readable_formats = {}
for format_type_ in cls.format_types:
finished_regexps = []
readable_formats = [] # analogous to the regexps,
# but human readable
year_re = '(?P<%s>\d{2,4})'
month_re = '(?P<%s>\w+)'
two_digit_decimal_re = '(?P<%s>\d{1,2})'
time_re = '%s[%s]%s' % (
two_digit_decimal_re % 'hour',
cls.parsing_separators['time'],
two_digit_decimal_re % 'minute')
time_readable = '%s%s%s' % (
cls.datetime_fields['hour'][cls.datetime_fields_indexes['format_code']],
cls.default_separators[format_type_]['time'],
cls.datetime_fields['minute'][cls.datetime_fields_indexes['format_code']])
date_field_re = {'year':year_re % 'year',
'month':month_re % 'month',
'day':two_digit_decimal_re % 'day'}
date_fields = list(cls.date_fields_order[format_type_])
for how_specific in ('day', 'month', 'year'):
date_sep_re = '[%s]' % cls.parsing_separators['date']
date_sep_readable = cls.default_separators[format_type_]['date']
date_field_regexps = [date_field_re[field] for field in date_fields]
date_field_readable = [cls.datetime_fields[field][cls.datetime_fields_indexes['format_code']] for field in date_fields]
date_re = date_sep_re.join(date_field_regexps)
date_readable = date_sep_readable.join(date_field_readable)
finished_regexps.append(date_re)
readable_formats.append(date_readable)
date_fields.remove(how_specific)
full_date_re = finished_regexps[0]
full_date_readable = readable_formats[0]
# Allow time to be before or after the date
for format_ in ('%(time_re)s%(sep)s%(full_date_re)s',
'%(full_date_re)s%(sep)s%(time_re)s'):
finished_regexps.insert(0, format_ % {
'time_re':time_re,
'sep':'\s',
'full_date_re':full_date_re})
readable_formats.insert(0, format_ % {
'time_re':time_readable,
'sep':' ',
'full_date_re':full_date_readable})
cls.matchers[format_type_] = [re.compile('^%s$' % regexp) for regexp in finished_regexps]
cls.readable_formats[format_type_] = readable_formats
#print format_type_, finished_regexps, readable_formats
for index, matcher in enumerate(cls.matchers[format_type]):
match = matcher.match(timedate_str)
if match:
timedate_dict = match.groupdict()
timedate_dict = cls.int_timedate(timedate_dict)
timedate_dict['readable_format'] = cls.readable_formats[format_type][index]
return timedate_dict
else:
acceptable_formats = ', '.join(["'%s'" % format_ for format_ in cls.readable_formats[format_type]])
raise DateConvertError("Cannot parse %s date '%s'. Acceptable formats: %s" % (format_type, timedate_str, acceptable_formats))
@classmethod
def int_timedate(cls, timedate_dict):
# Convert timedate string values to integers
int_timedate_dict = timedate_dict.copy()
for field in cls.datetime_fields.keys():
if timedate_dict.has_key(field):
val = timedate_dict[field]
if field == 'year':
if len(val) == 2:
# Deal with 2 digit dates
try:
int_val = int(val)
except ValueError:
raise DateConvertError('Expecting integer for %s value: %s' % (field, val))
val = cls.add_centurys_to_two_digit_year(int_val)
elif len(val) == 3:
raise DateConvertError('Expecting 2 or 4 digit year: "%s"' % (val))
if field == 'month':
# Deal with months expressed as words
if val in months:
val = months.index(val) + 1
if val in cls.months_abbreviated:
val = cls.months_abbreviated.index(val) + 1
try:
int_timedate_dict[field] = int(val)
except ValueError:
raise DateConvertError('Expecting integer for %s value: %s' % (field, val))
return int_timedate_dict
@classmethod
def iso_to_db(cls, iso_date, format):
# e.g. 'Wed, 06 Jan 2010 09:30:00'
# '%a, %d %b %Y %H:%M:%S'
assert isinstance(iso_date, (unicode, str))
try:
date_tuple = time.strptime(iso_date, format)
except ValueError, e:
raise DateConvertError('Could not read date as ISO format "%s". Date provided: "%s"' % (format, iso_date))
date_obj = datetime.datetime(*date_tuple[:4])
date_str = cls.date_to_db(date_obj)
return date_str
@classmethod
def strip_iso_timezone(cls, iso_date):
return cls.timezone_match.sub('', iso_date)
@classmethod
def form_to_db(cls, form_str, may_except=True):
'''
27/2/2005 -> 2005-02-27
27/Feb/2005 -> 2005-02-27
2/2005 -> 2005-02
Feb/2005 -> 2005-02
2005 -> 2005
'''
try:
# Allow blank input or None
if not form_str:
return u''
form_str = form_str.strip()
if not form_str:
return u''
# Parse form value
timedate_dict = cls.parse_timedate(form_str, 'form')
# Check range of dates and format as standard string
try:
db_datetime = cls.format(timedate_dict, 'db')
except DateConvertError, e:
msg = 'Date error reading in format \'%s\': %s' % (timedate_dict['readable_format'], ' '.join(e.args))
raise DateConvertError(msg)
return db_datetime
except DateConvertError, e:
if may_except:
raise e
else:
return form_str
@classmethod
def date_to_db(cls, date):
'''
datetime.date(2005, 2, 27) -> 2005-02-27
'''
assert isinstance(date, datetime.date)
date_str = date.strftime('%Y-%m-%d')
return date_str
@classmethod
def format(cls, datetime_dict, format_type):
'''Takes datetime_dict and formats them either for
the form or the database. If it encounters an out
of range value, it raises an exception.
'''
assert isinstance(datetime_dict, dict)
assert format_type in ('form', 'db')
# convert each field to a string
str_datetime_dict = {} # strings by field
for field in cls.datetime_fields:
if not datetime_dict.has_key(field):
break
val = datetime_dict[field]
min_, max_ = cls.datetime_fields[field][cls.datetime_fields_indexes['min']:cls.datetime_fields_indexes['max'] + 1]
if val < min_ or val > max_:
raise DateConvertError('%s value of "%s" is out of range.' % (field.capitalize(), val))
if format_type == 'form':
int_format_string = '%d'
elif format_type == 'db':
num_digits = cls.datetime_fields['hour'][cls.datetime_fields_indexes['digits']]
int_format_string = '%%0%sd' % num_digits
str_datetime_dict[field] = int_format_string % val
# assemble the date
date_fields = []
for field in cls.date_fields_order[format_type]:
if str_datetime_dict.has_key(field):
date_fields.append(str_datetime_dict[field])
formatted_datetime = unicode(cls.default_separators[format_type]['date'].join(date_fields))
# add in the time if specified
if str_datetime_dict.has_key('hour'):
if format_type == 'form':
datetime_format_string = '%(hour)s%(time_separator)s%(minute)s %(date)s'
elif format_type == 'db':
datetime_format_string = '%(date)s %(hour)s%(time_separator)s%(minute)s'
format_dict = str_datetime_dict.copy()
format_dict['date'] = formatted_datetime
format_dict['time_separator'] = cls.default_separators[format_type]['time']
formatted_datetime = datetime_format_string % format_dict
return formatted_datetime
@staticmethod
def form_validator(form_date_str, field=None):
try:
DateType.form_to_db(form_date_str)
except DateConvertError, e:
raise formalchemy.ValidationError(e)
@classmethod
def db_to_form(cls, db_str):
'2005-02-27 -> 27/2/2005 if correct format, otherwise, display as is.'
db_str = db_str.strip()
if not db_str:
return db_str
try:
timedate_dict = cls.parse_timedate(db_str, 'db')
except DateConvertError, e:
# cannot parse - simply display as-is
return db_str
try:
datetime_form = cls.format(timedate_dict, 'form')
except DateConvertError, e:
# values out of range - simply display as-is
return db_str
return datetime_form
@classmethod
def add_centurys_to_two_digit_year(cls, year, near_year=2010):
assert isinstance(year, int)
assert isinstance(near_year, int)
assert year < 1000, repr(year)
assert near_year > 1000 and near_year < 2200, repr(near_year)
year += 1000
while abs(year - near_year) > 50:
year += 100
return year