Merge lp:~methanal-developers/methanal/validate-large-numbers into lp:methanal

Proposed by Jonathan Jacobs
Status: Merged
Approved by: Tristan Seligmann
Approved revision: 146
Merged at revision: 146
Proposed branch: lp:~methanal-developers/methanal/validate-large-numbers
Merge into: lp:methanal
Diff against target: 324 lines (+220/-13)
4 files modified
methanal/js/Methanal/Tests/TestView.js (+90/-0)
methanal/js/Methanal/View.js (+14/-1)
methanal/test/test_view.py (+50/-2)
methanal/view.py (+66/-10)
To merge this branch: bzr merge lp:~methanal-developers/methanal/validate-large-numbers
Reviewer Review Type Date Requested Status
Tristan Seligmann Approve
Review via email: mp+30190@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Jonathan Jacobs (jjacobs) wrote :

I have a few concerns that I'd like some input on:

  1. There is no realtime validation for these limits. I could pass the ranges through to the client and do some validation but for really large values (like 2 ** 63) Javascript has rounding errors (since numerics are all internally backed by a double) and so validation for the really large numbers breaks down because the ranges are now inaccurately represented. This may not be a concern since there is still validation on the server side and for smaller numbers we will have still accurate validation. Is this worth implementing?

  2. The message that comes back from the server side doesn't indicate what input generated the error (if an input did generate one), so getting an error like "ValueError: 5 is bigger than 4" is not likely to help anyone, really. But maybe implementing client side validation, as in my first point, would help for the cases where the limits are more likely to impact users.

Revision history for this message
Tristan Seligmann (mithrandi) wrote :

1. The precision errors (not rounding errors, technically) are somewhat of an issue with JavaScript, but they shouldn't pose a problem to doing validation, as long as you define the range exclusively instead of inclusively. In other words, -9223372036854775809 < n < 9223372036854775808. 9223372036854775808 will actually end up as 9223372036854776000, but that's fine; any number above 9223372036854775295 will end up as that, and thus be out of range. The only way to avoid this would be to have IntegerInput send the value to the server as a string, and only do the integer conversion there, but that would make writing validators somewhat challenging. Ugh. I just noticed DecimalInput suffers from pretty much the same precision problem; how have we not tripped over this already? Perhaps we should discuss this further in a different venue.

2. Hopefully hitting that error would be a relatively rare case; I'd be more concerned about the silent inaccuracy of JS numbers of such a magnitude. I suppose the "preview" value would at least expose the issue to the user, but it is somewhat subtle.

review: Needs Fixing
Revision history for this message
Jonathan Jacobs (jjacobs) wrote :

> 1. The precision errors (not rounding errors, technically) are somewhat of an
> issue with JavaScript, but they shouldn't pose a problem to doing validation,
> as long as you define the range exclusively instead of inclusively. In other
> words, -9223372036854775809 < n < 9223372036854775808. 9223372036854775808
> will actually end up as 9223372036854776000, but that's fine; any number above
> 9223372036854775295 will end up as that, and thus be out of range. The only
> way to avoid this would be to have IntegerInput send the value to the server
> as a string, and only do the integer conversion there, but that would make
> writing validators somewhat challenging. Ugh. I just noticed DecimalInput
> suffers from pretty much the same precision problem; how have we not tripped
> over this already? Perhaps we should discuss this further in a different
> venue.

Done.

> 2. Hopefully hitting that error would be a relatively rare case; I'd be more
> concerned about the silent inaccuracy of JS numbers of such a magnitude. I
> suppose the "preview" value would at least expose the issue to the user, but
> it is somewhat subtle.

As we discussed, the preview functionality has its own quirks. Hopefully nobody is using these inputs to for really large numbers.

Revision history for this message
Tristan Seligmann (mithrandi) wrote :

Looks good :)

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'methanal/js/Methanal/Tests/TestView.js'
2--- methanal/js/Methanal/Tests/TestView.js 2010-07-14 10:13:31 +0000
3+++ methanal/js/Methanal/Tests/TestView.js 2010-07-19 13:49:42 +0000
4@@ -979,6 +979,47 @@
5 self.assertInvalidInput(control, 'a');
6 self.assertInvalidInput(control, '1.2');
7 });
8+ },
9+
10+
11+ /**
12+ * L{Methanal.View.IntegerInput} validates that values fall within a certain
13+ * exclusive range.
14+ */
15+ function test_bounds(self) {
16+ self.testControl({value: null,
17+ lowerBound: -11,
18+ upperBound: 8,},
19+ function (control) {
20+ self.assertValidInput(control, '-10');
21+ self.assertInvalidInput(control, '-11');
22+ self.assertValidInput(control, '7');
23+ self.assertInvalidInput(control, '8');
24+ });
25+ },
26+
27+
28+ /**
29+ * L{Methanal.View.IntegerInput} validates that values fall within a certain
30+ * exclusive range, even for really big numbers that have precision
31+ * problems in Javascript.
32+ */
33+ function test_bigBounds(self) {
34+ // These turn into -9223372036854776000 and 9223372036854776000.
35+ self.testControl({value: null,
36+ lowerBound: -9223372036854775809,
37+ upperBound: 9223372036854775808},
38+ function (control) {
39+ self.assertValidInput(control, null);
40+ self.assertValidInput(control, '');
41+ self.assertValidInput(control, '1');
42+ self.assertInvalidInput(control, '-9223372036854775800');
43+ self.assertInvalidInput(control, '-9223372036854775809');
44+ self.assertInvalidInput(control, '-92233720368547758090');
45+ self.assertInvalidInput(control, '9223372036854775800');
46+ self.assertInvalidInput(control, '9223372036854775808');
47+ self.assertInvalidInput(control, '92233720368547758080');
48+ });
49 });
50
51
52@@ -1055,6 +1096,55 @@
53 self.assertInvalidInput(control, 'a');
54 self.assertInvalidInput(control, '1.234');
55 });
56+ },
57+
58+
59+ /**
60+ * L{Methanal.View.DecimalInput} validates that values fall within a certain
61+ * exclusive range.
62+ */
63+ function test_bounds(self) {
64+ self.testControl({value: null,
65+ decimalPlaces: 2,
66+ lowerBound: -10.5,
67+ upperBound: 7.5,},
68+ function (control) {
69+ self.assertValidInput(control, '-10');
70+ self.assertValidInput(control, '-10.4');
71+ self.assertInvalidInput(control, '-11');
72+ self.assertInvalidInput(control, '-10.6');
73+ self.assertValidInput(control, '7');
74+ self.assertValidInput(control, '7.4');
75+ self.assertInvalidInput(control, '8');
76+ self.assertInvalidInput(control, '7.6');
77+ });
78+ },
79+
80+
81+ /**
82+ * L{Methanal.View.DecimalInput} validates that values fall within a certain
83+ * exclusive range, even for really big numbers that have precision
84+ * problems in Javascript.
85+ */
86+ function test_bigBounds(self) {
87+ // These turn into -9223372036854776000 and 9223372036854776000.
88+ self.testControl({value: null,
89+ decimalPlaces: 2,
90+ lowerBound: -9223372036854775809.5,
91+ upperBound: 9223372036854775808.5},
92+ function (control) {
93+ self.assertValidInput(control, null);
94+ self.assertValidInput(control, '');
95+ self.assertValidInput(control, '1');
96+ self.assertInvalidInput(control, '-9223372036854775800');
97+ self.assertInvalidInput(control, '-9223372036854775809');
98+ self.assertInvalidInput(control, '-9223372036854775808.5');
99+ self.assertInvalidInput(control, '-92233720368547758090');
100+ self.assertInvalidInput(control, '9223372036854775800');
101+ self.assertInvalidInput(control, '9223372036854775808');
102+ self.assertInvalidInput(control, '9223372036854775807.5');
103+ self.assertInvalidInput(control, '92233720368547758080');
104+ });
105 });
106
107
108
109=== modified file 'methanal/js/Methanal/View.js'
110--- methanal/js/Methanal/View.js 2010-07-17 14:02:19 +0000
111+++ methanal/js/Methanal/View.js 2010-07-19 13:49:42 +0000
112@@ -2228,6 +2228,13 @@
113 * for validity
114 */
115 Methanal.View.TextInput.subclass(Methanal.View, 'NumericInput').methods(
116+ function __init__(self, node, args) {
117+ self.lowerBound = args.lowerBound;
118+ self.upperBound = args.upperBound;
119+ Methanal.View.NumericInput.upcall(self, '__init__', node, args);
120+ },
121+
122+
123 function getValue(self) {
124 var value = Methanal.View.NumericInput.upcall(self, 'getValue');
125 if (!value) {
126@@ -2241,8 +2248,14 @@
127
128
129 function baseValidator(self, value) {
130- if (value === undefined || isNaN(value)) {
131+ if (value === null) {
132+ return;
133+ } else if (value === undefined || isNaN(value)) {
134 return 'Numerical value only';
135+ } else if (value <= self.lowerBound) {
136+ return 'Value too small; must be greater than ' + self.lowerBound;
137+ } else if (value >= self.upperBound) {
138+ return 'Value too large; must be less than ' + self.upperBound;
139 }
140 });
141
142
143=== modified file 'methanal/test/test_view.py'
144--- methanal/test/test_view.py 2010-07-13 09:36:38 +0000
145+++ methanal/test/test_view.py 2010-07-19 13:49:42 +0000
146@@ -267,7 +267,55 @@
147
148
149
150-class IntegerInputTests(FormInputTests):
151+class NumericInputTestsMixin(object):
152+ """
153+ Tests mixin for numeric inputs.
154+ """
155+ def _testMinimumMaximum(self, control, acceptable, tooSmall, tooLarge):
156+ """
157+ Assert that C{control} sets its parameter's value to C{None} value or
158+ C{acceptable}, when invoked with these values. When invoked with
159+ C{tooSmall} or C{tooLarge} an exception is raised indicating that the
160+ value is too small or too large.
161+ """
162+ param = control.parent.param
163+
164+ data = {param.name: None}
165+ control.invoke(data)
166+ self.assertIdentical(param.value, None)
167+
168+ data = {param.name: acceptable}
169+ control.invoke(data)
170+ self.assertEquals(param.value, acceptable)
171+
172+ data = {param.name: tooSmall}
173+ e = self.assertRaises(ValueError, control.invoke, data)
174+ self.assertIn('is smaller than', str(e))
175+
176+ data = {param.name: tooLarge}
177+ e = self.assertRaises(ValueError, control.invoke, data)
178+ self.assertIn('is larger than', str(e))
179+
180+
181+ def test_minimumMaximumDefaultValues(self):
182+ """
183+ Numeric inputs default to not accepting values smaller than C{-(2 **
184+ 63)} or larger than C{2 ** 63 }.
185+ """
186+ control = self.createControl(dict())
187+ self._testMinimumMaximum(control, 42, -(2 ** 63) - 1, 2 ** 63)
188+
189+
190+ def test_minimumMaximumOverrideValues(self):
191+ """
192+ Numeric inputs can override their minimum and maximum value range.
193+ """
194+ control = self.createControl(dict(minimumValue=0, maximumValue=10))
195+ self._testMinimumMaximum(control, 5, -1, 42)
196+
197+
198+
199+class IntegerInputTests(FormInputTests, NumericInputTestsMixin):
200 """
201 Tests for L{methanal.view.IntegerInput}.
202 """
203@@ -294,7 +342,7 @@
204
205
206
207-class DecimalInputTests(FormInputTests):
208+class DecimalInputTests(FormInputTests, NumericInputTestsMixin):
209 """
210 Tests for L{methanal.view.DecimalInput}.
211 """
212
213=== modified file 'methanal/view.py'
214--- methanal/view.py 2010-07-13 11:43:58 +0000
215+++ methanal/view.py 2010-07-19 13:49:42 +0000
216@@ -632,12 +632,70 @@
217
218
219
220-class IntegerInput(TextInput):
221+class NumericInput(TextInput):
222+ """
223+ Base class for numeric inputs.
224+
225+ @ivar minimumValue: Minimum allowed value, defaults to the smallest integer
226+ value allowed in SQLite.
227+
228+ @ivar maximumValue: Maximum allowed value, defaults to the largest integer
229+ value allowed in SQLite.
230+ """
231+ MINIMUM = -9223372036854775808
232+ MAXIMUM = 9223372036854775807
233+
234+
235+ def __init__(self, minimumValue=None, maximumValue=None, **kw):
236+ super(NumericInput, self).__init__(**kw)
237+ if minimumValue is None:
238+ minimumValue = self.MINIMUM
239+ self.minimumValue = minimumValue
240+
241+ if maximumValue is None:
242+ maximumValue = self.MAXIMUM
243+ self.maximumValue = maximumValue
244+
245+
246+ def getArgs(self):
247+ return {
248+ u'lowerBound': self.minimumValue - 1,
249+ u'upperBound': self.maximumValue + 1}
250+
251+
252+ def checkValue(self, value):
253+ """
254+ Check that C{value} is within the minimum and maximum ranges. Raise
255+ C{ValueError} if C{value} is outside of the allowed range.
256+ """
257+ if value is None:
258+ return
259+ if value > self.maximumValue:
260+ raise ValueError('%s is larger than %s' % (
261+ value, self.maximumValue))
262+ elif value < self.minimumValue:
263+ raise ValueError('%s is smaller than %s' % (
264+ value, self.minimumValue))
265+
266+
267+ def convertValue(self, value):
268+ return value
269+
270+
271+ def invoke(self, data):
272+ value = self.convertValue(data[self.param.name])
273+ self.checkValue(value)
274+ self.param.value = value
275+
276+
277+
278+class IntegerInput(NumericInput):
279 """
280 Integer input.
281 """
282 jsClass = u'Methanal.View.IntegerInput'
283
284+
285 def getValue(self):
286 value = self.param.value
287 if value is None:
288@@ -646,7 +704,7 @@
289
290
291
292-class DecimalInput(TextInput):
293+class DecimalInput(NumericInput):
294 """
295 Decimal input.
296 """
297@@ -667,6 +725,12 @@
298 self.decimalPlaces = decimalPlaces
299
300
301+ def convertValue(self, value):
302+ if value is not None:
303+ value = Decimal(str(value))
304+ return value
305+
306+
307 def getArgs(self):
308 return {u'decimalPlaces': self.decimalPlaces}
309
310@@ -679,14 +743,6 @@
311 return float(value)
312
313
314- def invoke(self, data):
315- value = data[self.param.name]
316- if value is None:
317- self.param.value = None
318- else:
319- self.param.value = Decimal(str(value))
320-
321-
322
323 class PercentInput(DecimalInput):
324 """

Subscribers

People subscribed via source and target branches