UNPKG

lossless-json

Version:

Parse JSON without risk of losing numeric information

187 lines (179 loc) 5.49 kB
/** * Test whether a string contains an integer number */ export function isInteger(value) { return INTEGER_REGEX.test(value); } const INTEGER_REGEX = /^-?[0-9]+$/; /** * Test whether a string contains a number * http://stackoverflow.com/questions/13340717/json-numbers-regular-expression */ export function isNumber(value) { return NUMBER_REGEX.test(value); } const NUMBER_REGEX = /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/; /** * Test whether a string can be safely represented with a number * without information loss. * * When approx is true, floating point numbers that lose a few digits but * are still approximately equal in value are considered safe too. * Integer numbers must still be exactly equal. */ export function isSafeNumber(value, config) { const num = Number.parseFloat(value); const str = String(num); if (value === str) { return true; } const valueDigits = countSignificantDigits(value); const strDigits = countSignificantDigits(str); if (valueDigits === strDigits) { return true; } if (config?.approx === true) { // A value is approximately equal when: // 1. it is a floating point number, not an integer // 2. it has at least 14 digits // 3. the first 14 digits are equal const requiredDigits = 14; if (strDigits >= requiredDigits && !isInteger(value)) { return true; } } return false; } export let UnsafeNumberReason = /*#__PURE__*/function (UnsafeNumberReason) { UnsafeNumberReason["underflow"] = "underflow"; UnsafeNumberReason["overflow"] = "overflow"; UnsafeNumberReason["truncate_integer"] = "truncate_integer"; UnsafeNumberReason["truncate_float"] = "truncate_float"; return UnsafeNumberReason; }({}); /** * When the provided value is an unsafe number, describe what the reason is: * overflow, underflow, truncate_integer, or truncate_float. * Returns undefined when the value is safe. */ export function getUnsafeNumberReason(value) { if (isSafeNumber(value, { approx: false })) { return undefined; } if (isInteger(value)) { return UnsafeNumberReason.truncate_integer; } const num = Number.parseFloat(value); if (!Number.isFinite(num)) { return UnsafeNumberReason.overflow; } if (num === 0) { return UnsafeNumberReason.underflow; } return UnsafeNumberReason.truncate_float; } /** * Convert a string into a number when it is safe to do so. * Throws an error otherwise, explaining the reason. */ export function toSafeNumberOrThrow(value, config) { const number = Number.parseFloat(value); const unsafeReason = getUnsafeNumberReason(value); if (config?.approx === true ? unsafeReason && unsafeReason !== UnsafeNumberReason.truncate_float : unsafeReason) { const unsafeReasonText = unsafeReason?.replace(/_\w+$/, ''); throw new Error(`Cannot safely convert to number: the value '${value}' would ${unsafeReasonText} and become ${number}`); } return number; } /** * Split a number into sign, digits, and exponent. * The value can be constructed again from a split number by inserting a dot * at the second character of the digits if there is more than one digit, * prepending it with the sign, and appending the exponent like `e${exponent}` */ export function splitNumber(value) { const match = value.match(/^(-?)(\d+\.?\d*)([eE]([+-]?\d+))?$/); if (!match) { throw new SyntaxError(`Invalid number: ${value}`); } const sign = match[1]; const digitsStr = match[2]; let exponent = match[4] !== undefined ? Number.parseInt(match[4]) : 0; const dot = digitsStr.indexOf('.'); exponent += dot !== -1 ? dot - 1 : digitsStr.length - 1; const digits = digitsStr.replace('.', '') // remove the dot (must be removed before removing leading zeros) .replace(/^0*/, zeros => { // remove leading zeros, add their count to the exponent exponent -= zeros.length; return ''; }).replace(/0*$/, ''); // remove trailing zeros return digits.length > 0 ? { sign, digits, exponent } : { sign, digits: '0', exponent: exponent + 1 }; } /** * Compare two strings containing a numeric value * Returns 1 when a is larger than b, 0 when they are equal, * and -1 when a is smaller than b. */ export function compareNumber(a, b) { if (a === b) { return 0; } const aa = splitNumber(a); const bb = splitNumber(b); const sign = aa.sign === '-' ? -1 : 1; if (aa.sign !== bb.sign) { if (aa.digits === '0' && bb.digits === '0') { return 0; } return sign; } if (aa.exponent !== bb.exponent) { return aa.exponent > bb.exponent ? sign : aa.exponent < bb.exponent ? -sign : 0; } return aa.digits > bb.digits ? sign : aa.digits < bb.digits ? -sign : 0; } /** * Count the significant digits of a number. * * For example: * '2.34' returns 3 * '-77' returns 2 * '0.003400' returns 2 * '120.5e+30' returns 4 **/ export function countSignificantDigits(value) { let start = 0; if (value[0] === '-') { start++; } while (value[start] === '0' || value[start] === '.') { start++; } let end = value.lastIndexOf('e'); if (end === -1) { end = value.lastIndexOf('E'); } if (end === -1) { end = value.length; } while (value[end - 1] === '0' || value[end - 1] === '.') { end--; } let digits = end >= start ? end - start : 0; const dot = value.indexOf('.', start); if (dot !== -1 && dot < end) { digits--; } return digits; } //# sourceMappingURL=utils.js.map