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