详解JavaScript中任意两数加减的解决方案
写在前面
本文是从初步解决到最终解决的思路,文章篇幅较长
虽然是一篇从0开始的文章,中间的思维跳跃可能比较大
代码的解析都在文章的思路分析和注释里,全文会帮助理解的几个关键词
1.number.max_safe_integer 和 number.min_safe_integer
2.15长度的字符串
3.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
number.max_safe_integer
是什么?
常量表示在 javascript 中最大的安全整数
所以,number.min_safe_integer就是最小安全系数
顾名思义,就是在javascript中加减法在这两个范围内是稳定的,是不是这样就安全了?好像还是有点小问题:
console.log( 10**21 ) // 1e+21 console.log(9999999999999999) // 9999999999999999 console.log(99999999999999999) // 10000000000000000 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.不符合条件1(todo)
const max = number.max_safe_integer; const min = number.min_safe_integer; /** * @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 }: 返回计算结果 */ function intadd(a = "", b = "") { let result = "0"; const inta = number(a), intb = number(b); if (inta === 0) return intb; if (intb === 0) return inta; if ( issafenumber(inta) && issafenumber(intb) && issafenumber(inta + intb) ) { result = inta + intb; } else { result = intcalc(a, b); } return result; } function intcalc(a, b) { // todo } function resclick() { const a = document.getelementbyid("ipt1").value; const b = document.getelementbyid("ipt2").value; const result = intadd(a, b); document.getelementbyid("res").innertext = result; }
如果不满足上面条件的呢?
思路:
获取数字转成字符串拆分成多个部分(数组),每一个部分的长度为 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; /** * @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 }: 返回计算结果 */ function intadd(a = "", b = "") { 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 (numberresult < 0) { while (result[1] === "0") { result = "-" + result.slice(2); } } else { result = "0"; } 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; }
测试结果:
全部代码请点击 这里
console.log(max) // 9007199254740991 intadd(max, '2') // '9007199254740993' intadd(max, '10000000000000000') // '19007199254740991' // 下面测试10的二十一次方的数据 1000000000000000000000 intadd(max, '1000000000000000000000') // '1000009007199254740991' intadd(max, `-${10 ** 16}`) // '-992800745259009' // 仍然存在一个问题,就是不要使用计算中的字符串,如下 intadd(max, `${10 ** 21}`) // '10.0000000071992548e+21' intadd(max, `-${10 ** 21}`) // '0'
转换科学计算
当然考虑到由于一般计算不会使用大数,书写字符串相加确实感觉怪怪的,可以在函数内加入判断,是科学计数法的提示并转换为10进制数,进行代码改进:
/** * @description: 计算两个数之和,返回计算结果 * @param { string }: a 相加的第一个整数字符串 * @param { string }: b 相加的第一个整数字符串 * @return { string }: 返回计算结果 */ function intadd(a = "", b = "") { 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; let stra = `${a}`, strb = `${b}`; const reg = /^\-?(\d+)(\.\d+)?e\+(\d+)$/; if (reg.test(a) || reg.test(b)) { console.warn( "由于存在科学计数法,计算结果不一定准确,请转化成字符串后计算" ); stra = 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"); }); strb = 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; // 即为会用到的整个数组长度 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); // 去掉正负数前面无意义的字符 ‘0' const numberresult = number(result); if (numberresult > 0) { while (result[0] === "0") { result = result.slice(1); } } else if (numberresult < 0) { while (result[1] === "0") { result = "-" + result.slice(2); } } else { result = "0"; } console.log(result); return result; } }
解决整数减法的坑
加法和减法同理,只需要把第二个参数取反后利用加法运算就可以了,由于之前已经提取了模板,可以直接定义减法函数
/** * @description: 整数减法函数入口 * @param { string }: a 减法的第一个整数字符串 * @param { string }: b 减法的第一个整数字符串 * @return { string }: 返回计算结果 */ function intsub(a = "0", b = "0") { const newa = `${a}`; const newb = number(b) > 0 ? `-${b}` : `${b}`.slice(1); const result = intadd(newa, newb); return result; }
测试结果
全部代码请点击 这里
intsub('9037499254750994', '-9007299251310995') // 18044798506061989
解决小数加法的坑
文章开头说了,小数加减,可以通过浮点进行计算,但是这里既然完成了整数的加减,那么能不能利用整数的加减原理来解决小数的加减计算呢?
- 整数加法代码中经常出现
padstart
这个向前补齐的函数,因为在整数前加字符‘0’的对本身没有影响。 - 小数也有这个原理,往尾部补‘0’同样对小数没有影响,然后再补齐后的数通过整数加减来计算。
首先来看下小数的加法计算实现
/** * @description: 小数加法函数入口 * @param { string }: a 相加的第一个整数字符串 * @param { string }: b 相加的第一个整数字符串 * @return { string }: 返回计算结果 */ 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] === "-" ? `-${newa}` : newa; newb = strb[0][0] === "-" ? `-${newb}` : newb; let result = intcalc(newa, newb); let tag = true, numresult = number(result); // 去掉正负数后面无意义的字符 ‘0' 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; } }
测试结果
floatadd('0.9037499254750994', '-0.9007299251310995') // 0.0030200003439999
解决小数减法的坑
与整数减法的原理相同,可以直接定义减法函数
/** * @description: 小数减法函数入口 * @param { string }: a 相减的第一个整数字符串 * @param { string }: b 相减的第一个整数字符串 * @return { string }: 返回计算结果 */ function floatsub(a = '0', b = '0') { const newa = `${a}` const newb = number(b) > 0 ? `-${b}`: `${b.slice(1)}` const result = floatadd(newa, newb) return result }
测试结果
全部代码请点击 这里
floatsub('0.9037499254750994', '-0.9007299251310995') // 1.8044798506061989
解决整数加小数的通用问题
其实在实际开发过程中,并不是整数相加减,小数相加减,都有可能出现,所以还要考虑整数与小数之间的加减计算
这里的解决思路仍然是往前补0和往后补0
把整数和小数都补充完整后,合在一起进行整数相加
最后根据之前保存的整数的长度,插入小数点
剩下的就是把无意义的0排除掉,输出结果
/** * @description: 计算两个数之差,返回计算结果 * @param { string }: a 相减的第一个整数字符串 * @param { string }: b 相减的第一个整数字符串 * @return { string }: 返回计算结果 */ function allsub(a = "0", b = "0") { 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 }: 返回计算结果 */ 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]; // 可能存在纯整数 或者纯小数 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); const numresult = number(result); if (result.length > arrintlen) { result = result.slice(0, -arrfloatlen) + "." + result.slice(arrfloatlen); } // 去掉正负数前面后面无意义的字符 ‘0' 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; } console.log(result); return result; } }
测试结果
全部代码请点击 这里
alladd("9037499254750994", "0.9007299251310995"); // 9037499254750994.9007299251310995 allsub("9037499254750994", "-0.9007299251310995"); // 9037499254750994.9007299251310995 alladd('9037499254750994.9037499254750994', '-9007299251310995.9007299251310995'); // 30200003439999.0030200003439999 allsub('9037499254750994.9037499254750994', '9007299251310995.9007299251310995'); // 30200003439999.0030200003439999
总结
number.max_safe_integer 和 number.min_safe_integer 之间的计算才是可信任的
小数加减的浮点精度问题转移到整数来解决
超大的数加减的时候,分区计算(理由是第1点)
拆分成每部分15长度的字符串(理由是number.max_safe_integer的长度为16,无论如何加减都是满足第一点的,这样就不需要去注意加减的安全性问题了)
科学计数法的问题,匹配是否为科学计数法的数,然后转换成十进制,同时提出警告,因为科学计数法的数存在误差,计算会存在不准确性
以上就是详解javascript中任意两数加减的解决方案的详细内容,更多关于javascript数字加减的资料请关注其它相关文章!
推荐阅读
-
详解JavaScript中任意两数加减的解决方案
-
JavaScript 中的无穷数(Infinity)详解
-
JavaScript 中的无穷数(Infinity)详解
-
图文详解JavaScript中数组转换为字符串的两种方法
-
详解Javascript中Array和Object两者之间的关系
-
图文详解JavaScript中数组转换为字符串的两种方法
-
详解javascript中动态合并两个对象的属性
-
[JavaScript]两数之和---给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的两个整数。
-
详解JavaScript中任意两数加减的解决方案
-
详解javascript中动态合并两个对象的属性