Merge lp:~methanal-developers/methanal/float-input into lp:methanal

Proposed by Jonathan Jacobs
Status: Merged
Approved by: Tristan Seligmann
Approved revision: 164
Merged at revision: 158
Proposed branch: lp:~methanal-developers/methanal/float-input
Merge into: lp:methanal
Diff against target: 576 lines (+363/-31)
11 files modified
methanal/js/Methanal/Tests/TestUtil.js (+39/-8)
methanal/js/Methanal/Tests/TestView.js (+137/-0)
methanal/js/Methanal/Tests/TestWidgets.js (+19/-0)
methanal/js/Methanal/Tests/Util.js (+18/-0)
methanal/js/Methanal/Util.js (+34/-6)
methanal/js/Methanal/View.js (+20/-0)
methanal/js/Methanal/Widgets.js (+7/-0)
methanal/model.py (+6/-5)
methanal/test/test_view.py (+36/-0)
methanal/view.py (+46/-12)
methanal/widgets.py (+1/-0)
To merge this branch: bzr merge lp:~methanal-developers/methanal/float-input
Reviewer Review Type Date Requested Status
Tristan Seligmann Approve
Review via email: mp+42248@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Tristan Seligmann (mithrandi) wrote :

48 + * L{Methanal.Util.strToFloat} converts a float value, represented
49 + * as a C{String}, to a C{Float}.

This docstring can't be accurate, as there is no such thing as Float in JavaScript.

367 + self._validInput = /^[-+]?\d*(\.\d*)?$/;

This regex could also be written as /^[-+]?\d*\.?\d*$/ but I can't really decide which one is easier to understand.

348 } else if (value === undefined || isNaN(value)) {
349 return 'Numerical value only';
350 - } else if (value <= self.lowerBound) {
351 + } else if (value !== undefined && value <= self.lowerBound) {
352 return 'Value too small; must be greater than ' + self.lowerBound;
353 - } else if (value >= self.upperBound) {
354 + } else if (value !== undefined && value >= self.upperBound) {

These changes look pointless; if value === undefined, the earlier case will match, thus by the time we reach the latter two, value !== undefined must be true.

review: Needs Fixing
162. By Jonathan Jacobs

Correct docstrings.

163. By Jonathan Jacobs

Slightly improve floating point regex.

164. By Jonathan Jacobs

Remove pointless checks.

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

> This docstring can't be accurate, as there is no such thing as Float in
> JavaScript.

Fixed.

> This regex could also be written as /^[-+]?\d*\.?\d*$/ but I can't really
> decide which one is easier to understand.

I think your version is slightly more readable.

> These changes look pointless; if value === undefined, the earlier case will
> match, thus by the time we reach the latter two, value !== undefined must be
> true.

Fixed.

Revision history for this message
Tristan Seligmann (mithrandi) :
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/TestUtil.js'
2--- methanal/js/Methanal/Tests/TestUtil.js 2011-02-13 18:50:04 +0000
3+++ methanal/js/Methanal/Tests/TestUtil.js 2011-02-18 09:27:38 +0000
4@@ -180,6 +180,26 @@
5
6
7 /**
8+ * Return C{true} iff the given string represents a base-10 numerical value.
9+ */
10+ function test_isNumericalString(self) {
11+ var CASES = [
12+ ['1234', true],
13+ ['1.23', true],
14+ ['0', true],
15+ ['01', true],
16+ [1234, false],
17+ [1.23, false],
18+ ['0x1', false],
19+ ['abc', false],
20+ [null, false],
21+ [undefined, false]];
22+ Methanal.Tests.Util.assertCases(
23+ self, Methanal.Util.isNumericalString, CASES);
24+ },
25+
26+
27+ /**
28 * L{Methanal.Util.strToInt} converts a base-10 integer value, represented
29 * as a C{String}, to an C{Integer}.
30 */
31@@ -192,14 +212,25 @@
32 ['123abc', undefined],
33 ['abc123', undefined],
34 ['0x123', undefined]];
35-
36- for (var i = 0; i < CASES.length; ++i) {
37- var input = CASES[i][0];
38- var expected = CASES[i][1];
39- var actual = Methanal.Util.strToInt(input);
40- self.assert(expected === actual, 'input = ' + input +
41- ' :: expected = ' + expected + ' :: actual = ' + actual);
42- }
43+ Methanal.Tests.Util.assertCases(self, Methanal.Util.strToInt, CASES);
44+ },
45+
46+
47+ /**
48+ * L{Methanal.Util.strToFloat} converts a float value, represented
49+ * as a C{String}, to a floating point C{Number}.
50+ */
51+ function test_strToFloat(self) {
52+ var CASES = [
53+ ['1234', 1234],
54+ ['01234.56', 1234.56],
55+ ['.0', 0],
56+ ['.5', 0.5],
57+ ['-1', -1],
58+ ['123.45abc', undefined],
59+ ['abc123.45', undefined],
60+ ['0x123', undefined]];
61+ Methanal.Tests.Util.assertCases(self, Methanal.Util.strToFloat, CASES);
62 },
63
64
65
66=== modified file 'methanal/js/Methanal/Tests/TestView.js'
67--- methanal/js/Methanal/Tests/TestView.js 2010-11-19 12:32:22 +0000
68+++ methanal/js/Methanal/Tests/TestView.js 2011-02-18 09:27:38 +0000
69@@ -1006,6 +1006,34 @@
70
71
72 /**
73+ * L{Methanal.View.IntegerInput.getValue} returns an integer value if the
74+ * input node's value is a valid number, C{null} if it is blank and
75+ * C{undefined} if the value is invalid.
76+ */
77+ function test_getValue(self) {
78+ var CASES = [
79+ [null, null],
80+ ['', null],
81+ ['abc', undefined],
82+ ['0.5', undefined],
83+ ['1a', undefined],
84+ ['0', 0],
85+ ['-1', -1],
86+ ['42', 42]];
87+
88+ self.testControl({value: null},
89+ function (control) {
90+ Methanal.Tests.Util.assertCases(
91+ self,
92+ function (value) {
93+ control.setValue(value);
94+ return control.getValue();
95+ }, CASES);
96+ });
97+ },
98+
99+
100+ /**
101 * L{Methanal.View.IntegerInput} only accepts input that is a valid integer.
102 */
103 function test_inputValidation(self) {
104@@ -1063,6 +1091,115 @@
105
106
107 /**
108+ * Tests for L{Methanal.View.FloatInput}.
109+ */
110+Methanal.Tests.TestView.BaseTestTextInput.subclass(
111+ Methanal.Tests.TestView, 'TestFloatInput').methods(
112+ function setUp(self) {
113+ self.controlType = Methanal.View.FloatInput;
114+ },
115+
116+
117+ /**
118+ * L{Methanal.View.FloatInput.getValue} returns an float value if the
119+ * input node's value is a valid number, C{null} if it is blank and
120+ * C{undefined} if the value is invalid.
121+ */
122+ function test_getValue(self) {
123+ var CASES = [
124+ [null, null],
125+ ['', null],
126+ ['abc', undefined],
127+ ['0.5', 0.5],
128+ ['.5', 0.5],
129+ ['-.5', -0.5],
130+ ['1a', undefined],
131+ ['0', 0],
132+ ['-1', -1],
133+ ['42', 42],
134+ ['1.2', 1.2]];
135+
136+ self.testControl({value: null},
137+ function (control) {
138+ Methanal.Tests.Util.assertCases(
139+ self,
140+ function (value) {
141+ control.setValue(value);
142+ return control.getValue();
143+ }, CASES);
144+ });
145+ },
146+
147+
148+ /**
149+ * L{Methanal.View.FloatInput} only accepts input that is a valid float.
150+ */
151+ function test_inputValidation(self) {
152+ self.testControl({value: null},
153+ function (control) {
154+ self.assertValidInput(control, null);
155+ self.assertValidInput(control, '');
156+ self.assertValidInput(control, '1');
157+ self.assertValidInput(control, '1.1');
158+ self.assertValidInput(control, '+1.0');
159+ self.assertValidInput(control, '1.');
160+ self.assertValidInput(control, '-1.0');
161+ self.assertValidInput(control, '.1');
162+ self.assertValidInput(control, '+.1');
163+ self.assertValidInput(control, '-.1');
164+ self.assertValidInput(control, '.0');
165+ self.assertInvalidInput(control, 'a');
166+ self.assertInvalidInput(control, '1.2.1');
167+ });
168+ },
169+
170+
171+ /**
172+ * L{Methanal.View.FloatInput} validates that values fall within a certain
173+ * exclusive range.
174+ */
175+ function test_bounds(self) {
176+ self.testControl({value: null,
177+ lowerBound: -11,
178+ upperBound: 8,},
179+ function (control) {
180+ self.assertValidInput(control, '-10.99');
181+ self.assertInvalidInput(control, '-11.0');
182+ self.assertValidInput(control, '7.99');
183+ self.assertInvalidInput(control, '8.0');
184+ });
185+ },
186+
187+
188+ /**
189+ * L{Methanal.View.FloatInput} validates that values fall within a certain
190+ * exclusive range, even for really big numbers that have precision
191+ * problems in Javascript.
192+ */
193+ function test_bigBounds(self) {
194+ // These turn into -9223372036854776000 and 9223372036854776000.
195+ self.testControl({value: null,
196+ decimalPlaces: 2,
197+ lowerBound: -9223372036854775809.5,
198+ upperBound: 9223372036854775808.5},
199+ function (control) {
200+ self.assertValidInput(control, null);
201+ self.assertValidInput(control, '');
202+ self.assertValidInput(control, '1');
203+ self.assertInvalidInput(control, '-9223372036854775800');
204+ self.assertInvalidInput(control, '-9223372036854775809');
205+ self.assertInvalidInput(control, '-9223372036854775808.5');
206+ self.assertInvalidInput(control, '-92233720368547758090');
207+ self.assertInvalidInput(control, '9223372036854775800');
208+ self.assertInvalidInput(control, '9223372036854775808');
209+ self.assertInvalidInput(control, '9223372036854775807.5');
210+ self.assertInvalidInput(control, '92233720368547758080');
211+ });
212+ });
213+
214+
215+
216+/**
217 * Tests for L{Methanal.View.DecimalInput}.
218 */
219 Methanal.Tests.TestView.BaseTestTextInput.subclass(
220
221=== modified file 'methanal/js/Methanal/Tests/TestWidgets.js'
222--- methanal/js/Methanal/Tests/TestWidgets.js 2010-07-11 19:17:21 +0000
223+++ methanal/js/Methanal/Tests/TestWidgets.js 2011-02-18 09:27:38 +0000
224@@ -276,6 +276,25 @@
225
226
227 /**
228+ * Tests for L{Methanal.Widgets.FloatColumn}.
229+ */
230+Methanal.Tests.TestWidgets.SimpleColumnTest.subclass(
231+ Methanal.Tests.TestWidgets, 'TestFloatColumn').methods(
232+ function setUp(self) {
233+ self.columnType = Methanal.Widgets.FloatColumn;
234+ self.columnValues = {
235+ 'a': {value: 42.0,
236+ link: null,
237+ nodeValue: '42'},
238+ 'b': {value: 1.23,
239+ link: 'quux',
240+ nodeValue: '1.23'}};
241+ Methanal.Tests.TestWidgets.TestFloatColumn.upcall(self, 'setUp');
242+ });
243+
244+
245+
246+/**
247 * Tests for L{Methanal.Widgets.BooleanColumn}.
248 */
249 Methanal.Tests.TestWidgets.SimpleColumnTest.subclass(
250
251=== modified file 'methanal/js/Methanal/Tests/Util.js'
252--- methanal/js/Methanal/Tests/Util.js 2010-07-23 20:07:38 +0000
253+++ methanal/js/Methanal/Tests/Util.js 2011-02-18 09:27:38 +0000
254@@ -111,3 +111,21 @@
255 }
256 throw new Error('Setter properties not supported!');
257 }
258+
259+
260+/**
261+ * Assert that an C{Array} of input and expected pairs (C{[input,
262+ * expectedOutput]}) match when the input is processed by C{fn}.
263+ */
264+Methanal.Tests.Util.assertCases = function assertCases(testcase, fn, cases) {
265+ for (var i = 0; i < cases.length; ++i) {
266+ var input = cases[i][0];
267+ var expected = cases[i][1];
268+ var actual = fn(input);
269+ var repr = Methanal.Util.repr;
270+ testcase.assert(
271+ expected === actual,
272+ 'input = ' + repr(input) + ' :: expected = ' + repr(expected) +
273+ ' :: actual = ' + repr(actual));
274+ }
275+};
276
277=== modified file 'methanal/js/Methanal/Util.js'
278--- methanal/js/Methanal/Util.js 2011-02-13 18:50:04 +0000
279+++ methanal/js/Methanal/Util.js 2011-02-18 09:27:38 +0000
280@@ -133,20 +133,32 @@
281
282
283 /**
284+ * Return C{true} iff the given string represents a base-10 numerical value.
285+ */
286+Methanal.Util.isNumericalString = function isNumericalString(s) {
287+ if (typeof s !== 'string') {
288+ return false;
289+ } else if (s.indexOf('x') !== -1) {
290+ return false;
291+ } else if (isNaN(s)) {
292+ return false;
293+ }
294+ return true;
295+};
296+
297+
298+
299+/**
300 * Convert a string to a base-10 integer.
301 *
302 * Not quite as simple to do properly as one might think.
303 *
304 * @type s: C{String}
305 *
306- * @rtype: C{Integer}
307+ * @rtype: C{Number} or C{undefined}
308 */
309 Methanal.Util.strToInt = function strToInt(s) {
310- if (typeof s !== 'string') {
311- return undefined;
312- } else if (s.indexOf('x') !== -1) {
313- return undefined;
314- } else if (isNaN(s)) {
315+ if (!Methanal.Util.isNumericalString(s)) {
316 return undefined;
317 }
318 return parseInt(s, 10);
319@@ -155,6 +167,22 @@
320
321
322 /**
323+ * Convert a string to a floating point C{Number}.
324+ *
325+ * @type s: C{String}
326+ *
327+ * @rtype: C{Number} or C{undefined}
328+ */
329+Methanal.Util.strToFloat = function strToFloat(s) {
330+ if (!Methanal.Util.isNumericalString(s)) {
331+ return undefined;
332+ }
333+ return parseFloat(s);
334+};
335+
336+
337+
338+/**
339 * Pretty print a decimal number with thousands separators.
340 *
341 * Useful for formatting large currency amounts in a human-readable way.
342
343=== modified file 'methanal/js/Methanal/View.js'
344--- methanal/js/Methanal/View.js 2010-11-22 17:37:26 +0000
345+++ methanal/js/Methanal/View.js 2011-02-18 09:27:38 +0000
346@@ -2364,6 +2364,26 @@
347
348
349 /**
350+ * Float input.
351+ */
352+Methanal.View.NumericInput.subclass(Methanal.View, 'FloatInput').methods(
353+ function __init__(self, node, args) {
354+ Methanal.View.FloatInput.upcall(self, '__init__', node, args);
355+ self._validInput = /^[-+]?\d*\.?\d*$/;
356+ },
357+
358+
359+ function getValue(self) {
360+ var value = Methanal.View.FloatInput.upcall(self, 'getValue');
361+ if (value) {
362+ value = Methanal.Util.strToFloat(value);
363+ }
364+ return value;
365+ });
366+
367+
368+
369+/**
370 * Decimal number input.
371 *
372 * @type decimalPlaces: C{Integer}
373
374=== modified file 'methanal/js/Methanal/Widgets.js'
375--- methanal/js/Methanal/Widgets.js 2010-11-17 15:58:21 +0000
376+++ methanal/js/Methanal/Widgets.js 2011-02-18 09:27:38 +0000
377@@ -187,6 +187,13 @@
378
379
380 /**
381+ * Column representing float values.
382+ */
383+Methanal.Widgets.Column.subclass(Methanal.Widgets, 'FloatColumn');
384+
385+
386+
387+/**
388 * Column representing boolean values.
389 */
390 Methanal.Widgets.Column.subclass(Methanal.Widgets, 'BooleanColumn');
391
392=== modified file 'methanal/model.py'
393--- methanal/model.py 2010-02-22 21:29:52 +0000
394+++ methanal/model.py 2011-02-18 09:27:38 +0000
395@@ -279,11 +279,12 @@
396
397
398 _paramTypes = {
399- attributes.integer: Value,
400- attributes.text: Value,
401- attributes.boolean: Value,
402- attributes.textlist: List,
403- attributes.timestamp: Value}
404+ attributes.integer: Value,
405+ attributes.ieee754_double: Value,
406+ attributes.text: Value,
407+ attributes.boolean: Value,
408+ attributes.textlist: List,
409+ attributes.timestamp: Value}
410
411 def paramFromAttribute(store, attr, value, name=None):
412 doc = attr.doc or None
413
414=== modified file 'methanal/test/test_view.py'
415--- methanal/test/test_view.py 2010-07-17 20:16:49 +0000
416+++ methanal/test/test_view.py 2011-02-18 09:27:38 +0000
417@@ -342,6 +342,42 @@
418
419
420
421+class FloatInputTests(FormInputTests, NumericInputTestsMixin):
422+ """
423+ Tests for L{methanal.view.FloatInput}.
424+ """
425+ controlType = view.FloatInput
426+
427+
428+ def test_getValue(self):
429+ """
430+ L{methanal.view.FloatInput.getValue} retrieves an empty string in the
431+ C{None} case and a C{float} in the case where a value exists.
432+ """
433+ control = self.createControl(dict())
434+ param = control.parent.param
435+
436+ param.value = None
437+ self.assertEquals(control.getValue(), u'')
438+
439+ param.value = u'5'
440+ self.assertTrue(isinstance(control.getValue(), float))
441+ self.assertEquals(control.getValue(), 5.0)
442+ param.value = 5.0
443+ self.assertTrue(isinstance(control.getValue(), float))
444+ self.assertEquals(control.getValue(), 5.0)
445+
446+
447+ def test_minimumMaximumDefaultValues(self):
448+ """
449+ The limits for SQL and Python float types are the same value, so no
450+ minimum or maximum checking is required in Methanal for the default
451+ ranges.
452+ """
453+ pass
454+
455+
456+
457 class DecimalInputTests(FormInputTests, NumericInputTestsMixin):
458 """
459 Tests for L{methanal.view.DecimalInput}.
460
461=== modified file 'methanal/view.py'
462--- methanal/view.py 2010-09-10 13:41:41 +0000
463+++ methanal/view.py 2011-02-18 09:27:38 +0000
464@@ -9,7 +9,7 @@
465 from twisted.python.versions import Version
466 from twisted.python.deprecate import deprecated
467
468-from axiom.attributes import text, integer, timestamp, boolean
469+from axiom.attributes import text, integer, timestamp, boolean, ieee754_double
470
471 from nevow.page import renderer
472 from nevow.athena import expose
473@@ -649,6 +649,8 @@
474 MINIMUM = -9223372036854775808
475 MAXIMUM = 9223372036854775807
476
477+ NO_BOUNDS = object()
478+
479
480 def __init__(self, minimumValue=None, maximumValue=None, **kw):
481 super(NumericInput, self).__init__(**kw)
482@@ -662,9 +664,12 @@
483
484
485 def getArgs(self):
486- return {
487- u'lowerBound': self.minimumValue - 1,
488- u'upperBound': self.maximumValue + 1}
489+ args = {}
490+ if self.minimumValue is not self.NO_BOUNDS:
491+ args[u'lowerBound'] = self.minimumValue - 1
492+ if self.maximumValue is not self.NO_BOUNDS:
493+ args[u'upperBound'] = self.maximumValue + 1
494+ return args
495
496
497 def checkValue(self, value):
498@@ -674,12 +679,13 @@
499 """
500 if value is None:
501 return
502- if value > self.maximumValue:
503+ maximumValue, minimumValue = self.maximumValue, self.minimumValue
504+ if maximumValue is not self.NO_BOUNDS and value > maximumValue:
505 raise ValueError('%s is larger than %s' % (
506- value, self.maximumValue))
507- elif value < self.minimumValue:
508+ value, maximumValue))
509+ elif minimumValue is not self.NO_BOUNDS and value < minimumValue:
510 raise ValueError('%s is smaller than %s' % (
511- value, self.minimumValue))
512+ value, minimumValue))
513
514
515 def convertValue(self, value):
516@@ -708,6 +714,33 @@
517
518
519
520+class FloatInput(NumericInput):
521+ """
522+ Float input.
523+ """
524+ jsClass = u'Methanal.View.FloatInput'
525+
526+
527+ def __init__(self, minimumValue=NumericInput.NO_BOUNDS,
528+ maximumValue=NumericInput.NO_BOUNDS, **kw):
529+ super(FloatInput, self).__init__(
530+ minimumValue=minimumValue, maximumValue=maximumValue, **kw)
531+
532+
533+ def convertValue(self, value):
534+ if value is not None:
535+ value = float(value)
536+ return value
537+
538+
539+ def getValue(self):
540+ value = self.param.value
541+ if value is None:
542+ return u''
543+ return float(value)
544+
545+
546+
547 class DecimalInput(NumericInput):
548 """
549 Decimal input.
550@@ -1320,10 +1353,11 @@
551
552
553 _inputTypes = {
554- boolean: lambda env: CheckboxInput,
555- text: lambda env: TextInput,
556- integer: lambda env: IntegerInput,
557- timestamp: lambda env:
558+ boolean: lambda env: CheckboxInput,
559+ text: lambda env: TextInput,
560+ integer: lambda env: IntegerInput,
561+ ieee754_double: lambda env: FloatInput,
562+ timestamp: lambda env:
563 lambda **kw: DateInput(timezone=env['timezone'], **kw)}
564
565 def inputTypeFromAttribute(attr, **env):
566
567=== modified file 'methanal/widgets.py'
568--- methanal/widgets.py 2010-04-25 13:48:54 +0000
569+++ methanal/widgets.py 2011-02-18 09:27:38 +0000
570@@ -211,6 +211,7 @@
571 columnTypes = {
572 'text': u'Methanal.Widgets.TextColumn',
573 'integer': u'Methanal.Widgets.IntegerColumn',
574+ 'ieee754_double': u'Methanal.Widgets.FloatColumn',
575 'boolean': u'Methanal.Widgets.BooleanColumn',
576 'timestamp': u'Methanal.Widgets.TimestampColumn'}
577

Subscribers

People subscribed via source and target branches

to all changes: