js 字符串加减法_基于字符串的数值之加减乘除JS算法研究

在我们的日常js项目中,我们不免会碰到需要进行前端计算的场景。而大家都知道,计算机进行计算时存在精度问题,且数值有值域,偶尔会碰到溢出问题。在最近的一个项目中,由于遇到了一个超过20位的数,因此,又不得不再来对js中数值的运算进行研究。

在js中,一旦遇到非常大的数,例如一个超过Number.MAX_SAFE_VALUE的值,在运算时就会出现问题,而且在展示时(toString)会出现把大数指数化后展示(12.3221124e18),最后导致展示问题。针对这些问题,我们目前已知ES标准已经将BigInt类型作为下一阶段标准的一个方案,基于BigInt,未来实现BigFloat也是可行的。但对于开发者而言,在不可知的未来,BigInt也可能遇到问题,例如,不可以和普通Number进行直接的混合运算。因此,在这些年的经验中,前端总结出来一套方案,就是以字符串的形式进行数值运算。

解决数值溢出问题

解决小数精度问题

字符串数值运算听上去很简单,但是,实际上在实现过程中,会遇到不少问题,一旦你开始去写代码,就会遇到这些坑。由于我自己实现了一套加减乘除算法,所以,现在把这个过程写下来,以为后来的同学借鉴。

竖式

在实现字符串数值的运算时,我们不能按照计算机语言的思维方式去做,我们要回归到数学的本质,什么是加?什么是减?什么是乘?什么是除?数学的奇妙,让我们即使笨拙,也可以找到门路。我所发现的实现方式,就是我们小学的时候学到的“竖式”。它是一种能让小学生理解的运算方式。让我们来看一个最简单的竖式:

132

+ 79

------

`1

`1

2

------

= 211

这是一个加法竖式的例子。而且两个竖式可以串联,例如,上面的竖式结束之后,你需要继续计算211+64,还可以继续在下面进行下一轮运算。

基于竖立的理解,我实现了一套js的演算。其中加法和减法是完完全全按照竖式实现的,而乘法和除法则是在加法和减法基础上的继续叠加实现的。下面,我就会对每一个实现进行详细的讲解。

数的拆解

在真正开始进行算法讲解之前,我需要做一件事,就是讲清楚,我在每一个算法中,如何对待一个数值。因为我们用于运算的所有数值在js中的本质是一个字符串,因此,我们需要对数进行拆解,我们需要对算法进行叠加,对数的每一部分进行分析,然后综合得到最后的结果。

一个数,在表达中分为三个部分:整数部分,小数部分,正负。

shudechaijie.png

对于字符串而言,这三部分都非常容易得到,不需要太多的处理和识别。

在算法中,我基本上遵循这样的规律:

整数部分和小数部分分开运算,最后叠加起来

创建一个完全正整数的元运算算法

正负号影响处理方式,但不对运算本身造成影响,因此,一般在运算开始之前考虑正负号带来的影响

某些特殊值具有快速响应能力,例如一个数乘以0,永远得到0,诸如此类的运算,都可以不用走复杂的运算流程,以此增加性能

对于字符串处理结果而言,我们还需要注意一个事实,就是,字符串不会自动处理数值前后的00,例如我们用两个数相减,得到-007.1200,我们应该在代码中自动把开头的00和末尾的00都去掉,以一个正确正常的值返回运算结果。

加法运算

两个数相加的竖式已经在上面的例子中提到了。我们现在来再研究一下。两个数相加,无论多少位,它的规则都是从末位开始,依次按位相加,最后将相加结果串联起来。但是,在这个过程中需要注意的是,当两个单位相加结果大于10时,需要向前进一位,也就是在下一位相加时,需要再加1.这个进位的规则使得整个运算难了很多。但是,我们还是可以通过各种手段,判断出在计算时是否应该进位。

加法元

所谓元运算,在本文中是指,对两个正整数进行的某项运算算法。加法的元运算相对比较简单,主要考虑的是进位规则:

// 创建一个整数相加函数

const plus = (x, y) => {

let xr = x.split('').reverse()

let yr = y.split('').reverse()

let len = Math.max(xr.length, yr.length)

let items = []

for (let i = 0; i < len; i ++) {

let xv = xr[i] || '0'

let yv = yr[i] || '0'

items[i] = ((+xv) + (+yv)) + ''

}

let sum = items.reduce((sum, item, index) => {

let sumlen = sum.length

// 如果之前相加进了一位

if (sumlen > index) {

let borrow = sum.substring(0, 1)

let placed = sum.substring(1)

let next = (+borrow + +item) + ''

return next + placed

}

else {

return item + sum

}

}, '')

return sum

}

上边这个函数解决的是两个正整数相加的元运算。无论这两个正整数位数有多大,都能正常计算结果。

加法变形

本文中所谓运算变形,是指一些场景下,在元运算基础上进行的叠加处理。加法的运算变形包括:

a, b任何一个值为0,则返回另外一个值,无需进行深入运算

如果a为正数,b为负数,相当于a-b,直接调用minusby(a, b.substring(1))

如果a为负数,b为正数,则反过来调用minusby(b, a.substring(1))

小数相加时,先补齐小数位数,再去掉小数点,元运算之后,再把小数点加回来

a, b都是负数,相当于plusby(a.substring(1), b.substring(1))后,结果再取负

以上就是加法的运算变形。

number-compute-2.png

基于这些变形,我们得到最终的加法运算如下:

/**

* 基于字符串数值的a+b

* @param {string} a

* @param {string} b

*/

export function plusby(a, b) {

a = numerify(a)

b = numerify(b)

if (a === '0') {

return b

}

else if (b === '0') {

return a

}

var [ ia, da = '0' ] = a.split('.')

var [ ib, db = '0' ] = b.split('.')

// 是否为负数

var na = false

var nb = false

if (ia.indexOf('-') === 0) {

ia = ia.substring(1)

na = true

}

if (ib.indexOf('-') === 0) {

ib = ib.substring(1)

nb = true

}

// 一正一负相当于相减

if (na && !nb) {

return minusby(b, a.substring(1))

}

if (nb && !na) {

return minusby(a, b.substring(1))

}

// 创建一个整数相加函数

const plus = (x, y) => {

let xr = x.split('').reverse()

let yr = y.split('').reverse()

let len = Math.max(xr.length, yr.length)

let items = []

for (let i = 0; i < len; i ++) {

let xv = xr[i] || '0'

let yv = yr[i] || '0'

items[i] = ((+xv) + (+yv)) + ''

}

let sum = items.reduce((sum, item, index) => {

let sumlen = sum.length

// 如果之前相加进了一位

if (sumlen > index) {

let borrow = sum.substring(0, 1)

let placed = sum.substring(1)

let next = (+borrow + +item) + ''

return next + placed

}

else {

return item + sum

}

}, '')

return sum

}

// 补齐位数用以相加

const dalen = da.length

const dblen = db.length

const dlen = Math.max(dalen, dblen)

if (dalen < dlen) {

da = padRight(da, dlen, '0')

}

if (dblen < dlen) {

db = padRight(db, dlen, '0')

}

const ta = ia + da

const tb = ib + db

var sum = plus(ta, tb)

// 还原小数位数

var sumr = sum.split('')

var sumlen = sumr.length

var index = sumlen - dlen

sumr.splice(index, 0, '.')

sum = sumr.join('')

sum = clearNumberZero(sum)

sum = sum === '' ? '0' : sum

// 都是负数

if (sum !== '0' && na && nb) {

sum = '-' + sum

}

return sum

}

上面代码中有几个函数,他们的源码如下:

/**

* 对字符串向右补位

* @param {*} str

* @param {*} len

* @param {*} pad

*/

export function padRight(str, len, pad) {

if (str.length >= len) {

return str

}

let diff = len - str.length

let letters = createArray(pad, diff)

return str + letters.join('')

}

/**

* 移除数字首尾的00

* @param {*} input

*/

export function clearNumberZero(input) {

input = input.toString()

var [ integerPart, decimalPart = '' ] = input.split('.')

var isNegative = false

if (integerPart.indexOf('-') === 0) {

isNegative = true

integerPart = integerPart.substring(1)

}

integerPart = integerPart.replace(/^0+/, '') // 去除开头的000

decimalPart = decimalPart.replace(/0+$/, '') // 去除末尾的000

var value = (isNegative && (integerPart || decimalPart) ? '-' : '') + (integerPart ? integerPart : '0') + (decimalPart ? '.' + decimalPart : '')

return value

}

/**

* 创建一个特定位数,且每个元素值相同的数组

* @param {*} count

* @param {*} value

*/

export function createArray(value, count = 1) {

return [].fill.call(new Array(count), value);

}

/**

* 获取数字的字符串形式

* @param {*} num

*/

export function numerify(num) {

if (isString(num)) {

if (!isNumeric(num)) {

return ''

}

let value = clearNumberZero(num)

return value

}

else if (isNumber(num)) {

let value = num.toString()

// num.toString()之后得到的指数有两种,一种是精确的小值指数形式,另一种是非精确的大值指数形式

// 但无论哪一种,这里都是最终的办法,非精确的情况下,无法做到数值转化

if (value.indexOf('e')) {

return convertNumberWithExponential(value)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值