Attachment 'Formatter.py'
Download 1 # Formatter.py
2 #
3 # ------------------------------------------------------------
4 # Copyright 2002, 2004 by Samuel Reynolds. All rights reserved.
5 #
6 # Permission to use, copy, modify, and distribute this software and its
7 # documentation for any purpose and without fee is hereby granted,
8 # provided that the above copyright notice appear in all copies and that
9 # both that copyright notice and this permission notice appear in
10 # supporting documentation, and that the name of Samuel Reynolds
11 # not be used in advertising or publicity pertaining to distribution
12 # of the software without specific, written prior permission.
13 #
14 # SAMUEL REYNOLDS DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
15 # INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO
16 # EVENT SHALL SAMUEL REYNOLDS BE LIABLE FOR ANY SPECIAL, INDIRECT, OR
17 # CONSEQUENTIAL DAMAGES, OR FOR ANY DAMAGES WHATSOEVER RESULTING FROM
18 # LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
19 # NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
20 # WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
21 # ------------------------------------------------------------
22
23
24 """
25 Formatters for converting and validating data values.
26 """
27
28 import copy, re, time
29
30 class Formatter( object ):
31 """
32 Formatter/validator for data values.
33 """
34 def __init__( self, *args, **kwargs ):
35 pass
36
37 # Default (dummy) validate routine
38 def validate( self, value ):
39 """
40 Return true if value is valid for the field.
41 value is a string from the UI.
42 """
43 return True
44
45 # Default (dummy) format routine
46 def format( self, value ):
47 """Format a value for presentation in the UI."""
48 if value == None:
49 return ''
50 return str(value)
51
52 # Default (dummy) coerce routine
53 def coerce( self, value ):
54 """Convert a string from the UI into a storable value."""
55 return value
56
57
58 class EnumFormatter( Formatter ):
59 """
60 Formatter for enumerated (EnumType) data values.
61 """
62 def __init__( self, enumeration, *args, **kwargs ):
63
64 super(EnumFormatter,self).__init__( *args, **kwargs )
65
66 self.enumeration = enumeration
67
68
69 def validValues( self ):
70 """
71 Return list of valid value (id,label) pairs.
72 """
73 return copy.copy( self.enumeration.items() )
74
75
76 def validate( self, value ):
77 """
78 Return true if value is valid for the field.
79 value is a string from the UI.
80 """
81 vv = [ s for i, s in self.validValues() ]
82 return ( value in vv )
83
84
85 def format( self, value ):
86 """Format a value for presentation in the UI."""
87 return self.enumeration[value]
88
89
90 def coerce( self, value ):
91 """Convert a string from the UI into a storable value."""
92 return getattr( self.enumeration, value )
93
94
95 class FormatterMeta( type ):
96 """
97 Metaclass for subclasses of Formatter.
98
99 Each instance class MUST define either validate(self, value) method
100 or re_validation regular expression string.
101
102 If the latter, the validate method will be autogenerated.
103
104 If re_validation is defined, validate method is overridden to
105 validate the string value against the regular expression.
106
107 If re_validation_flags is defined, the flags will be
108 used when the re_validation regular expression string is compiled.
109
110 Each instance class MAY define:
111 - format(self, value) method.
112 - coerce(self, value) method.
113 """
114 def __new__( cls, classname, bases, classdict ):
115 newdict = copy.copy( classdict )
116
117 # Generate __init__ method
118 # Direct descendants of Formatter automatically get __init__.
119 # Indirect descendants don't automatically get one.
120 if Formatter in bases:
121 def __init__( self, *args, **kwargs ):
122 Formatter.__init__( self, *args, **kwargs )
123 initialize = getattr( self, 'initialize', None )
124 if initialize:
125 initialize()
126 newdict['__init__'] = __init__
127 else:
128 def __init__( self, *args, **kwargs ):
129 super(self.__class__,self).__init__( *args, **kwargs)
130 initialize = getattr( self, 'initialize', None )
131 if initialize:
132 initialize()
133 newdict['__init__'] = __init__
134
135 # Generate validate-by-RE method if specified
136 re_validation = newdict.get( 're_validation', None )
137 if re_validation:
138 # Override validate method
139 re_validation_flags = newdict.get( 're_validation_flags', 0 )
140 newdict['_re_validation'] = re.compile( re_validation, re_validation_flags )
141 def validate( self, value ):
142 return ( self._re_validation.match( value ) != None )
143 newdict['validate'] = validate
144
145 # Delegate class creation to the expert
146 return type.__new__( cls, classname, bases, newdict )
147
148
149 class ObjectIdFormatter( Formatter ):
150 """
151 Object ID is assumed to be a large (32 bit?) unsigned integer.
152 """
153 __metaclass__ = FormatterMeta
154 re_validation = '^[0-9]+$'
155 def coerce( self, value ):
156 if value: return long(value)
157 return value
158
159 class StringFormatter( Formatter ):
160 __metaclass__ = FormatterMeta
161
162 class AlphaFormatter( StringFormatter ):
163 """Alphabetic characters only."""
164 re_validation = '^[a-zA-Z]*$'
165
166 class AlphaNumericFormatter( StringFormatter ):
167 """Alphanumeric characters only."""
168 re_validation = '^[a-zA-Z0-9]*$'
169
170 class EmailFormatter( StringFormatter ):
171 """Internet email addresses (more or less)."""
172 # This regex does not match all legal email addresses, but
173 # it does a pretty good job.
174 # Strangely enough, '/' is legal in email addresses.
175 # However, I've never seen it used, so I prefer to leave it out.
176 _re_subs = {
177 'sub1' : r'[a-zA-Z~_-][a-zA-Z0-9_:~-]*',
178 'sub2' : r'(\.[a-zA-Z0-9_:~-]+)*',
179 'sfx' : r'\.[a-zA-Z]{2,3}'
180 }
181 re_validation = '^%(sub1)s%(sub2)s[@]%(sub1)s%(sub2)s%(sfx)s$' % _re_subs
182
183 class MoneyFormatter( StringFormatter ):
184 """Assumes decimal money, but doesn't assume currency."""
185 re_validation = '^(([0-9]+([.][0-9]{2})?)|([0-9]*[.][0-9]{2}))$'
186
187 class IntFormatter( Formatter ):
188 """Signed or unsigned integer."""
189 __metaclass__ = FormatterMeta
190 #re_validation = '^[-+]?[0-9]+$'
191 def validate( self, value ):
192 try:
193 v = int( value )
194 return True
195 except:
196 return False
197 def coerce( self, value ):
198 if value: return int(value)
199 return value
200
201 class Int8Formatter( IntFormatter ):
202 pass
203
204 class Int16Formatter( IntFormatter ):
205 pass
206
207 class Int24Formatter( IntFormatter ):
208 def coerce( self, value ):
209 if value: return long(value)
210 return value
211
212 class Int32Formatter( IntFormatter ):
213 def coerce( self, value ):
214 if value: return long(value)
215 return value
216
217 class Int64Formatter( IntFormatter ):
218 def coerce( self, value ):
219 if value: return long(value)
220 return value
221
222 class UIntFormatter( Formatter ):
223 """Unsigned integer."""
224 __metaclass__ = FormatterMeta
225 re_validation = '^[0-9]+$'
226 def coerce( self, value ):
227 if value: return int(value)
228 return value
229
230 class UInt8Formatter( UIntFormatter ):
231 pass
232
233 class UInt16Formatter( UIntFormatter ):
234 def coerce( self, value ):
235 if value: return long(value)
236 return value
237
238 class UInt24Formatter( UIntFormatter ):
239 def coerce( self, value ):
240 if value: return long(value)
241 return value
242
243 class UInt32Formatter( UIntFormatter ):
244 def coerce( self, value ):
245 if value: return long(value)
246 return value
247
248 class FloatFormatter( Formatter ):
249 """Signed or unsigned floating-point number."""
250 __metaclass__ = FormatterMeta
251 re_validation = '^[-+]?(([0-9]+[.]?[0-9]*)|([0-9]*[.]?[0-9]+))$'
252 def coerce( self, value ):
253 if value: return float(value)
254 return value
255
256 class DoubleFormatter( FloatFormatter ):
257 pass
258
259 class UFloatFormatter( Formatter ):
260 """Unsigned floating-point number."""
261 __metaclass__ = FormatterMeta
262 re_validation = '^(([0-9]+[.]?[0-9]*)|([0-9]*[.]?[0-9]+))$'
263 def coerce( self, value ):
264 if value: return float(value)
265 return value
266
267 class UDoubleFormatter( UFloatFormatter ):
268 pass
269
270
271 class TextFormatter( Formatter ):
272 __metaclass__ = FormatterMeta
273
274
275 class TimeElapsedFormatter( Formatter ):
276 """Elapsed time string (HH:MM:SS)."""
277 __metaclass__ = FormatterMeta
278 re_validation = '^([1][0-2]|[0]?[0-9]):[0-5][0-9](:[0-5][0-9])?$'
279
280 class DateFormatter( Formatter ):
281 """
282 Date string (YYYY-MM-DD).
283
284 Storage format: YYYY-MM-DD
285 Presentation format: YYYY-MM-DD
286
287 Accepts only YYYY-MM-DD format (allows variant separators '/' and '.').
288 Accepts dates in range (1000-2999)-(01-12)-(01-31).
289 Leading zeros optional in month and day.
290 Does not enforce # of days in month.
291 """
292 __metaclass__ = FormatterMeta
293
294 re_validation = r'^[1-2][0-9]{3}([-/.])([0][1-9]|[1][0-2])\1([0][1-9]|[12][0-9]|[3][0-1])$'
295
296 def coerce( self, value ):
297 """Convert alternate date separators to '-'."""
298 return re.sub( r'[/.]', '-', value )
299
300 class DateFormatterMDY( DateFormatter ):
301 """Alternate date string (MM-DD-YYYY).
302
303 Storage format: YYYY-MM-DD
304 Presentation format: MM-DD-YYYY
305
306 Accepts only MM-DD-YYYY format (allows variant separators '/' and '.').
307 Accepts dates in range (01-12)-(01-31)-(1000-2999).
308 Leading zeros optional in month and day.
309 Does not enforce # of days in month.
310 """
311 re_validation = r'^([0][1-9]|[1][0-2])([-/.])([0][1-9]|[12][0-9]|[3][0-1])\1[12][0-9]{3}$'
312
313 def format( self, value ):
314 dt = time.strptime( value, '%Y-%m-%d' )
315 return time.strftime( '%m-%d-%Y', dt )
316
317 def coerce( self, value ):
318 # value = re.sub( r'[/.]', '-', value )
319 # dt = time.strptime( value, '%m-%d-%Y' )
320 # return time.strftime( '%Y-%m-%d', dt )
321 m, d, y = re.split( '[-/.]', value )
322 return '%04d-%02d-%02d' % ( int(y), int(m), int(d) )
323
324 class TimeFormatter( Formatter ):
325 """
326 Time string (12-hour or 24-hour format, with or without seconds or am/pm).
327
328 Storage format: HH:MM:SS -- 24-hour format.
329 Presentation format: HH:MM -- 24-hour format.
330
331 Accepts 12-hour or 24-hour format, with or without seconds or am/pm.
332 """
333 __metaclass__ = FormatterMeta
334
335 reTime24 = r'(([0]?[0-9]|[1][0-9]|[2][0-3]):[0-5][0-9](:[0-5][0-9])?)'
336 reTimeAP = r'(([1][0-2]|[0]?[0-9]):[0-5][0-9](:[0-5][0-9])?[ ]*([aApP][mM])?)'
337 re_validation = r'^%s|%s$' % ( reTime24, reTimeAP )
338
339 def format( self, value ):
340 return ':'.join( value.split(':')[:2] )
341
342 def coerce( self, value ):
343 for fmt in ( '%H:%M:%S', '%H:%M', '%I:%M:%S %p', '%I:%M %p','%I:%M:%S' ):
344 try:
345 dt = time.strptime( value, fmt )
346 break
347 except ValueError:
348 pass
349 return time.strftime( '%H:%M:%S', dt )
350
351 class TimeFormatter12H( TimeFormatter ):
352 """
353 Alternate time string (12-hour format, without seconds).
354
355 Storage format: HH:MM(:SS) -- 24-hour format.
356 Presentation format: HH:MM( aa) -- 12-hour format.
357 (aa may be 'am' or 'pm')
358 """
359
360 def format( self, value ):
361 dt = time.strptime( value, '%H:%M:%S' )
362 return time.strftime( '%I:%M %p', dt )
363
364 class DateTimeFormatter( Formatter ):
365 """
366 Date/time string.
367
368 Uses a DateFormatter and a TimeFormatter.
369 """
370 __metaclass__ = FormatterMeta
371
372 # Storage format: YYYY-MM-DD HH:MM:SS -- 24-hour format.
373 # Presentation format: same as storage format.
374
375 def validate( self , value ):
376 datef = DateFormatter()
377 timef = TimeFormatter()
378 date, time = re.split( r'[ ]+', value )
379 return ( datef.validate( date ) and timef.validate( time ) )
380
381
382 if __name__ == '__main__':
383
384 from EnumType import EnumType
385
386 # from prs.PRSObject import PRSObject
387 #
388 # print 'dir(ObjectIdField) =', dir(ObjectIdField)
389 # print 'ObjectIdField.__init__ =', ObjectIdField.__init__
390 # print 'ObjectIdField.__init__.func_doc =', ObjectIdField.__init__.func_doc
391 # anIdField = ObjectIdFormatter( False )
392 # print 'dir(anIdField) =', dir(anIdField)
393 # print 'anIdField.__class__ =', anIdField.__class__
394 #
395 # print
396 # print 'dir(DateFormatterMDY) =', dir(DateFormatterMDY)
397 # print
398
399 # ========== EnumFormatter ==========
400 print
401 print 'EnumFormatter'
402 TestEnum = EnumType( 'A', 'B', 'C', 'D' )
403 formatter = EnumFormatter( TestEnum )
404 passCount = 0
405 failCount = 0
406
407 # formatter.validValues
408 expectedValidValues = [ (0,'A'), (1,'B'), (2,'C'), (3,'D') ]
409 temp = formatter.validValues()
410 if temp == expectedValidValues:
411 passCount += 1
412 else:
413 failCount += 1
414 print 'formatter.validValues() = %r -- WRONG -- expected %r' % (temp, expectedValidValues )
415
416 # formatter.format and formatter.coerce
417 for id, name in TestEnum.items():
418 # formatter.format
419 temp = formatter.format( id )
420 if temp == name:
421 passCount += 1
422 else:
423 failCount += 1
424 print 'formatter.format(%d) = %r -- WRONG -- expected %r' % (id, temp, name )
425 # formatter.coerce
426 temp = formatter.coerce( name )
427 if temp == id:
428 passCount += 1
429 else:
430 failCount += 1
431 print 'formatter.coerce(%r) = %r -- WRONG -- expected %r' % (name, temp, id )
432 if failCount == 0:
433 print '\tPASS (%d tests)' % passCount
434
435
436 # ========== EmailFormatter ==========
437 print
438 print 'EmailFormatter'
439
440 tests = [
441 ( 'a@b.com', True ),
442 ( 'a@b', False ),
443 ( '@b', False ),
444 ( '@b.c', False ),
445 ( '@b.us', False ),
446 ( 'a@b.us', True ),
447 ( '~joe_public-73@bozo.net', True ),
448 ]
449 formatter = EmailFormatter()
450 passCount = 0
451 failCount = 0
452
453 for email, expect in tests:
454 actual = formatter.validate( email )
455 if actual == expect:
456 passCount += 1
457 else:
458 failCount += 1
459 if failCount == 0:
460 print '\tPASS (%d tests)' % passCount
Attached Files
To refer to attachments on a page, use attachment:filename, as shown below in the list of files. Do NOT use the URL of the [get] link, since this is subject to change and can break easily.You are not allowed to attach a file to this page.