jsprim
Advanced tools
Comparing version 1.3.1 to 1.4.0
@@ -7,2 +7,6 @@ # Changelog | ||
## v1.4.0 (2017-03-13) | ||
* #7 Add parseInteger() function for safer number parsing | ||
## v1.3.1 (2016-09-12) | ||
@@ -9,0 +13,0 @@ |
@@ -5,3 +5,3 @@ /* | ||
var mod_assert = require('assert'); | ||
var mod_assert = require('assert-plus'); | ||
var mod_util = require('util'); | ||
@@ -33,2 +33,4 @@ | ||
exports.parseInteger = parseInteger; | ||
exports.iso8601 = iso8601; | ||
@@ -284,2 +286,247 @@ exports.rfc1123 = rfc1123; | ||
/* | ||
* Number.*_SAFE_INTEGER isn't present before node v0.12, so we hardcode | ||
* the ES6 definitions here, while allowing for them to someday be higher. | ||
*/ | ||
var MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER || 9007199254740991; | ||
var MIN_SAFE_INTEGER = Number.MIN_SAFE_INTEGER || -9007199254740991; | ||
/* | ||
* Default options for parseInteger(). | ||
*/ | ||
var PI_DEFAULTS = { | ||
base: 10, | ||
allowSign: true, | ||
allowPrefix: false, | ||
allowTrailing: false, | ||
allowImprecise: false, | ||
trimWhitespace: false, | ||
leadingZeroIsOctal: false | ||
}; | ||
var CP_0 = 0x30; | ||
var CP_9 = 0x39; | ||
var CP_A = 0x41; | ||
var CP_B = 0x42; | ||
var CP_O = 0x4f; | ||
var CP_T = 0x54; | ||
var CP_X = 0x58; | ||
var CP_Z = 0x5a; | ||
var CP_a = 0x61; | ||
var CP_b = 0x62; | ||
var CP_o = 0x6f; | ||
var CP_t = 0x74; | ||
var CP_x = 0x78; | ||
var CP_z = 0x7a; | ||
var PI_CONV_DEC = 0x30; | ||
var PI_CONV_UC = 0x37; | ||
var PI_CONV_LC = 0x57; | ||
/* | ||
* A stricter version of parseInt() that provides options for changing what | ||
* is an acceptable string (for example, disallowing trailing characters). | ||
*/ | ||
function parseInteger(str, uopts) | ||
{ | ||
mod_assert.string(str, 'str'); | ||
mod_assert.optionalObject(uopts, 'options'); | ||
var baseOverride = false; | ||
var options = PI_DEFAULTS; | ||
if (uopts) { | ||
baseOverride = hasKey(uopts, 'base'); | ||
options = mergeObjects(options, uopts); | ||
mod_assert.number(options.base, 'options.base'); | ||
mod_assert.ok(options.base >= 2, 'options.base >= 2'); | ||
mod_assert.ok(options.base <= 36, 'options.base <= 36'); | ||
mod_assert.bool(options.allowSign, 'options.allowSign'); | ||
mod_assert.bool(options.allowPrefix, 'options.allowPrefix'); | ||
mod_assert.bool(options.allowTrailing, | ||
'options.allowTrailing'); | ||
mod_assert.bool(options.allowImprecise, | ||
'options.allowImprecise'); | ||
mod_assert.bool(options.trimWhitespace, | ||
'options.trimWhitespace'); | ||
mod_assert.bool(options.leadingZeroIsOctal, | ||
'options.leadingZeroIsOctal'); | ||
if (options.leadingZeroIsOctal) { | ||
mod_assert.ok(!baseOverride, | ||
'"base" and "leadingZeroIsOctal" are ' + | ||
'mutually exclusive'); | ||
} | ||
} | ||
var c; | ||
var pbase = -1; | ||
var base = options.base; | ||
var start; | ||
var mult = 1; | ||
var value = 0; | ||
var idx = 0; | ||
var len = str.length; | ||
/* Trim any whitespace on the left side. */ | ||
if (options.trimWhitespace) { | ||
while (idx < len && isSpace(str.charCodeAt(idx))) { | ||
++idx; | ||
} | ||
} | ||
/* Check the number for a leading sign. */ | ||
if (options.allowSign) { | ||
if (str[idx] === '-') { | ||
idx += 1; | ||
mult = -1; | ||
} else if (str[idx] === '+') { | ||
idx += 1; | ||
} | ||
} | ||
/* Parse the base-indicating prefix if there is one. */ | ||
if (str[idx] === '0') { | ||
if (options.allowPrefix) { | ||
pbase = prefixToBase(str.charCodeAt(idx + 1)); | ||
if (pbase !== -1 && (!baseOverride || pbase === base)) { | ||
base = pbase; | ||
idx += 2; | ||
} | ||
} | ||
if (pbase === -1 && options.leadingZeroIsOctal) { | ||
base = 8; | ||
} | ||
} | ||
/* Parse the actual digits. */ | ||
for (start = idx; idx < len; ++idx) { | ||
c = translateDigit(str.charCodeAt(idx)); | ||
if (c !== -1 && c < base) { | ||
value *= base; | ||
value += c; | ||
} else { | ||
break; | ||
} | ||
} | ||
/* If we didn't parse any digits, we have an invalid number. */ | ||
if (start === idx) { | ||
return (new Error('invalid number: ' + JSON.stringify(str))); | ||
} | ||
/* Trim any whitespace on the right side. */ | ||
if (options.trimWhitespace) { | ||
while (idx < len && isSpace(str.charCodeAt(idx))) { | ||
++idx; | ||
} | ||
} | ||
/* Check for trailing characters. */ | ||
if (idx < len && !options.allowTrailing) { | ||
return (new Error('trailing characters after number: ' + | ||
JSON.stringify(str.slice(idx)))); | ||
} | ||
/* If our value is 0, we return now, to avoid returning -0. */ | ||
if (value === 0) { | ||
return (0); | ||
} | ||
/* Calculate our final value. */ | ||
var result = value * mult; | ||
/* | ||
* If the string represents a value that cannot be precisely represented | ||
* by JavaScript, then we want to check that: | ||
* | ||
* - We never increased the value past MAX_SAFE_INTEGER | ||
* - We don't make the result negative and below MIN_SAFE_INTEGER | ||
* | ||
* Because we only ever increment the value during parsing, there's no | ||
* chance of moving past MAX_SAFE_INTEGER and then dropping below it | ||
* again, losing precision in the process. This means that we only need | ||
* to do our checks here, at the end. | ||
*/ | ||
if (!options.allowImprecise && | ||
(value > MAX_SAFE_INTEGER || result < MIN_SAFE_INTEGER)) { | ||
return (new Error('number is outside of the supported range: ' + | ||
JSON.stringify(str.slice(start, idx)))); | ||
} | ||
return (result); | ||
} | ||
/* | ||
* Interpret a character code as a base-36 digit. | ||
*/ | ||
function translateDigit(d) | ||
{ | ||
if (d >= CP_0 && d <= CP_9) { | ||
/* '0' to '9' -> 0 to 9 */ | ||
return (d - PI_CONV_DEC); | ||
} else if (d >= CP_A && d <= CP_Z) { | ||
/* 'A' - 'Z' -> 10 to 35 */ | ||
return (d - PI_CONV_UC); | ||
} else if (d >= CP_a && d <= CP_z) { | ||
/* 'a' - 'z' -> 10 to 35 */ | ||
return (d - PI_CONV_LC); | ||
} else { | ||
/* Invalid character code */ | ||
return (-1); | ||
} | ||
} | ||
/* | ||
* Test if a value matches the ECMAScript definition of trimmable whitespace. | ||
*/ | ||
function isSpace(c) | ||
{ | ||
return (c === 0x20) || | ||
(c >= 0x0009 && c <= 0x000d) || | ||
(c === 0x00a0) || | ||
(c === 0x1680) || | ||
(c === 0x180e) || | ||
(c >= 0x2000 && c <= 0x200a) || | ||
(c === 0x2028) || | ||
(c === 0x2029) || | ||
(c === 0x202f) || | ||
(c === 0x205f) || | ||
(c === 0x3000) || | ||
(c === 0xfeff); | ||
} | ||
/* | ||
* Determine which base a character indicates (e.g., 'x' indicates hex). | ||
*/ | ||
function prefixToBase(c) | ||
{ | ||
if (c === CP_b || c === CP_B) { | ||
/* 0b/0B (binary) */ | ||
return (2); | ||
} else if (c === CP_o || c === CP_O) { | ||
/* 0o/0O (octal) */ | ||
return (8); | ||
} else if (c === CP_t || c === CP_T) { | ||
/* 0t/0T (decimal) */ | ||
return (10); | ||
} else if (c === CP_x || c === CP_X) { | ||
/* 0x/0X (hexadecimal) */ | ||
return (16); | ||
} else { | ||
/* Not a meaningful character */ | ||
return (-1); | ||
} | ||
} | ||
function validateJsonObjectJS(schema, input) | ||
@@ -286,0 +533,0 @@ { |
{ | ||
"name": "jsprim", | ||
"version": "1.3.1", | ||
"version": "1.4.0", | ||
"description": "utilities for primitive JavaScript types", | ||
@@ -8,5 +8,6 @@ "main": "./lib/jsprim.js", | ||
"type": "git", | ||
"url": "git://github.com/davepacheco/node-jsprim.git" | ||
"url": "git://github.com/joyent/node-jsprim.git" | ||
}, | ||
"dependencies": { | ||
"assert-plus": "1.0.0", | ||
"extsprintf": "1.0.2", | ||
@@ -13,0 +14,0 @@ "json-schema": "0.2.3", |
@@ -142,2 +142,57 @@ # jsprim: utilities for primitive JavaScript types | ||
### parseInteger(str, options) | ||
Parses the contents of `str` (a string) as an integer. On success, the integer | ||
value is returned (as a number). On failure, an error is **returned** describing | ||
why parsing failed. | ||
By default, leading and trailing whitespace characters are not allowed, nor are | ||
trailing characters that are not part of the numeric representation. This | ||
behaviour can be toggled by using the options below. The empty string (`''`) is | ||
not considered valid input. If the return value cannot be precisely represented | ||
as a number (i.e., is smaller than `Number.MIN_SAFE_INTEGER` or larger than | ||
`Number.MAX_SAFE_INTEGER`), an error is returned. Additionally, the string | ||
`'-0'` will be parsed as the integer `0`, instead of as the IEEE floating point | ||
value `-0`. | ||
This function accepts both upper and lowercase characters for digits, similar to | ||
`parseInt()`, `Number()`, and [strtol(3C)](https://illumos.org/man/3C/strtol). | ||
The following may be specified in `options`: | ||
Option | Type | Default | Meaning | ||
------------------ | ------- | ------- | --------------------------- | ||
base | number | 10 | numeric base (radix) to use, in the range 2 to 36 | ||
allowSign | boolean | true | whether to interpret any leading `+` (positive) and `-` (negative) characters | ||
allowImprecise | boolean | false | whether to accept values that may have lost precision (past `MAX_SAFE_INTEGER` or below `MIN_SAFE_INTEGER`) | ||
allowPrefix | boolean | false | whether to interpret the prefixes `0b` (base 2), `0o` (base 8), `0t` (base 10), or `0x` (base 16) | ||
allowTrailing | boolean | false | whether to ignore trailing characters | ||
trimWhitespace | boolean | false | whether to trim any leading or trailing whitespace/line terminators | ||
leadingZeroIsOctal | boolean | false | whether a leading zero indicates octal | ||
Note that if `base` is unspecified, and `allowPrefix` or `leadingZeroIsOctal` | ||
are, then the leading characters can change the default base from 10. If `base` | ||
is explicitly specified and `allowPrefix` is true, then the prefix will only be | ||
accepted if it matches the specified base. `base` and `leadingZeroIsOctal` | ||
cannot be used together. | ||
**Context:** It's tricky to parse integers with JavaScript's built-in facilities | ||
for several reasons: | ||
- `parseInt()` and `Number()` by default allow the base to be specified in the | ||
input string by a prefix (e.g., `0x` for hex). | ||
- `parseInt()` allows trailing nonnumeric characters. | ||
- `Number(str)` returns 0 when `str` is the empty string (`''`). | ||
- Both functions return incorrect values when the input string represents a | ||
valid integer outside the range of integers that can be represented precisely. | ||
Specifically, `parseInt('9007199254740993')` returns 9007199254740992. | ||
- Both functions always accept `-` and `+` signs before the digit. | ||
- Some older JavaScript engines always interpret a leading 0 as indicating | ||
octal, which can be surprising when parsing input from users who expect a | ||
leading zero to be insignificant. | ||
While each of these may be desirable in some contexts, there are also times when | ||
none of them are wanted. `parseInteger()` grants greater control over what | ||
input's permissible. | ||
### iso8601(date) | ||
@@ -144,0 +199,0 @@ |
30460
617
293
4
+ Addedassert-plus@1.0.0
+ Addedassert-plus@1.0.0(transitive)