写在前面的话
注:本文所讲述内容全部基于这篇文章JavaScript中任意两个数加减的解决方案,经过实践,发现文中所给出的js方法有部分bug,本文的立意就是针对这篇文章作补充说明,所以在阅读本文前建议先阅读这篇文章,这篇文章中所讲述过的内容在本文中将不会重复说明。
补充文章
- js浮点数相加问题: 为什么0.1+0.2不等于0.3?原来编程语言是这么算的……
- 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);之后,根据a和b的整数部分位数和小数部分位数,进而对result的结果进行处理:确定小数点的位置。
现以allSub('2.2', '1.6')的执行顺序来说明计算错误的原因
通过打断点的方式可以看出,2.2 - 1.6实际上是000000000000002200000000000000 + -0000000000001600000000000000,计算结果是600000000000000,显而易见,结果少了位数,实际结果应为0600000000000000(当计算结果位数比原有数值位数少时,以0补齐)这样做的目的是为了后续确定小数点位置时,保证小数点的位数正确
进而我们确定了计算结果补0位的逻辑为
对应的,将原函数中的此处代码
替换为
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比较简单,就是单纯的负号问题,直接给出解决方案
将
改为
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提供一种解决方案,如有错误还请指出