2 * @fileoverview Rule to flag numbers that will lose significant figure precision at runtime
8 //------------------------------------------------------------------------------
10 //------------------------------------------------------------------------------
17 description: "disallow literal numbers that lose precision",
18 category: "Possible Errors",
20 url: "https://eslint.org/docs/rules/no-loss-of-precision"
24 noLossOfPrecision: "This number literal will lose precision at runtime."
31 * Returns whether the node is number literal
32 * @param {Node} node the node literal being evaluated
33 * @returns {boolean} true if the node is a number literal
35 function isNumber(node) {
36 return typeof node.value === "number";
40 * Gets the source code of the given number literal. Removes `_` numeric separators from the result.
41 * @param {Node} node the number `Literal` node
42 * @returns {string} raw source code of the literal, without numeric separators
44 function getRaw(node) {
45 return node.raw.replace(/_/gu, "");
49 * Checks whether the number is base ten
50 * @param {ASTNode} node the node being evaluated
51 * @returns {boolean} true if the node is in base ten
53 function isBaseTen(node) {
54 const prefixes = ["0x", "0X", "0b", "0B", "0o", "0O"];
56 return prefixes.every(prefix => !node.raw.startsWith(prefix)) &&
57 !/^0[0-7]+$/u.test(node.raw);
61 * Checks that the user-intended non-base ten number equals the actual number after is has been converted to the Number type
62 * @param {Node} node the node being evaluated
63 * @returns {boolean} true if they do not match
65 function notBaseTenLosesPrecision(node) {
66 const rawString = getRaw(node).toUpperCase();
69 if (rawString.startsWith("0B")) {
71 } else if (rawString.startsWith("0X")) {
77 return !rawString.endsWith(node.value.toString(base).toUpperCase());
81 * Adds a decimal point to the numeric string at index 1
82 * @param {string} stringNumber the numeric string without any decimal point
83 * @returns {string} the numeric string with a decimal point in the proper place
85 function addDecimalPointToNumber(stringNumber) {
86 return `${stringNumber.slice(0, 1)}.${stringNumber.slice(1)}`;
90 * Returns the number stripped of leading zeros
91 * @param {string} numberAsString the string representation of the number
92 * @returns {string} the stripped string
94 function removeLeadingZeros(numberAsString) {
95 return numberAsString.replace(/^0*/u, "");
99 * Returns the number stripped of trailing zeros
100 * @param {string} numberAsString the string representation of the number
101 * @returns {string} the stripped string
103 function removeTrailingZeros(numberAsString) {
104 return numberAsString.replace(/0*$/u, "");
108 * Converts an integer to to an object containing the integer's coefficient and order of magnitude
109 * @param {string} stringInteger the string representation of the integer being converted
110 * @returns {Object} the object containing the integer's coefficient and order of magnitude
112 function normalizeInteger(stringInteger) {
113 const significantDigits = removeTrailingZeros(removeLeadingZeros(stringInteger));
116 magnitude: stringInteger.startsWith("0") ? stringInteger.length - 2 : stringInteger.length - 1,
117 coefficient: addDecimalPointToNumber(significantDigits)
123 * Converts a float to to an object containing the floats's coefficient and order of magnitude
124 * @param {string} stringFloat the string representation of the float being converted
125 * @returns {Object} the object containing the integer's coefficient and order of magnitude
127 function normalizeFloat(stringFloat) {
128 const trimmedFloat = removeLeadingZeros(stringFloat);
130 if (trimmedFloat.startsWith(".")) {
131 const decimalDigits = trimmedFloat.split(".").pop();
132 const significantDigits = removeLeadingZeros(decimalDigits);
135 magnitude: significantDigits.length - decimalDigits.length - 1,
136 coefficient: addDecimalPointToNumber(significantDigits)
141 magnitude: trimmedFloat.indexOf(".") - 1,
142 coefficient: addDecimalPointToNumber(trimmedFloat.replace(".", ""))
149 * Converts a base ten number to proper scientific notation
150 * @param {string} stringNumber the string representation of the base ten number to be converted
151 * @returns {string} the number converted to scientific notation
153 function convertNumberToScientificNotation(stringNumber) {
154 const splitNumber = stringNumber.replace("E", "e").split("e");
155 const originalCoefficient = splitNumber[0];
156 const normalizedNumber = stringNumber.includes(".") ? normalizeFloat(originalCoefficient)
157 : normalizeInteger(originalCoefficient);
158 const normalizedCoefficient = normalizedNumber.coefficient;
159 const magnitude = splitNumber.length > 1 ? (parseInt(splitNumber[1], 10) + normalizedNumber.magnitude)
160 : normalizedNumber.magnitude;
162 return `${normalizedCoefficient}e${magnitude}`;
167 * Checks that the user-intended base ten number equals the actual number after is has been converted to the Number type
168 * @param {Node} node the node being evaluated
169 * @returns {boolean} true if they do not match
171 function baseTenLosesPrecision(node) {
172 const normalizedRawNumber = convertNumberToScientificNotation(getRaw(node));
173 const requestedPrecision = normalizedRawNumber.split("e")[0].replace(".", "").length;
175 if (requestedPrecision > 100) {
178 const storedNumber = node.value.toPrecision(requestedPrecision);
179 const normalizedStoredNumber = convertNumberToScientificNotation(storedNumber);
181 return normalizedRawNumber !== normalizedStoredNumber;
186 * Checks that the user-intended number equals the actual number after is has been converted to the Number type
187 * @param {Node} node the node being evaluated
188 * @returns {boolean} true if they do not match
190 function losesPrecision(node) {
191 return isBaseTen(node) ? baseTenLosesPrecision(node) : notBaseTenLosesPrecision(node);
197 if (node.value && isNumber(node) && losesPrecision(node)) {
199 messageId: "noLossOfPrecision",