Repository: plaid/sanctuary
Pull request #46 (2015-06-04 21:02:02)

parse: disallow invalid characters in S.parseInt
See #18


charleswli: `eval('parseInt("excellent job, ", 16) - parseInt("chambers", 16)')` :+1: 

Patch:
diff --git a/index.js b/index.js
index 7fcdac1..fd4edfb 100644
--- a/index.js
+++ b/index.js
@@ -1223,7 +1223,14 @@
   //. fact represent a number in the base specified by the radix; Nothing
   //. otherwise.
   //.
+  //. This function is stricter than [`parseInt`][parseInt]: a string
+  //. is considered to represent an integer only if all its non-prefix
+  //. characters are members of the character set specified by the radix.
+  //.
   //. ```javascript
+  //. > S.parseInt(10, '-42')
+  //. Just(-42)
+  //.
   //. > S.parseInt(16, '0xFF')
   //. Just(255)
   //.
@@ -1231,8 +1238,22 @@
   //. Nothing()
   //. ```
   S.parseInt = def('parseInt', [Number, String], function(radix, s) {
-    var n = parseInt(s, radix);
-    return n === n ? Just(n) : Nothing();
+    if (radix < 2 || radix > 36) {
+      throw new RangeError('Radix not in [2 .. 36]');
+    }
+
+    var charset = R.take(radix, '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ');
+
+    return R.pipe(
+      Just,
+      R.filter(R.pipe(R.replace(/^[+-]/, ''),
+                      radix === 16 ? R.replace(/^0x/i, '') : R.identity,
+                      R.split(''),
+                      R.all(R.pipe(R.toUpper,
+                                   R.indexOf(_, charset),
+                                   R.gte(_, 0))))),
+      R.map(R.partialRight(parseInt, radix))
+    )(s);
   });
 
   //# parseJson :: String -> Maybe *
@@ -1280,3 +1301,4 @@
 //. [R.map]:        http://ramdajs.com/docs/#map
 //. [Ramda]:        http://ramdajs.com/
 //. [Semigroup]:    https://github.com/fantasyland/fantasy-land#semigroup
+//. [parseInt]:     https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/parseInt


Test patch:
diff --git a/test/index.js b/test/index.js
index eb17135..343566b 100644
--- a/test/index.js
+++ b/test/index.js
@@ -1729,6 +1729,116 @@ describe('parse', function() {
       eq(S.parseInt(10, 'xxx'), S.Nothing());
     });
 
+    it('accepts radix in [2 .. 36]', function() {
+      eq(S.parseInt(2, '1'), S.Just(1));
+      eq(S.parseInt(2, '2'), S.Nothing());
+      eq(S.parseInt(3, '2'), S.Just(2));
+      eq(S.parseInt(3, '3'), S.Nothing());
+      eq(S.parseInt(4, '3'), S.Just(3));
+      eq(S.parseInt(4, '4'), S.Nothing());
+      eq(S.parseInt(5, '4'), S.Just(4));
+      eq(S.parseInt(5, '5'), S.Nothing());
+      eq(S.parseInt(6, '5'), S.Just(5));
+      eq(S.parseInt(6, '6'), S.Nothing());
+      eq(S.parseInt(7, '6'), S.Just(6));
+      eq(S.parseInt(7, '7'), S.Nothing());
+      eq(S.parseInt(8, '7'), S.Just(7));
+      eq(S.parseInt(8, '8'), S.Nothing());
+      eq(S.parseInt(9, '8'), S.Just(8));
+      eq(S.parseInt(9, '9'), S.Nothing());
+      eq(S.parseInt(10, '9'), S.Just(9));
+      eq(S.parseInt(10, 'A'), S.Nothing());
+      eq(S.parseInt(11, 'A'), S.Just(10));
+      eq(S.parseInt(11, 'B'), S.Nothing());
+      eq(S.parseInt(12, 'B'), S.Just(11));
+      eq(S.parseInt(12, 'C'), S.Nothing());
+      eq(S.parseInt(13, 'C'), S.Just(12));
+      eq(S.parseInt(13, 'D'), S.Nothing());
+      eq(S.parseInt(14, 'D'), S.Just(13));
+      eq(S.parseInt(14, 'E'), S.Nothing());
+      eq(S.parseInt(15, 'E'), S.Just(14));
+      eq(S.parseInt(15, 'F'), S.Nothing());
+      eq(S.parseInt(16, 'F'), S.Just(15));
+      eq(S.parseInt(16, 'G'), S.Nothing());
+      eq(S.parseInt(17, 'G'), S.Just(16));
+      eq(S.parseInt(17, 'H'), S.Nothing());
+      eq(S.parseInt(18, 'H'), S.Just(17));
+      eq(S.parseInt(18, 'I'), S.Nothing());
+      eq(S.parseInt(19, 'I'), S.Just(18));
+      eq(S.parseInt(19, 'J'), S.Nothing());
+      eq(S.parseInt(20, 'J'), S.Just(19));
+      eq(S.parseInt(20, 'K'), S.Nothing());
+      eq(S.parseInt(21, 'K'), S.Just(20));
+      eq(S.parseInt(21, 'L'), S.Nothing());
+      eq(S.parseInt(22, 'L'), S.Just(21));
+      eq(S.parseInt(22, 'M'), S.Nothing());
+      eq(S.parseInt(23, 'M'), S.Just(22));
+      eq(S.parseInt(23, 'N'), S.Nothing());
+      eq(S.parseInt(24, 'N'), S.Just(23));
+      eq(S.parseInt(24, 'O'), S.Nothing());
+      eq(S.parseInt(25, 'O'), S.Just(24));
+      eq(S.parseInt(25, 'P'), S.Nothing());
+      eq(S.parseInt(26, 'P'), S.Just(25));
+      eq(S.parseInt(26, 'Q'), S.Nothing());
+      eq(S.parseInt(27, 'Q'), S.Just(26));
+      eq(S.parseInt(27, 'R'), S.Nothing());
+      eq(S.parseInt(28, 'R'), S.Just(27));
+      eq(S.parseInt(28, 'S'), S.Nothing());
+      eq(S.parseInt(29, 'S'), S.Just(28));
+      eq(S.parseInt(29, 'T'), S.Nothing());
+      eq(S.parseInt(30, 'T'), S.Just(29));
+      eq(S.parseInt(30, 'U'), S.Nothing());
+      eq(S.parseInt(31, 'U'), S.Just(30));
+      eq(S.parseInt(31, 'V'), S.Nothing());
+      eq(S.parseInt(32, 'V'), S.Just(31));
+      eq(S.parseInt(32, 'W'), S.Nothing());
+      eq(S.parseInt(33, 'W'), S.Just(32));
+      eq(S.parseInt(33, 'X'), S.Nothing());
+      eq(S.parseInt(34, 'X'), S.Just(33));
+      eq(S.parseInt(34, 'Y'), S.Nothing());
+      eq(S.parseInt(35, 'Y'), S.Just(34));
+      eq(S.parseInt(35, 'Z'), S.Nothing());
+      eq(S.parseInt(36, 'Z'), S.Just(35));
+      eq(S.parseInt(36, '['), S.Nothing());
+    });
+
+    it('throws if radix is not in [2 .. 36]', function() {
+      assert.throws(function() { S.parseInt(1, ''); },
+                    errorEq(RangeError, 'Radix not in [2 .. 36]'));
+
+      assert.throws(function() { S.parseInt(37, ''); },
+                    errorEq(RangeError, 'Radix not in [2 .. 36]'));
+    });
+
+    it('is not case-sensitive', function() {
+      eq(S.parseInt(16, 'FF'), S.Just(255));
+      eq(S.parseInt(16, 'Ff'), S.Just(255));
+      eq(S.parseInt(16, 'fF'), S.Just(255));
+      eq(S.parseInt(16, 'ff'), S.Just(255));
+    });
+
+    it('accepts optional "+" or "-" prefix', function() {
+      eq(S.parseInt(10, '+42'), S.Just(42));
+      eq(S.parseInt(16, '+2A'), S.Just(42));
+      eq(S.parseInt(10, '-42'), S.Just(-42));
+      eq(S.parseInt(16, '-2A'), S.Just(-42));
+    });
+
+    it('accepts optional "0x" or "0X" prefix when radix is 16', function() {
+      eq(S.parseInt(16, '0xFF'), S.Just(255));
+      eq(S.parseInt(16, '0XFF'), S.Just(255));
+      eq(S.parseInt(17, '0xFF'), S.Nothing());
+      eq(S.parseInt(17, '0XFF'), S.Nothing());
+      eq(S.parseInt(16, '+0xFF'), S.Just(255));
+      eq(S.parseInt(16, '+0XFF'), S.Just(255));
+      eq(S.parseInt(16, '-0xFF'), S.Just(-255));
+      eq(S.parseInt(16, '-0XFF'), S.Just(-255));
+    });
+
+    it('returns a Nothing if one or more characters are invalid', function() {
+      eq(S.parseInt(16, 'alice'), S.Nothing());  // parseInt('alice', 16) == 10
+    });
+
     it('is curried', function() {
       eq(S.parseInt(10)('42'), S.Just(42));
     });