JavaScript中任意两个数加减的解决方案

写在前面的话

本文是从初步解决到最终解决的思路,文章篇幅较长
虽然是一篇从0开始的文章,中间的思维跳跃可能比较大
代码的解析都在文章的思路分析和注释里,全文会帮助理解的几个关键词

  • Number.MAX_SAFE_INTEGER 和 Number.MIN_SAFE_INTEGER
  • 15长度的字符串
  • padStart 和 padEnd

分析填坑思路

相信很多前端都知道这段神奇的代码吧

console.log(0.1 + 0.2 === 0.3)  // false
console.log(0.3 - 0.2 === 0.1)  // false
复制代码

网络上有很多文章解释,这里就不剖析了。
至少我们可以知道,小数加减是存在问题的!
那怎么解决小数的加减呢?有一个思路:

既然小数加减存在问题,那么避开这个问题。
直接把小数转换成整数后加减计算,这总可以吧。
复制代码

小数的坑现在转到了整数,再看看整数加减的坑...

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
复制代码

Number.MAX_SAFE_INTEGER 是何物?
根据 MDN 里面的定义

常量表示在 JavaScript 中最大的安全整数
复制代码

同理可知,Number.MIN_SAFE_INTEGER 也就是最小的安全整数
整数的加减在最大安全整数和最小安全整数以内的计算才是稳稳的
计算结果安全了么?emmm好像还有一个问题...

console.log(10 ** 21)  // 1e+21
console.log(999999999999999999999)  // 1e+21
复制代码

从上面的结果可以看到,不可能忍受的是

1.最后的输出结果显示的是科学计数法
2.科学计数法表示的数并不能准确知道真实的数是多少
复制代码

既然数字的显示存在这样的问题,把输入结果和输出结果都用字符串表示

console.log(`${
     10 ** 21}`)  // '1e+21'
console.log('' + 10 ** 21)  // '1e+21'
console.log((10 ** 21).toString())  // '1e+21'
复制代码

我们发现即使直接就转换成字符串仍然会显示为科学计数法,那么可以直接输入字符串了,跳过转成字符串的过程

解决整数加法的坑

在这里先试着解决整数加法的问题
这里有几个可能性

1.输入的数字都在安全整数以内相加之后,且计算的结果也在安全整数之内,则直接输出结果
2.如果不满足上面条件的...(等下再说)
复制代码
const MAX = Number.MAX_SAFE_INTEGER
const MIN = Number.MIN_SAFE_INTEGER
/**
* @param { number } num 需要检查的整数
* @return { boolean } 返回数字是否为安全的整数
*/
function isSafeNumber(num) {
    // 即使 num 成了科学计数法也能正确的和 MAX, MIN 比较大小
    return MIN <= num && num <= MAX
}
/**
* @param { string } a 相加的第一个整数字符串
* @param { string } b 相加的第二个整数字符串
* @return { string } 返回相加的结果
*/
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
}
复制代码

如果不满足上面条件的呢?
笔者的思路是

获取数字转成字符串拆分成多个部分(数组),每一个部分的长度为 Number.MAX_SAFE_INTEGER 转成字符串后的长度减一(15),长度不足15的用字符‘0’填充首部,再计算每个部分的结果后拼接在一起
同时考虑到正负号的问题,拆分后的计算需要带上符号
复制代码

长度减一的原因是接下来每部分的所有计算都是安全的,不需要在考虑是数字计算结果为安全的整数
同时每部分计算后的结果存在问题以及笔者的解决方案

注意:下面会使用15这个数字,15上面说过了,是Number.MAX_SAFE_INTEGER的长度减一
1.计算结果为0
    那么这个部分赋值15个字符‘0’组成的字符串,即‘000000000000000’
2.计算结果为负数
    那么向上一级数组借10的15次方,同时高位(下一级数组)减一,低位用10的15次方再加上这个负数,做为这个部分的结果
3.计算结果为正数,判断长度:
    如果长度超过15,那么去掉结果的第一位字符(因为进位,第一个字符一定是‘1’),同时高位(下一级数组)加一
    如果长度没有超过15,向首部补充0直到长度足够15
    如果长度等于15,直接添加到结果中
复制代码

直接上代码吧,里面会有详细的注释

const MAX = Number.MAX_SAFE_INTEGER
const MIN = Number.MIN_SAFE_INTEGER
const intLen = `${MAX}`.length - 1  // 下面会频繁用到的长度 15

function isSafeNumber(num) {
    // 即使 num 成了科学计数法也能正确的和 MAX, MIN 比较大小
    return MIN <= num && num <= MAX
}

// 整数加法函数入口
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  // 即为会用到的整个数组长度
        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)
        // 去掉正负数前面无意义的字符 ‘0’
        const numberResult = Number(result)
        if (numberResult > 0) {
            while (result[0] === '0') {
                result = result.slice(1)
            }
        } else if (
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值