Skip to content

Instantly share code, notes, and snippets.

@Jancat
Last active May 20, 2018 08:03
Show Gist options
  • Save Jancat/eaee5b0b95b96a1a5c0dbfc5b981bad2 to your computer and use it in GitHub Desktop.
Save Jancat/eaee5b0b95b96a1a5c0dbfc5b981bad2 to your computer and use it in GitHub Desktop.
JavaScript 大数运算
/*
* 大整数乘法
* 功能:大整数乘法精确运算,同时解决浮点数误差问题
* 问题:超过 Number.MAX_SAGE_INTEGER 的数不能准确表示,同时运算结果也不准确
* 原理:通过字符串表示大数,并且从字符串末尾开始把每个字符转成数值进行运算,然后再处理进位和借位,最后结果也是用字符串表示
* 说明:
* 支持正负整数;
* 不支持科学计数法表示的字符串,如 1.2e+10;
* 进行运算的字符串假设数值格式尽量标准
*/
// 大整数相乘
function largeNumMulti(num1, num2) {
// 从末尾开始计算乘积
const num1Arr = num1.split('').reverse()
const num2Arr = num2.split('').reverse()
const result = []
// 填充0,为后面的累加作准备
result.length = num1Arr.length + num2Arr.length - 1
result.fill(0)
// result[i + j] 上不断累加对应位的乘积
num1Arr.forEach((a, i) => {
num2Arr.forEach((b, j) => {
result[i + j] += parseInt(a) * parseInt(b)
})
})
// 处理 result 进位
const lastCarry = result.reduce((carry, v, k) => {
result[k] += carry
if (result[k] >= 10) {
const _carry = Math.floor(result[k] / 10)
result[k] %= 10
return _carry
}
return 0
}, 0)
lastCarry && result.push(lastCarry)
return result.reverse().join('')
}
// 处理异常数值和符号
function largeNumCalculation(num1, num2, ...others) {
if (others.length > 0) {
return largeNumCalculation(largeNumCalculation(num1, num2), others[0], ...others.slice(1))
}
if (
Number.isNaN(parseFloat(num1)) ||
Number.isNaN(parseFloat(num2)) ||
!/^[0-9-]*$/.test(num1) ||
!/^[0-9-]*$/.test(num2)
) {
throw new Error('Not a valid number')
}
const num1Trimmed = num1.trim().replace(/^0+/g, '')
const num2Trimmed = num2.trim().replace(/^0+/g, '')
const isNum1Positive = !num1[0].startsWith('-')
const isNum2Positive = !num2[0].startsWith('-')
// 处理符号
if (num1Trimmed && num2Trimmed) {
if (isNum1Positive && isNum2Positive) {
return largeNumMulti(num1Trimmed, num2Trimmed)
} else if (!isNum1Positive && !isNum2Positive) {
return largeNumMulti(num1Trimmed.slice(1), num2Trimmed.slice(1))
} else {
return (
'-' +
largeNumMulti(
isNum1Positive ? num1Trimmed : num1Trimmed.slice(1),
isNum2Positive ? num2Trimmed : num2Trimmed.slice(1),
)
)
}
}
return num1Trimmed + num2Trimmed
}
const a = '-3455'
const b = '-2200'
console.log(+a * +b)
console.log(largeNumCalculation(a, b))
/*
* 大数相加、相减
* 功能:大数加减精确运算,同时解决浮点数误差问题
* 问题:超过 Number.MAX_SAGE_INTEGER 的数不能准确表示,同时运算结果也不准确
* 原理:通过字符串表示大数,并且从字符串末尾开始把每个字符转成数值进行运算,然后再处理进位和借位,最后结果也是用字符串表示
* 说明:
* 支持正负数、浮点数、整数;
* 不支持科学计数法表示的字符串,如 1.2e+10;
* 进行运算的字符串假设数值格式尽量标准
*/
// 删除字符串中多余的空白字符和 0
function trimZero(str) {
const newStr = str.trim()
return newStr.replace(
/(?:(-)(?![0.]+(?![\d.]))|-)?\d*?([1-9]\d*|0)(?:(?:(\.\d*[1-9])|\.)\d*)?(?![\d.])/gm,
'$1$2$3',
)
}
// 分割整数部分和小数部分
function divide(num) {
const numPointIndex = num.indexOf('.')
const numInteger = num.slice(0, numPointIndex > -1 ? numPointIndex : undefined)
const numFraction = numPointIndex > -1 ? num.slice(numPointIndex + 1) : ''
return [numInteger, numFraction]
}
// 两数对齐,用 '0' 填充,并且移除小数点
// 返回对齐后的数和小数点位置
function alignNumber(num1, num2) {
function align(_num1, _num2, padFn) {
const maxLength = Math.max(_num1.length, _num2.length)
return [padFn.call(_num1, maxLength, '0'), padFn.call(_num2, maxLength, '0')]
}
// 分割整数和小数
let [num1Integer, num1Fraction] = divide(num1)
let [num2Integer, num2Fraction] = divide(num2)
// 整数部分和小数部分对齐
;[num1Integer, num2Integer] = align(num1Integer, num2Integer, String.prototype.padStart)
;[num1Fraction, num2Fraction] = align(num1Fraction, num2Fraction, String.prototype.padEnd)
return [num1Integer + num1Fraction, num2Integer + num2Fraction, num1Integer.length]
}
// 大数相加
// numTrimmed 一定是正数
function largeNumAddition(num1Trimmed, num2Trimmed) {
const [alignedNum1, alignedNum2, pointIndex] = alignNumber(num1Trimmed, num2Trimmed)
const result = []
let borrow = 0
// 倒序一个个字符相加,把相加结果保存在 result[] 中
for (let i = alignedNum1.length - 1; i >= 0; i--) {
const a = parseInt(alignedNum1[i])
const b = parseInt(alignedNum2[i])
if (a + b + borrow >= 10) {
result.unshift(a + b + borrow - 10)
borrow = 1
} else {
result.unshift(a + b + borrow)
borrow = 0
}
}
// 如果是小数则把小数点插入回原来的位置
if (pointIndex < alignedNum1.length) {
result.splice(pointIndex, 0, '.')
}
if (borrow === 1) {
result.unshift(1)
}
return trimZero(result.join(''))
}
// 大数相减
// numTrimmed 一定是正数
function largeNumSubtraction(num1Trimmed, num2Trimmed) {
// 如果 num1Trimmed 的整数部分长度小于 num2Trimmed 的整数部分长度,说明 num1Trimmed 小于 num2Trimmed
// 减数交换并对结果转负
if (divide(num1Trimmed)[0].length < divide(num2Trimmed)[0].length) {
return '-' + largeNumSubtraction(num2Trimmed, num1Trimmed)
}
const [alignedNum1, alignedNum2, pointIndex] = alignNumber(num1Trimmed, num2Trimmed)
const result = []
let borrow = 0
// 倒序一个个字符相加,把相加结果保存在 result[] 中
for (let i = alignedNum1.length - 1; i >= 0; i--) {
const a = parseInt(alignedNum1[i])
const b = parseInt(alignedNum2[i])
if (a - b - borrow < 0) {
result.unshift(a - b - borrow + 10)
borrow = 1
} else {
result.unshift(a - b - borrow)
borrow = 0
}
}
if (pointIndex < alignedNum1.length) {
result.splice(pointIndex, 0, '.')
}
// 和函数最开始的判断逻辑一致,这里根据最后的借位判断是否要交换减数
if (borrow) {
return '-' + largeNumSubtraction(num2Trimmed, num1Trimmed)
}
return trimZero(result.join(''))
}
function largeNumCalculation(num1, num2, ...others) {
if (others.length > 0) {
return largeNumCalculation(largeNumCalculation(num1, num2), others[0], ...others.slice(1))
}
if (
Number.isNaN(parseFloat(num1)) ||
Number.isNaN(parseFloat(num2)) ||
!/^[0-9.-]*$/.test(num1) ||
!/^[0-9.-]*$/.test(num2)
) {
throw new Error('Not a valid number')
}
const num1Trimmed = trimZero(num1)
const num2Trimmed = trimZero(num2)
// 根据正负数判断选择相加还是相减
const isNum1Positive = !num1[0].startsWith('-')
const isNum2Positive = !num2[0].startsWith('-')
// 处理符号
if (num1Trimmed && num2Trimmed) {
if (isNum1Positive && isNum2Positive) {
return largeNumAddition(num1Trimmed, num2Trimmed)
} else if (isNum1Positive && !isNum2Positive) {
return largeNumSubtraction(num1Trimmed, num2Trimmed.slice(1))
} else if (isNum2Positive && !isNum1Positive) {
return largeNumSubtraction(num2Trimmed, num1Trimmed.slice(1))
} else {
return '-' + largeNumAddition(num1Trimmed.slice(1), num2Trimmed.slice(1))
}
}
return num1Trimmed + num2Trimmed
}
const a = '-8888888888888.888888888888888'
const b = '000099999999.0000944444444444444444444440'
console.log(parseFloat(a) + parseFloat(b))
console.log(largeNumCalculation(a, b))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment