The words written in the front
This article is from the initial solution to the final solution of the idea, although the length of the article is long, it is an article starting from 0, the middle of the thinking jump may be relatively large code analysis are in the article idea analysis and notes, the full text will help understand several key words
- Number. MAX_SAFE_INTEGER and Number. MIN_SAFE_INTEGER
- The value is a string of 15 characters
- PadStart and padEnd
Analyze the idea of pit filling
Many front ends know this magic code
console.log(0.1 + 0.2= = =0.3) // false
console.log(0.3 - 0.2= = =0.1) // false
Copy the code
There are many articles on the Internet to explain it, but I won’t analyze it here. At least we know that adding and subtracting decimals is problematic! So how do we add and subtract decimals? Here’s an idea:
Since adding and subtracting decimals is a problem, avoid it. You can add and subtract by converting decimals to whole numbers.Copy the code
The decimal pit is now an integer, and look at the integer plus and minus pit…
const max = Number.MAX_SAFE_INTEGER
console.log(max) / / 9007199254740991
console.log(max + 2) / / 9007199254740992
const min = Number.MIN_SAFE_INTEGER
console.log(min) / / - 9007199254740991
console.log(min - 2) / / - 9007199254740992
Copy the code
What is number. MAX_SAFE_INTEGER? According to the definition in MDN
Constants represent the largest safe integer in JavaScriptCopy the code
Number.MIN_SAFE_INTEGER is the smallest safe integer that can be added or subtracted from the largest safe integer and the smallest safe integer. Emmm seems to have another problem…
console.log(10 ** 21) // 1e+21
console.log(999999999999999999999) // 1e+21
Copy the code
As can be seen from the results above, the impossible is
1. The final output shows scientific notation 2. Scientific notation does not tell you exactly what the real number isCopy the code
Since numeric displays have such problems, both input and output results are represented as strings
console.log(`The ${10 ** 21}`) // '1e+21'
console.log(' ' + 10 ** 21) // '1e+21'
console.log((10 ** 21).toString()) // '1e+21'
Copy the code
We found that even if we converted directly to a string it would still show up as a scientific notation, so we can just type in the string and skip the conversion
Solve the integer addition pit
So let’s try to solve the integer addition problem here and there are a couple of possibilities
1. If all the entered digits are added within the safe integer and the calculated result is also within the safe integer, result 2 is displayed. If the above conditions are not met... (More on that later)Copy the code
const MAX = Number.MAX_SAFE_INTEGER
const MIN = Number.MIN_SAFE_INTEGER
/** * @param {number} num Integer to check * @return {Boolean} Returns whether the number is a safe integer */
function isSafeNumber(num) {
// Even if num becomes scientific notation, it can be correctly compared with MAX and MIN
return MIN <= num && num <= MAX
}
/** * @param {string} the first integer string added to a * @param {string} the second integer string added to b * @return {string} returns the result of the addition */
function IntAdd(a = ' ', b = ' ') {
let resulte = '0'
const intA = Number(a), intB = Number(b)
if (intA === 0) return b
if (intB === 0) return a
if (isSafeNumber(intA) && isSafeNumber(intB) && isSafeNumber(intA + intB)) {
resulte = intA + intB
} else {
resulte = IntCalc(a, b)
}
return resulte
}
function IntCalc(a, b) {
// TODO
}
Copy the code
What if it doesn’t? The author’s thinking is
MAX_SAFE_INTEGER The length of the converted string is reduced by one (15). If the length is less than 15, fill the head with the character '0'. Then calculate the results of each part and join them together. The split calculation needs to be signedCopy the code
The reason why the length is reduced by one is that all the subsequent calculations of each part are safe, and there is no need to consider that the numerical calculation result is a safe integer, and there are problems in the calculation result of each part and the author’s solution
Note: the Number 15 will be used below. 15, as stated above, is the length of number. MAX_SAFE_INTEGER minus 1. If the result is 0 then this part is assigned a string of 15 characters' 0 ', i.e. '000000000000000' 2. If it's a negative number, I'll borrow 10 to the 15th from the top, subtract one from the top, and I'll add 10 to the 15th plus the negative number for this part, 3. Calculation result is positive, judge length: if the length more than 15, then remove the results of the first character (carry because it is over, the first character must be a '1'), at the same time high array (lower level) plus one If the length is no more than 15, added to the first 0 until enough 15 if the length is equal to the length of 15, directly added to the resultCopy the code
Go straight to the code, there will be detailed comments
const MAX = Number.MAX_SAFE_INTEGER
const MIN = Number.MIN_SAFE_INTEGER
const intLen = `${MAX}`.length - 1 // The length 15 is used frequently
function isSafeNumber(num) {
// Even if num becomes scientific notation, it can be correctly compared with MAX and MIN
return MIN <= num && num <= MAX
}
// Integer addition function entry
function intAdd(a = '0', b = '0') {
const statusObj = checkNumber(a, b)
if(! statusObj.status) {return statusObj.data
} else {
const tagA = Number(a) < 0, tagB = Number(b) < 0
const strA = `${a}`, strB = `${b}`
const lenA = tagA ? strA.length - 1 : strA.length
const lenB = tagB ? strB.length - 1 : strB.length
const maxLen = Math.max(lenA, lenB)
const padLen = Math.ceil(maxLen / intLen) * intLen // Is the entire array length to be used
const newA = tagA ? ` -${strA.slice(1).padStart(padLen, '0')}` : strA.padStart(padLen, '0')
const newB = tagB ? ` -${strB.slice(1).padStart(padLen, '0')}` : strB.padStart(padLen, '0')
let result = intCalc(newA, newB)
// Remove the null character '0' before positive and negative numbers
const numberResult = Number(result)
if (numberResult > 0) {
while (result[0= = ='0') {
result = result.slice(1)}}else if (numberResult < 0) {
while (result[1= = ='0') {
result = The '-' + result.slice(2)}}else {
result = '0'
}
console.log(result)
return result
}
}
/** * @param {string} the first integer string added to a * @param {string} the second integer string added to b * @return {string} returns the result of the addition */
function intCalc(a, b) {
let result = '0'
const intA = Number(a), intB = Number(b)
// Check whether the number is safe, and enter complex calculation mode if the number is not safe
if (isSafeNumber(intA) && isSafeNumber(intB) && isSafeNumber(intA + intB)) {
result = `${intA + intB}`
} else {
const sliceA = a.slice(1), sliceB = b.slice(1)
if(a[0= = =The '-' && b[0= = =The '-') {
// Both numbers are negative numbers
result = The '-' + calc(sliceA, sliceB, true)}else if (a[0= = =The '-') {
// The first number is negative and the second number is positive
const newV = compareNumber(sliceA, b)
if (newV === 1) {
// Since the absolute value of a is greater than that of b, the absolute value of a is used as the first argument to ensure that the result returned is positive
result = The '-' + calc(sliceA, b, false)}else if (newV === - 1) {
// For the same reason
result = calc(b, sliceA, false)}}else if (b[0= = =The '-') {
// The first number is positive and the second number is negative
const newV = compareNumber(sliceB, a)
if (newV === 1) {
// Since the absolute value of b is greater than that of A, the absolute value of b is used as the first argument to ensure that the result returned is positive
result = The '-' + calc(sliceB, a, false)}else if (newV === - 1) {
// For the same reason
result = calc(a, sliceB, false)}}else {
// If both numbers are positive, count directly
result = calc(a, b, true)}}return result
}
/** * @param {string} a compares the first integer string * @param {string} b compares the second integer string * @return {object} returns the status of whether to exit the function and the data returned by exit function */
function checkNumber(a, b) {
const obj = {
status: true.data: null
}
const typeA = typeof(a), typeB = typeof(b)
const allowTypes = ['number'.'string']
if(! allowTypes.includes(typeA) || ! allowTypes.includes(typeB)) {console.error('Invalid data in argument, data type only supports number and string')
obj.status = false
obj.data = false
}
if (Number.isNaN(a) || Number.isNaN(b)) {
console.error('NaN should not exist in arguments')
obj.status = false
obj.data = false
}
const intA = Number(a), intB = Number(b)
if (intA === 0) {
obj.status = false
obj.data = b
}
if (intB === 0) {
obj.status = false
obj.data = a
}
const inf = [Infinity, -Infinity]
if (inf.includes(intA) || inf.includes(intB)) {
console.error('There's Infinity or -infinity in the argument')
obj.status = false
obj.data = false
}
return obj
}
/** * @param {string} a compares the first integer string * @param {string} b compares the second integer string * @return {Boolean} returns the comparison of the first argument with the second argument */
function compareNumber(a, b) {
if (a === b) return 0
if (a.length > b.length) {
return 1
} else if (a.length < b.length) {
return - 1
} else {
for (let i=0; i<a.length; i++) {
if (a[i] > b[i]) {
return 1
} else if (a[i] < b[i]) {
return - 1}}}}/** * @param {string} the first integer string added to a * @param {string} the second integer string added to b * @param {string} type Two arguments are added (true) (false) * @return {string} returns the sum */
function calc(a, b, type = true) {
const arr = [] // An array of results for each part
for (let i=0; i<a.length; i+=intLen) {
// Each section is a 15-length crop string
const strA = a.slice(i, i + intLen)
const strB = b.slice(i, i + intLen)
const newV = Number(strA) + Number(strB) * (type ? 1 : - 1) // The calculation results of each part are not processed for the time being
arr.push(`${newV}`)}let num = ' ' // Concatenate each part of the string
for (let i=arr.length- 1; i>=0; i--) {
if (arr[i] > 0) {
// The result of each part is greater than 0
const str = `${arr[i]}`
if (str.length < intLen) {
// Prefix supplementary character '0' with length less than 15
num = str.padStart(intLen, '0') + num
} else if (str.length > intLen) {
If the length exceeds 15, discard the first part and increment the next part
num = str.slice(1) + num
if (i >= 1 && str[0]! = ='0') arr[i- 1] + +else num = '1' + num
} else {
// The length equals 15
num = str + num
}
} else if(arr[i] < 0) {
// If the result of each part is less than 0, the result is always positive, and the head is filled with characters' 0 'to 15 bits
const newV = `The ${10 ** intLen + Number(arr[i])}`
num = newV.padStart(intLen, '0') + num
if (i >= 1) arr[i- 1] -}else {
// The result of each part is equal to 0, 15 consecutive characters' 0 '
num = '0'.padStart(intLen, '0') + num
}
}
return num
}
Copy the code
See the code for the test results section here
console.log(MAX) / / 9007199254740991
intAdd(MAX, '2') / / '9007199254740993'
intAdd(MAX, '10000000000000000') / / '19007199254740991'
// Test 10 ^ 21 data 1000000000000000000000
intAdd(MAX, '1000000000000000000000') / / '1000009007199254740991'
intAdd(MAX, ` -The ${10 ** 16}`) // '-992800745259009'
// There is still a problem with not using strings in calculations, as follows
intAdd(MAX, `The ${10 ** 21}`) / / '10.0000000071992548 e+21'
intAdd(MAX, ` -The ${10 ** 21}`) / / '0'
Copy the code
Of course, considering that the general calculation does not use large numbers, it does feel strange to write string addition, you can add judgment in the function, which is a hint of scientific notation and convert to base 10, code improvement
// Integer addition function entry
function intAdd(a = '0', b = '0') {
const statusObj = checkNumber(a, b)
if(! statusObj.status) {return statusObj.data
} else {
let newA, newB, maxLen
const tagA = Number(a) < 0, tagB = Number(b) < 0
const strA = `${a}`, strB = `${b}`
const reg = / ^ \ -? (\d+)(\.\d+)? e\+(\d+)$/
if(reg.test(a) || reg.test(b)) {
console.warn('Due to the existence of scientific notation, the calculation result may not be accurate, please convert it to a string and calculate it')
a = strA.replace(reg, function(. rest){
const str = rest[2]? rest[1] + rest[2].slice(1) : rest[1]
return str.padEnd(Number(rest[3]) + 1.'0')
})
b = strB.replace(reg, function(. rest){
const str = rest[2]? rest[1] + rest[2].slice(1) : rest[1]
return str.padEnd(Number(rest[3]) + 1.'0')
})
maxLen = Math.max(a.length, b.length)
} else {
const lenA = tagA ? strA.length - 1 : strA.length
const lenB = tagB ? strB.length - 1 : strB.length
maxLen = Math.max(lenA, lenB)
}
const padLen = Math.ceil(maxLen / intLen) * intLen // Is the entire array length to be used
newA = tagA ? ` -${strA.slice(1).padStart(padLen, '0')}` : strA.padStart(padLen, '0')
newB = tagB ? ` -${strB.slice(1).padStart(padLen, '0')}` : strB.padStart(padLen, '0')
let result = intCalc(newA, newB)
// Remove the null character '0' before positive and negative numbers
const numberResult = Number(result)
if (numberResult > 0) {
while (result[0= = ='0') {
result = result.slice(1)}}else if (numberResult < 0) {
while (result[1= = ='0') {
result = The '-' + result.slice(2)}}else {
result = '0'
}
console.log(result)
return result
}
}
Copy the code
Continue testing the code for this section of the code see here
// Warning: Due to the existence of scientific notation, the calculation result may not be accurate, please convert to a string and calculate
intAdd(MAX, 10 ** 21) / / '1000009007199254740991'
// Warning: Due to the existence of scientific notation, the calculation result may not be accurate, please convert to a string and calculate
intAdd(MAX, 10 ** 21 + 2) / / '1000009007199254740991'
intAdd(MAX, NaN) // Error: NaN should not exist in argument
intAdd(MAX, {}) // Error: Invalid data exists in the parameter. Only number and string are supported
// Large number calculation
intAdd('9037499254750994'.'9007299251310995') / / '30200003439999'
intAdd('8107499231750996'.'9007299254310995') // '-899800022559999'
intAdd('9907492547350994'.'9007399254750995') // '-900093292599999'
intAdd('9997492547350994'.'9997399254750995') / / '19994891802101989'
intAdd('9997492547350994'.'9997399254750995') // '-19994891802101989'
intAdd('4707494254750996000004254750996'.'9707494254750996007299232150995') / / '5000000000000000007294977399999'
intAdd('4707494254750996900004254750996'.'9707494254750996007299232150995') / / '4999999999999999107294977399999'
Copy the code
Solve integer subtraction pit
The same with addition and subtraction, you just need to take the second parameter and use the addition operation, since the template has been extracted, you can directly define the subtraction function
// Integer subtraction function entry
function intSub(a = '0', b = '0') {
const newA = `${a}`
const newB = Number(b) > 0 ? ` -${b}`: `${b}`.slice(1)
const statusObj = checkNumber(newA, newB)
if(! statusObj.status) {return statusObj.data
} else {
const result = IntAdd(newA, newB)
return result
}
}
Copy the code
The test results
IntSub('9037499254750994'.'9007299251310995') / / '18044798506061989'
IntSub('8107499231750996'.'9007299254310995') / / '17114798486061991'
IntSub('9907492547350994'.'9007399254750995') // '-18914891802101989'
IntSub('9997492547350994'.'9997399254750995') / / '93292599999'
IntSub('4707494254750996000004254750996'.'9707494254750996007299232150995') // '-14414988509501992007303486901991'
IntSub('4707494254750996900004254750996'.'9707494254750996007299232150995') // '-14414988509501992907303486901991'
Copy the code
Solve the decimal addition pit
The small add and subtract pit in JavaScript is due to floating-point accuracy calculations. There are many articles on the web, but I’m not going to start with floating-point calculations. Since we have solved the problem of integer addition and subtraction before, we can also use the principle of integer addition and subtraction to realize decimals.
The 'padStart' function often appears in the integer addition code, because adding the character '0' before the integer has no effect on itself. The same principle applies to decimals, adding zeros to the end of the decimal has no effect on the decimal, and then adding and subtracting the resulting number is calculated by adding and subtracting integers.Copy the code
Based on the idea of integer addition
// Decimal addition function entry
function floatAdd(a = '0', b = '0') {
const statusObj = checkNumber(a, b)
if(! statusObj.status) {return statusObj.data
} else {
const strA = `${a}`.split('. '), strB = `${b}`.split('. ')
let newA = strA[1], newB = strB[1]
const maxLen = Math.max(newA.length, newB.length)
const floatLen = Math.ceil(maxLen / intLen) * intLen
newA = newA.padEnd(floatLen, '0')
newB = newB.padEnd(floatLen, '0')
newA = strA[0] [0= = =The '-' ? ` -${newA}` : newA
newB = strB[0] [0= = =The '-' ? ` -${newB}` : newB
let result = intCalc(newA, newB)
let tag = true, numResult = Number(result)
// Remove the null character '0' after positive and negative numbers
if(numResult ! = =0) {
if (numResult < 0) {
result = result.slice(1)
tag = false
}
result = result.length === floatLen ? ` 0.${result}` : ` 1.${result.slice(1)}`
result = tag ? result : ` -${result}`
let index = result.length - 1
while (result[index] === '0') {
result = result.slice(0.- 1)
index--
}
} else {
result = '0'
}
console.log(result)
return result
}
}
Copy the code
See the code for the test results section here
floatAdd('0.9037499254750994'.'0.9007299251310995') / / '0.0030200003439999'
floatAdd('0.8107499231750996'.'0.9007299254310995') // '-0.0899800022559999'
floatAdd('0.9907492547350994'.'0.9007399254750995') // '-0.0900093292599999'
floatAdd('0.9997492547350994'.'0.9997399254750995') / / '1.9994891802101989'
floatAdd('0.9997492547350994'.'0.9997399254750995') // '-1.9994891802101989'
floatAdd('0.4707494254750996000004254750996'.'0.9707494254750996007299232150995') / / '0.5000000000000000007294977399999'
floatAdd('0.4707494254750996900004254750996'.'0.9707494254750996007299232150995') / / '0.4999999999999999107294977399999'
Copy the code
Solve the decimal subtraction pit
In the same way that integer subtraction works, subtraction functions can be defined directly
// Decimal subtraction function entry
function floatSub(a = '0', b = '0') {
const newA = `${a}`
const newB = Number(b) > 0 ? ` -${b}`: `${b.slice(1)}`
const statusObj = checkNumber(newA, newB)
if(! statusObj.status) {return statusObj.data
} else {
const result = floatAdd(newA, newB)
return result
}
}
Copy the code
The test results of the above part of the code see here
floatSub('0.9037499254750994'.'0.9007299251310995') / / '1.8044798506061989'
floatSub('0.8107499231750996'.'0.9007299254310995') / / '1.7114798486061991'
floatSub('0.9907492547350994'.'0.9007399254750995') // '-1.8914891802101989'
floatSub('0.9997492547350994'.'0.9997399254750995') / / '0.0000093292599999'
floatSub('0.9997492547350994'.'0.9997399254750995') // '-0.0000093292599999'
floatSub('0.4707494254750996000004254750996'.'0.9707494254750996007299232150995') // '-1.4414988509501992007303486901991'
floatSub('0.4707494254750996900004254750996'.'0.9707494254750996007299232150995') // '-1.4414988509501992907303486901991'
Copy the code
Solve the general problem of integer plus decimal
As a result of the actual number encountered in many cases is integer plus decimal, the following analysis began
So the idea here is again to fill in zeros and zeros in front and zeros in back and then you add them together and then you add the integers and then you insert the decimal point based on the length of the integers that you saved and all you're left with is to get rid of the zeros that don't make sense and print out the resultCopy the code
This is when you have a side that doesn’t have a decimal
// Entry to any number of addition functions
function allAdd(a = '0', b = '0') {
const statusObj = checkNumber(a, b)
if(! statusObj.status) {return statusObj.data
} else {
const strA = `${a}`.split('. '), strB = `${b}`.split('. ')
let intAs = strA[0], floatA = strA.length === 1 ? '0' : strA[1]
let intBs = strB[0], floatB = strB.length === 1 ? '0' : strB[1]
const tagA = intAs > 0, tagB = intBs > 0
const maxIntLen = Math.max(intAs.length, intBs.length)
const arrIntLen = Math.ceil(maxIntLen / intLen) * intLen
const maxFloatLen = Math.max(floatA.length, floatB.length)
const arrFloatLen = Math.ceil(maxFloatLen / intLen) * intLen
intAs = tagA ? intAs.padStart(arrIntLen, '0') : intAs.slice(1).padStart(arrIntLen, '0')
intBs = tagB ? intBs.padStart(arrIntLen, '0') : intBs.slice(1).padStart(arrIntLen, '0')
let newA = floatA === '0' ? intAs + '0'.padEnd(arrFloatLen, '0') : intAs + floatA.padEnd(arrFloatLen, '0')
let newB = floatB === '0' ? intBs + '0'.padEnd(arrFloatLen, '0') : intBs + floatB.padEnd(arrFloatLen, '0')
newA = tagA ? newA : ` -${newA}`
newB = tagB ? newB : ` -${newB}`
let result = intCalc(newA, newB)
const numResult = Number(result)
if (result.length > arrIntLen) {
result = result.slice(0, -arrFloatLen) + '. ' + result.slice(-arrFloatLen)
}
// Remove the null character '0' after positive and negative numbers
if(numResult ! = =0) {
if (numResult > 0) {
while (result[0= = ='0') {
result = result.slice(1)}}else if (numResult < 0) {
while (result[1= = ='0') {
result = The '-' + result.slice(2)
}
result = result.slice(1)
tag = false
}
let index = result.length - 1
while (result[index] === '0') {
result = result.slice(0.- 1)
index--
}
} else {
result = '0'
}
if (result[result.length - 1= = ='. ') {
result = result.slice(0.- 1)}if (result[0= = ='. ') {
result = '0' + result
}
console.log(result)
return result
}
}
// Arbitrary subtraction function entry
function allSub(a = '0', b = '0') {
const newA = `${a}`
const newB = Number(b) > 0 ? ` -${b}`: `${b}`.slice(1)
const statusObj = checkNumber(newA, newB)
if(! statusObj.status) {return statusObj.data
} else {
const result = allAdd(newA, newB)
return result
}
}
Copy the code
The test results of the above part of the code see here
/ / 30200003439999.0030200003439999
allAdd('9037499254750994.9037499254750994'.'9007299251310995.9007299251310995')
/ / 5000000000000000007294977399998.9100199977440001
allAdd('9707494254750996007299232150995.8107499231750996'.'4707494254750996000004254750996.9007299254310995')
/ / 19994891802101990.9994891802101989
allAdd('9997492547350994.9997492547350994'.'9997399254750995.9997399254750995')
/ / 30200003439999.0030200003439999
allSub('9037499254750994.9037499254750994'.'9007299251310995.9007299251310995')
/ / 18044798506061990.8044798506061989
allSub('9037499254750994.9037499254750994'.'9007299251310995.9007299251310995')
/ / 17144998486501991.714499848650199
allSub('8107499231750996.8107499231750996'.'9037499254750994.9037499254750994')
Copy the code
conclusion
This article is too long, so the code part is not detailed (all in the comments) mainly analyzes the whole idea of solving the problem, grasp a few key understanding
- 1. The calculation between number. MAX_SAFE_INTEGER and number. MIN_SAFE_INTEGER can be trusted
- 2. The floating-point accuracy problem of decimal addition and subtraction is transferred to integers
- 3. Partition calculation when adding or subtracting large numbers (reason # 1)
- 4. Split each part into 15-length strings (because the length of Number.MAX_SAFE_INTEGER is 16, so you don’t need to pay attention to the security of addition and subtraction)
- 5. The problem of scientific counting method, whether the match is the number of scientific counting method, and then convert it to decimal, at the same time, it warns that because there are errors in the number of scientific counting method, there will be inaccurate calculation
Code has a lot of places can be optimized, the completion of the comparison of sloppy (light spray) you are welcome to modify the comments
Thanks for watching
Author: @itagn-Github @itagn