- Author:
- mike_dibernardo
- Posted:
- March 10, 2008
- Language:
- Python
- Version:
- .96
- Score:
- 2 (after 2 ratings)
A TimeField that lets you parse a wide variety of freeform-text time descriptions. This doesn't inherit from TimeField because it doesn't use any of its functionality.
Includes unit tests demonstrating some examples of what the parser will and won't handle.
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 | # Author: Michael DiBernardo ([email protected])
from django import newforms as forms
from django.newforms import ValidationError
import datetime
import re
class KungfuTimeField(forms.Field):
"""
Extension to Django's time fields that parses a much larger range of times
without explicitly needing to specify all the time formats yourself.
"""
# Matches any string with a 24-hourish format (sans AM/PM) but puts no
# limits on the size of the numbers (e.g. 64:99 is OK.)
_24_HOUR_PATTERN_STRING = r'^\s*(?P<hour>\d\d?)\s*:?\s*(?P<minute>\d\d)?\s*'
# Matches any string with a 12-hourish format (with AM/PM) but puts no
# limits on the size of the numbers (e.g. 64:99pm is OK.)
_TIME_PATTERN_STRING = _24_HOUR_PATTERN_STRING + \
r'((?P<ampm>[AaPp])\s*\.?\s*[Mm]?\s*\.?\s*)?$'
# Matcher for our time pattern.
_TIME_PATTERN = re.compile(_TIME_PATTERN_STRING)
# Validation error messages.
_ERROR_MESSAGES = {
'invalid' : u'Enter a valid time.',
}
def __init__(self, *args, **kwargs):
"""
Create a new KungFuTimeField with the mojo of a thousand TimeFields.
"""
super(KungfuTimeField, self).__init__(*args, **kwargs)
def clean(self, value):
"""
Parses datetime from the given value. If it can't figure it out, throws
a ValidationError.
"""
super(KungfuTimeField, self).clean(value)
if not value:
return None
if isinstance(value, datetime.time):
return value
cleaned = self._parse_time(value)
return cleaned
def _parse_time(self, value):
"""
Tries to recognize a time using our hefty regexp. If it doesn't match
the regexp, throws a validation error. If it DOES match but the
resulting numbers are out of range (e.g. an hour of 99), will also
throw a ValidationError.
"""
match = self._TIME_PATTERN.match(value)
if not match:
raise ValidationError(self._ERROR_MESSAGES['invalid'])
# Hour has to be there because it's required in the regexp. Set the
# minute to 0 for now. We dunno if there's AM/PM or not.
(hour, minute, ampm) = (int(match.group('hour')), 0, match.group('ampm'))
# Let's see if the user typed a minute.
try:
# Raises TypeError if group fetch returns None.
minute = int(match.group('minute'))
except TypeError:
pass
if ampm:
hour = self._handle_twelve_hour_time(hour, minute, ampm)
# If the numbers are out of range, we'll find out here.
try:
return datetime.time(hour, minute)
except ValueError:
raise ValidationError(self._ERROR_MESSAGES['invalid'])
# I don't expect any other problems, but if there are, they'll
# propagate.
def _handle_twelve_hour_time(self, hour, minute, ampm):
"""
Detect 24 hour time with an am/pm (e.g. 18:30pm, 0:30am) and do the
necessary transform for converting pm times to 24 hour times.
"""
if hour < 1 or hour > 12:
raise ValidationError(self._ERROR_MESSAGES['invalid'])
elif ampm.lower() == "a" and hour == 12:
return 0
elif ampm.lower() == "p" and 1 <= hour and hour <= 11:
return hour + 12
else:
return hour
"""
Tests that our extension to the Django time field can parse a wide variety of
time formats.
"""
# Author: Michael DiBernardo ([email protected])
from formhelpers.fields import KungfuTimeField
from django.newforms import ValidationError
import datetime as dt
import unittest
class TestTimeParsing(unittest.TestCase):
def setUp(self):
self.field = KungfuTimeField()
def test24HourFormats(self):
"""
Tests a variety of 24 hour formats.
"""
twofour_hour_tests = (
("8:30", dt.time(8, 30)),
("14:30", dt.time(14, 30)),
("8", dt.time(8)),
("16", dt.time(16)),
("1742", dt.time(17, 42)),
("856", dt.time(8, 56)),
("101", dt.time(1, 01)),
("1 01", dt.time(1, 01)),
("13 01", dt.time(13, 01)),
("01 01", dt.time(1, 1)),
(" 13 01 ", dt.time(13, 01)),
)
for (input, expected) in twofour_hour_tests:
self.assertTimeEquals(input, expected)
def test12HourFormats(self):
"""
Tests a variety of 12 hour formats.
"""
twelve_hour_tests = (
("12:30pm", dt.time(12, 30)),
("12:30PM", dt.time(12, 30)),
("12:30P.m.", dt.time(12, 30)),
("12:30 pm ", dt.time(12, 30)),
("12:30 P m", dt.time(12, 30)),
("12:30 p . M .", dt.time(12, 30)),
("12:30p", dt.time(12, 30)),
("12:30 p", dt.time(12, 30)),
("12 30pm", dt.time(12, 30)),
("12 30 pm", dt.time(12, 30)),
("1230 p m", dt.time(12, 30)),
(" 1230 p m", dt.time(12, 30)),
("12 30 p . m .", dt.time(12, 30)),
("1:30am", dt.time(1, 30)),
("1:30 am ", dt.time(1, 30)),
("1:30 a m", dt.time(1, 30)),
("1:30 a . m .", dt.time(1, 30)),
("1:30a", dt.time(1, 30)),
("1:30 a", dt.time(1, 30)),
("1 30am", dt.time(1, 30)),
(" 1 30 am", dt.time(1, 30)),
("130 a m", dt.time(1, 30)),
("3:30 p m", dt.time(15, 30)),
(" 1 30 a . m ", dt.time(1, 30)),
)
for (input, expected) in twelve_hour_tests:
self.assertTimeEquals(input, expected)
def testBadTimes(self):
"""
Tests a variety of times that should bork and die, not necessarily in
that order.
"""
bad_inputs = (
"",
" aa ",
"12345",
"1340am",
"030pm",
"25:23",
"24:10",
"this ain't nothing like a time",
"-2:30",
)
for bad_input in bad_inputs:
try:
self.field.clean(bad_input)
self.fail("Bad input %s validated." % bad_input)
except AssertionError, e:
raise e
except ValidationError, e:
pass
except Exception, e:
self.fail("Validator threw unexpected exception %s" % str(e))
def assertTimeEquals(self, timestring, expected):
"""
Try to parse a time, and if it throws an exception, fail.
"""
try:
actual = self.field.clean(timestring)
self.assertEquals(actual, expected,
"String %s did not parse to expected datetime %s: Got %s" %
(timestring, str(expected), str(actual))
)
except AssertionError, e:
# Propagate assertion failures.
raise e
except ValidationError, e:
self.fail("String %s did not validate." % timestring)
except Exception, e:
self.fail("String %s caused unexpected exception: %s" %
(timestring, str(e)))
|
More like this
- Template tag - list punctuation for a list of items by shapiromatron 10 months, 1 week ago
- JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 10 months, 2 weeks ago
- Serializer factory with Django Rest Framework by julio 1 year, 5 months ago
- Image compression before saving the new model / work with JPG, PNG by Schleidens 1 year, 6 months ago
- Help text hyperlinks by sa2812 1 year, 6 months ago
Comments
Whoops -- forgot to include the unit tests. The test cases provide a pretty good sampling of the kinds of inputs it will handle.
#
Useful, thanks :-)
I did need to add a couple lines to parse seconds to wind up with something that handled the way Django fills in a Time field by default (eg. 13:20:00), though.
#
Please login first before commenting.