天天看點

js解決精度丢失的問題

作者:程式設計經驗分享

精度丢失問題想必大家都遇到過,你們又是如何解決的呢?

精度丢失的原因

由于浮點數的表示方式,某些數值無法精确表示。例如,0.1 在二進制中是一個無限循環小數,是以在計算機中表示為一個近似值。

當數字超過JavaScript中的安全整數範圍(數值的精度隻能到 53 個二進制位)時,計算結果可能不準确。例如:

console.log(9007199254740992 + 1);           

還有其他一些原因,導緻js無法精确的計算浮點數。

為了解決此問題,在網上搜尋了一番,大緻的計算方式有如下幾種:

使用整數進行計算

将浮點數轉換為整數,然後進行計算,最後再将結果轉換回浮點數。例如,将兩個浮點數相乘時,可以将它們分别乘以一個相同的倍數(例如10的幂),使其變為整數,然後進行整數相乘,最後再除以相應的倍數。

具體代碼如下:

function multiply(a, b) {
  const multiplier = 10000; // 選擇一個合适的倍數,如10000
  const intA = a * multiplier;
  const intB = b * multiplier;
  return (intA * intB) / (multiplier * multiplier);
}           

自己以前也用過類似的方法進行封裝,但是發現,很多時候還是會導緻精度丢失,這種方法不建議大家使用

使用第三方庫

有一些第三方庫專門用于處理浮點數計算精度問題,如 decimal.js、bignumber.js 等。這些庫提供了一系列方法來進行高精度的浮點數計算。

第三方封裝的還不錯,不會有精度丢失的問題。具體代碼如下:

import Decimal from 'decimal.js';

const a = new Decimal(0.1);
const b = new Decimal(0.2);
const result = a.plus(b).toNumber();           

安裝decimal.js:npm install --save decimal.js

官方文檔:http://mikemcl.github.io/decimal.js/

自己封裝的函數

自己最開始隻是簡單将倆個數相乘程式設計整數再進行操作,但是bug依然很多,計算精度有問題。

具體代碼如下:

new Vue({
    el:'#app',
    data(){
        return {
            number_a:'',
            number_b:'',
            result:'',
            result1:''
        }
    },
    methods:{
        decimalNum(){
            this.result1 = (parseInt(this.number_a*1000)+parseInt(this.number_b*1000))/1000
        }
    }
});           

假設 number_a=1.23154 ,number_b=3.2156 計算的結果:4.446,但是實際應該是:4.447

因為第一步浮點數轉整數時已經舍去最後面的兩位,導緻最終的結算結果有誤。根據這個原因,修改第一步功能并重新封裝成如下:

new Vue({
    el:'#app',
    data(){
        return {
            number_a:'',
            number_b:'',
            result:'',
            result1:''
        }
    },
    methods:{
        //解析浮點數資訊
        parseNum(num){
            num = num+'';
            let numArr = num.split('.')
            if(!numArr[1]) numArr[1] = '';
            return numArr;
        },
        getScaleNum(scale){
            let scaleNum = '1';
            for (let i=0;i<scale;i++){
                scaleNum += '0';
            }
            return scaleNum*1;
        },
        //兩個任意精度的數字相加,num1,num2:為源數字   scale:為保留小數點後多少位
        bcAdd(num1,num2,scale){
            num1 = this.parseNum(num1);
            num2 = this.parseNum(num2);
            let maxLength = num1[1].length > num2[1].length ? num1[1].length : num2[1].length;
            let str1 = num1[0];
            let str2 = num2[0];
            let floatNum = '1';
            for (let i=0;i<maxLength;i++){
                str1 += (num1[1][i] ? num1[1][i] : '0');
                str2 += (num2[1][i] ? num2[1][i] : '0');
                floatNum += '0';
            }
            floatNum = floatNum*1;
            let num = str1 * 1 + str2 * 1;
            let scaleNum = this.getScaleNum(scale);

            return parseInt( (num / floatNum)*scaleNum ) / scaleNum;
        },
        decimalNum(){
            this.result = this.bcAdd(this.number_a,this.number_b,3);
        }
    }
});           

這種方式基本就解決了因截取導緻計算精度丢失的問題,減法及乘法也是如此:

//兩個任意精度的數字相減
bcsub(num1,num2,scale){
    num1 = this.parseNum(num1);
    num2 = this.parseNum(num2);
    let maxLength = num1[1].length>num2[1].length?num1[1].length : num2[1].length;
    let str1 = num1[0];
    let str2 = num2[0];
    let floatNum = '1';
    for (let i=0;i<maxLength;i++){
        str1 += (num1[1][i] ? num1[1][i] : '0');
        str2 += (num2[1][i] ? num2[1][i] : '0');
        floatNum += '0';
    }
    floatNum = floatNum*1;

    let num = str1 * 1 - str2 * 1;

    let scaleNum = this.getScaleNum(scale);

    return parseInt( (num / floatNum)*scaleNum ) / scaleNum;
},
//兩個任意精度的數字相乘
bcmul(num1,num2,scale){
    num1 = this.parseNum(num1);
    num2 = this.parseNum(num2);
    let maxLength = num1[1].length>num2[1].length?num1[1].length : num2[1].length;
    let str1 = num1[0];
    let str2 = num2[0];
    let floatNum = '1';
    for (let i=0;i<maxLength;i++){
        str1 += (num1[1][i] ? num1[1][i] : '0');
        str2 += (num2[1][i] ? num2[1][i] : '0');
        floatNum += '0';
    }
    floatNum = floatNum*1;

    let num = str1 * str2;

    let scaleNum = this.getScaleNum(scale);

    return parseInt( (num / floatNum/floatNum)*scaleNum ) / scaleNum;
},           

但是在計算除法時,此算法就不能使用了,由于除法有可能無法除盡,是以在使用除法時隻能使用源資料相除後再保留想要的位數,具體代碼如下:

//兩個任意精度的數字相除
bcdiv(num1,num2,scale){
    let scaleNum = this.getScaleNum(scale);
    return parseInt((num1/num2)*scaleNum)/scaleNum;
}           

常用的幾種浮點數操作就這麼多了。

希望這篇文章可以幫到正在閱讀的你,如果覺得此文對你有幫助,可以分享給你身邊的朋友,同僚,你關心誰就分享給誰,一起學習共同進步~~~

繼續閱讀