JS任意两个数字相加减

2,070 阅读4分钟

写在前面的话

注:本文所讲述内容全部基于这篇文章JavaScript中任意两个数加减的解决方案,经过实践,发现文中所给出的js方法有部分bug,本文的立意就是针对这篇文章作补充说明,所以在阅读本文前建议先阅读这篇文章,这篇文章中所讲述过的内容在本文中将不会重复说明。

补充文章

  1. js浮点数相加问题: 为什么0.1+0.2不等于0.3?原来编程语言是这么算的……
  2. js安全数问题: JavaScript 里最大的安全的整数为什么是2的53次方减一?

计算bug

用上述文章中提供的计算方法进行测试,得到不同结果

1. allAdd('123456789.123456789', '987654321.987654321') // 1111111111.11111111
2. allAdd('1111111111111111111111.12', '1111111111111111111111.12') // 2222222222222222222222.22222222222222224
3. allAdd('0.2', '0.6') // 8
4. allSub('987654321.987654321', '123456789.123456789') // 864197532.864197532
5. allSub('987654321.987654321', '-123456789.123456789') // 1111111111.11111111
6. allSub('2222222222222222222222.24', '1111111111111111111111.12') // 1111111111111111111111.11111111111111112
7. allSub('2222222222222222222222.24', '-1111111111111111111111.12') // 3333333333333333333333.33333333333333336
8. allSub('2.2', '1.6') // 6
9. allSub('1.2', '0.6') // 6
10. allSub('0.2', '0.6') // 
11. allSub('2.2', '6.6') // 4

从上述测试结果可以看出

  • 2、3、6、7、8、9、10、11的计算结果均存在一定的错误
  • 2、6、7的错误类型为:计算结果小数部分位数不对且计算错误
  • 3、9的错误类型为:计算结果整数位为0时,不显示整数位
  • 10、11的错误类型为:计算结果均为负数,负号不显示,只显示整数位(10计算结果为空是由于整数位是0而不显示,这个问题与3、9的错误类型一致

由于该计算方法allSub()的计算原理是将第二个参数b转换成-b的形式进而进行加法运算,故引起上述bug的代码逻辑存在于allAdd()函数中,故对allAdd()函数进行改造

函数改造

改造一:千分位数字参与运算

当数值过于大的时候,常常会以千分位的形式展示,例如345,068,483.92,然而千分位中的,号无法参与运算,故而第一步是将数字转换成普通形式参与运算

function allAdd(a = "0", b = "0") {
  if(a) a = a.replace(/,/g,'')
  if(b) b = b.replace(/,/g,'')
  ...
}
function allSub(a = "0", b = "0") {
  if(a) a = a.replace(/,/g,'')
  if(b) b = b.replace(/,/g,'')
  ...
}

改造二:bug修复之整数位为0时不显示和小数位不准确

引起上述bug的原因,归根结底是由于在执行完let result = intCalc(newA, newB);之后,根据ab的整数部分位数和小数部分位数,进而对result的结果进行处理:确定小数点的位置。 现以allSub('2.2', '1.6')的执行顺序来说明计算错误的原因

image.png

通过打断点的方式可以看出,2.2 - 1.6实际上是000000000000002200000000000000 + -0000000000001600000000000000,计算结果是600000000000000,显而易见,结果少了位数,实际结果应为0600000000000000(当计算结果位数比原有数值位数少时,以0补齐)这样做的目的是为了后续确定小数点位置时,保证小数点的位数正确 进而我们确定了计算结果补0位的逻辑为

image.png

对应的,将原函数中的此处代码

image.png 替换为

function allAdd(a = "0", b = "0") {
    ...
    if(!isNaN(Number(result[0]))){
      // 结果首位不是符号
      if(result.length <= arrIntLen){
        const len = arrIntLen - result.length + 1
        result = new Array(len).fill(0).join('') + result
      }
      result = result.slice(0, -arrFloatLen) + "." + result.slice(-arrFloatLen);
    } else {
      if(result.length - 1 <= arrIntLen) {
        const len = arrIntLen - (result.length - 1) + 1
        result = result.slice(0, 1) + new Array(len).fill(0).join('')  + result.slice(1, result.length - 1)
        let Index = 0
        for(let i = 0, j = result.length; i < j; i++){
          if(!isNaN(Number(result.charAt(i)))){
            Index = i
            break
          }
        }
        const newResult = result.slice(Index, result.length - 1)
        const floatLen = arrIntLen - newResult.length + 1
        result = result.slice(0, Index) + newResult.slice(0, -arrFloatLen + floatLen) + '.' + newResult.slice(-arrFloatLen + floatLen)
      } else {
        result = result.slice(0, -arrFloatLen) + "." + result.slice(-arrFloatLen);
      }
    }
    const numResult = Number(result);
    ...
}

测试结果

1. allAdd('123456789.123456789', '987654321.987654321') // 1111111111.11111111
2. allAdd('1111111111111111111111.12', '1111111111111111111111.12') // 2222222222222222222222.24
3. allAdd('0.2', '0.6') // 0.8
4. allSub('987654321.987654321', '123456789.123456789') // 864197532.864197532
5. allSub('987654321.987654321', '-123456789.123456789') // 1111111111.11111111
6. allSub('2222222222222222222222.24', '1111111111111111111111.12') // 1111111111111111111111.12
7. allSub('2222222222222222222222.24', '-1111111111111111111111.12') // 3333333333333333333333.36
8. allSub('2.2', '1.6') // 0.6
9. allSub('2.2', '6.6') // 4.4
10. allSub('1.2', '0.6') // 0.6
11. allSub('0.2', '0.6') // 0.4

改造三: 负号不显示

到上述改造二为止,我们的bug就只剩下计算allSub('2.2', '6.6')allSub('0.2', '0.6')时数值部分正确,符号位错误,这个bug比较简单,就是单纯的负号问题,直接给出解决方案 将

image.png 改为

function allAdd(a = "0", b = "0") {
    ...
   // 去掉正负数前面后面无意义的字符 ‘0’
    let minusFlag = false
   if(result[0] === '-'){
     minusFlag = true
    }
    if (numResult !== 0) {
      if (numResult > 0) {
        while (result[0] === "0") {
          result = result.slice(1);
        }
      } else if (numResult < 0) {
        while (result[1] === "0") {
          result = "-" + 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;
    }
   if(minusFlag) result = '-' + result
    console.log(result);
    return result;
}

测试结果

allSub('2.2', '6.6') // -4.4
allSub('0.2', '0.6') // -0.4

给出全部函数

const MAX = Number.MAX_SAFE_INTEGER;
const MIN = Number.MIN_SAFE_INTEGER;
const intLen = `${MAX}`.length - 1;
/**
 * @Description: 判断输入的数字是否在javascript的安全系数范围内
 * @param { number } 需要检查的数字
 * @return { boolean }: 返回数字是否为安全的整数
 */
function isSafeNumber(num) {
  // 即使 num 成了科学计数法也能正确的和 MAX, MIN 比较大小
  return MIN <= num && num <= MAX;
}
/**
 * @Description: 计算两个数之差,返回计算结果
 * @param { String }: a 相减的第一个整数字符串
 * @param { String }: b 相减的第一个整数字符串
 * @return { string }: 返回计算结果
 */
export function allSub(a = "0", b = "0") {
  if(a) a = a.replace(/,/g,'')
  if(b) b = b.replace(/,/g,'')
  const newA = `${a}`;
  const newB = Number(b) > 0 ? `-${b}` : `${b}`.slice(1);
  const result = allAdd(newA, newB);
  return result;
}
/**
 * @Description: 计算两个数之和,返回计算结果
 * @param { String }: a 相加的第一个整数字符串
 * @param { String }: b 相加的第一个整数字符串
 * @return { string }: 返回计算结果
 */
export function allAdd(a = "0", b = "0") {
  if(a) a = a.replace(/,/g,'')
  if(b) b = b.replace(/,/g,'')
  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];
    // 可能存在纯整数 或者纯小数 0.xxxxxxx
    const tagA = intAs > 0 || !intAs[0] === '-' || intAs[0] === '0',
      tagB = intBs > 0 || !intBs[0] === '-' || intBs[0] === '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);

    
     if(!isNaN(Number(result[0]))){
      // 结果首位不是符号
      if(result.length <= arrIntLen){
        const len = arrIntLen - result.length + 1
        result = new Array(len).fill(0).join('') + result
      }
      result = result.slice(0, -arrFloatLen) + "." + result.slice(-arrFloatLen);
    } else {
      if(result.length - 1 <= arrIntLen) {
        const len = arrIntLen - (result.length - 1) + 1
        result = result.slice(0, 1) + new Array(len).fill(0).join('')  + result.slice(1, result.length - 1)
        let Index = 0
        for(let i = 0, j = result.length; i < j; i++){
          if(!isNaN(Number(result.charAt(i)))){
            Index = i
            break
          }
        }
        const newResult = result.slice(Index, result.length - 1)
        const floatLen = arrIntLen - newResult.length + 1
        result = result.slice(0, Index) + newResult.slice(0, -arrFloatLen + floatLen) + '.' + newResult.slice(-arrFloatLen + floatLen)
      } else {
        result = result.slice(0, -arrFloatLen) + "." + result.slice(-arrFloatLen);
      }
    }
    const numResult = Number(result);

    // 去掉正负数前面后面无意义的字符 ‘0’
    let minusFlag = false
   if(result[0] === '-'){
     minusFlag = true
    }
    if (numResult !== 0) {
      if (numResult > 0) {
        while (result[0] === "0") {
          result = result.slice(1);
        }
      } else if (numResult < 0) {
        while (result[1] === "0") {
          result = "-" + 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;
    }
   if(minusFlag) result = '-' + result
    console.log(result);
    return result;
  }
}
function intCalc(a, b) {
  let result = "0";
  const intA = Number(a),
    intB = Number(b);
  // 判断是否为安全数,不为安全数的操作进入复杂计算模式
  if (isSafeNumber(intA) && isSafeNumber(intB) && isSafeNumber(intA + intB)) {
    result = `${intA + intB}`;
  } else {
    const sliceA = a.slice(1),
      sliceB = b.slice(1);
    if (a[0] === "-" && b[0] === "-") {
      // 两个数都为负数,取反后计算,结果再取反
      result = "-" + calc(sliceA, sliceB, true);
    } else if (a[0] === "-") {
      // 第一个数为负数,第二个数为正数的情况
      const newV = compareNumber(sliceA, b);
      if (newV === 1) {
        // 由于 a 的绝对值比 b 大,为了确保返回结果为正数,a的绝对值作为第一个参数
        result = "-" + calc(sliceA, b, false);
      } else if (newV === -1) {
        // 道理同上
        result = calc(b, sliceA, false);
      }
    } else if (b[0] === "-") {
      // 第一个数为正数,第二个数为负数的情况
      const newV = compareNumber(sliceB, a);
      if (newV === 1) {
        // 由于 b 的绝对值比 a 大,为了确保返回结果为正数,b的绝对值作为第一个参数
        result = "-" + calc(sliceB, a, false);
      } else if (newV === -1) {
        // 道理同上
        result = calc(a, sliceB, false);
      }
    } else {
      // 两个数都为正数,直接计算
      result = calc(a, b, true);
    }
  }
  return result;
}
/**
 * @Description: 比较两个整数字符串是否正确
 * @param { string }: 比较的第一个整数字符串
 * @param { string }: 比较的第一个整数字符串
 * @return { object }: 返回是否要退出函数的状态和退出函数返回的数据
 */
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("参数中存在非法的数据,数据类型只支持 number 和 string");
    obj.status = false;
    obj.data = false;
  }
  if (Number.isNaN(a) || Number.isNaN(b)) {
    console.error("参数中不应该存在 NaN");
    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("参数中存在Infinity或-Infinity");
    obj.status = false;
    obj.data = false;
  }
  return obj;
}
/**
 * @Description: 比较两个整数字符串正负
 * @param { string } a 比较的第一个整数字符串
 * @param { string } b 比较的第二个整数字符串
 * @return { boolean } 返回第一个参数与第二个参数的比较
 */
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;
      }
    }
  }
}

/**
 * @Description: 相加的结果
 * @param { string } a 相加的第一个整数字符串
 * @param { string } b 相加的第二个整数字符串
 * @param { string } type 两个参数是 相加(true) 还是相减(false)
 * @return { string } 返回相加的结果
 */
function calc(a, b, type = true) {
  const arr = []; // 保存每个部分计算结果的数组
  for (let i = 0; i < a.length; i += intLen) {
    // 每部分长度 15 的裁取字符串
    const strA = a.slice(i, i + intLen);
    const strB = b.slice(i, i + intLen);
    const newV = Number(strA) + Number(strB) * (type ? 1 : -1); // 每部分的计算结果,暂时不处理
    arr.push(`${newV}`);
  }
  let num = ""; // 连接每个部分的字符串
  for (let i = arr.length - 1; i >= 0; i--) {
    if (arr[i] > 0) {
      // 每部分结果大于 0 的处理方案
      const str = `${arr[i]}`;
      if (str.length < intLen) {
        // 长度不足 15 的首部补充字符‘0’
        num = str.padStart(intLen, "0") + num;
      } else if (str.length > intLen) {
        // 长度超过 15 的扔掉第一位,下一部分进位加一
        num = str.slice(1) + num;
        if (i >= 1 && str[0] !== "0") arr[i - 1]++;
        else num = "1" + num;
      } else {
        // 长度等于 15 的直接计算
        num = str + num;
      }
    } else if (arr[i] < 0) {
      // 每部分结果小于 0 的处理方案,借位 10的15次方计算,结果恒为正数,首部填充字符‘0’到15位
      const newV = `${10 ** intLen + Number(arr[i])}`;
      num = newV.padStart(intLen, "0") + num;
      if (i >= 1) arr[i - 1]--;
    } else {
      // 每部分结果等于 0 的处理方案,连续15个字符‘0’
      num = "0".padStart(intLen, "0") + num;
    }
  }
  return num;
}

结语

感谢JavaScript中任意两个数加减的解决方案作者提供的函数思路,本文仅对此方案中的bug提供一种解决方案,如有错误还请指出