lossless-json
Version:
Parse JSON without risk of losing numeric information
187 lines (179 loc) • 5.49 kB
JavaScript
/**
* 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