圖文剖析 big.js 四則運算源碼

来源:https://www.cnblogs.com/zsxblog/archive/2023/11/29/17864751.html
-Advertisement-
Play Games

big.js,一個小型、快速的用於任意精度的十進位算術的JavaScript 庫。 big.js 用於解決平常項目中進行算術運算時精度丟失引起的結果不准確的問題。和 big.js 類似的兩個庫 bignumber.js 和 decimal.js 也都是出自同一作者(MikeMcl)之手。 作者在 這 ...


big.js,一個小型、快速的用於任意精度的十進位算術的JavaScript 庫。

big.js 用於解決平常項目中進行算術運算時精度丟失引起的結果不准確的問題。和 big.js 類似的兩個庫 bignumber.js 和 decimal.js 也都是出自同一作者(MikeMcl)之手。

作者在 這裡 詳細說明瞭他們之間的區別

big.js 是最小的任意精度的計算庫。big.js 是三者中最小也最簡單的,它只有 bignumber.js 一半的方法,不到 bignumber.js 的一半大。

bignumber.js 和 decimal.js 存儲值的進位比 big.js 更高,因此當操作大量數字時,前兩者的速度會更快。

bignumber.js 可能更適合金融類應用,因為用戶不用擔心丟失精度,除非使用了涉及除法的操作。

這篇文章分別就 big.js 的解析函數,以及加減乘除運算的源碼進行剖析,瞭解作者的設計思路。在四則運算的源碼中,相比加減乘,除法運算最為複雜。

用法

創建 Big 對象時,new 操作符是可選的

x = new Big(123.4567)
y = Big('123456.7e-3')                 // 'new' is optional
z = new Big(x)
x.eq(y) && x.eq(z) && y.eq(z)          // true

構造函數

構造函數中關鍵代碼如下

function Big(n) {
  var x = this;

  // 使用構造函數前面可以不帶 new 關鍵字
  if (!(x instanceof Big)) return n === UNDEFINED ? _Big_() : new Big(n);

  // 如果傳進來的參數已經是 Big 的實例對象,則複製一份,否則使用 parse 函數創建一個實例對象
  if (n instanceof Big) {
    x.s = n.s;
    x.e = n.e;
    x.c = n.c.slice();
  } else {
    if (typeof n !== 'string') {
      if (Big.strict === true && typeof n !== 'bigint') {
        throw TypeError(INVALID + 'value');
      }

      // 傳入的如果是 -0 ,則轉為字元串表示 '-0'
      n = n === 0 && 1 / n < 0 ? '-0' : String(n);
    }

    parse(x, n);
  }

使用構造函數前面可以不帶 new 關鍵字

如果傳進來的參數已經是 Big 的實例對象,則將實例對象的屬性複製一份,否則使用 parse 函數為實例對象創建屬性。

parse 函數

function parse(x, n) {
    var e, i, nl;

    // NUMERIC = /^-?(\d+(\.\d*)?|\.\d+)(e[+-]?\d+)?$/i;
    if (!NUMERIC.test(n)) {
      throw Error(INVALID + 'number');
    }

    // Determine sign.
    x.s = n.charAt(0) == '-' ? (n = n.slice(1), -1) : 1;

    // Decimal point?
    if ((e = n.indexOf('.')) > -1) n = n.replace('.', '');

    // Exponential form?
    if ((i = n.search(/e/i)) > 0) {

      // Determine exponent.
      if (e < 0) e = i;
      e += +n.slice(i + 1);
      n = n.substring(0, i);
    } else if (e < 0) {

      // Integer.
      e = n.length;
    }

    nl = n.length;

    // Determine leading zeros.
    for (i = 0; i < nl && n.charAt(i) == '0';) ++i;

    if (i == nl) {

      // Zero.
      x.c = [x.e = 0];
    } else {

      // Determine trailing zeros.
      for (; nl > 0 && n.charAt(--nl) == '0';);
      x.e = e - i - 1;
      x.c = [];

      // Convert string to array of digits without leading/trailing zeros.
      for (e = 0; i <= nl;) x.c[e++] = +n.charAt(i++);
    }

    return x;
  }

parse 函數會為實例對象添加三個屬性;

  • x.s,表示數字的符號,即是正數還是負數,即正負值,若是正數,x.s = 1,負數則為 -1
  • x.e,表示數字對應的指數表示法的指數,比如 n = 1234 的指數為 3
  • x.c,數字數組,比如 1234 轉換後是 [1,2,3,4]
1234 會被轉化為 

{
    c:[1,2,3,4],
    e:3,
    s:1
}

這種表示,和 IEEE 754 雙精度浮點數的存儲方式 很類似,而 JavaScript 的 Number類型就是一個雙精度 64 位二進位格式 IEEE 754 值使用 64 位來表示 3 個部分:

  • 1 位用於表示符號(sign) (正數或者負數)
  • 11 位用於表示指數(exponent) (-1022 到 1023)
  • 52 位用於表示尾數(mantissa) (表示 0 和 1 之間的數值)

big.sj.png

下麵分析 parse 函數轉化的詳細過程,以 Big('123400')Big('0.1234')Big('100e2') 為例

註意:Big('100e2') 中 100e2 以字元串形式傳進來才能檢測到 e ,Number形式的 Big(100e2),執行 parse 前會被轉化為 Big(10000)

  1. 校驗傳入的值,只允許數字,'.1',指數形式的寫法。比如 2.34.2 ,10e2
// NUMERIC = /^-?(\d+(\.\d*)?|\.\d+)(e[+-]?\d+)?$/i;
if (!NUMERIC.test(n)) {
  throw Error(INVALID + 'number');
}

Big('123400'),Big('-0.1234'),Big('100e2') 都通過
  1. x.s = n.charAt(0) == '-' ? (n = n.slice(1), -1) : 1; 確定符號
Big('123400') => x.s = 1
Big('-0.1234') => x.s = -1   並且 -0.1234 => 0.1234
Big('100e2') => x.s = 1
  1. if ((e = n.indexOf('.')) > -1) n = n.replace('.', ''); 是否含有小數點,如果是,則刪除小數點,並將 e 的初始值設為小數點的位置
Big('123400') => x.e = -1 , n = 123400
Big('-0.1234') => x.e = 1 , n = 01234
Big('100e2') => x.e = -1 , n = 100e2
  1. 如果數字是科學表示法,比如 100e2 ,e 的位置是 3,e 後面的指數是 2 ,則 x.e = 3 + 2
if ((i = n.search(/e/i)) > 0) {

  // Determine exponent.
  if (e < 0) e = i;
  e += +n.slice(i + 1);
  n = n.substring(0, i);
} else if (e < 0) {

  // Integer.
  e = n.length;
}


Big('123400') 
x.e = -1  =>  x.e = n.length = 6
n = 123400

Big('-0.1234')
x.e = 1 
n = 01234

Big('100e2')
x.e = -1  =>  x.e = e 在 100e2 中的位置 + e 後面緊跟的指數繫數 = 3 + 2 = 5
n = 100e2  =>  n = 100
  1. nl = n.length; nl 表示傳進來的數字的長度
Big('123400') 
x.e = 6
n = 123400
nl = 6

Big('-0.1234')
x.e = 1 
n = 01234
nl = 5

Big('100e2')
x.e = 5
n = 100
nl = 3
  1. for (i = 0; i < nl && n.charAt(i) == '0';) ++i; 確定數字是否有前置 0 ,這裡的 i 表示第一個不為 0 的數字的位置,也可以表示數字前面有多少個 0
Big('123400') 
x.e = 6
n = 123400
nl = 6
i = 0

Big('-0.1234')
x.e = 1 
n = 01234
nl = 5
i = 1

Big('100e2')
x.e = 5
n = 100
nl = 3
i = 0
  1. 如果 i = nl,則說明傳進來的輸入是一個 0 或者多個 0
if (i == nl) {

  // Zero.
  x.c = [x.e = 0];
} else {

  // 排除尾隨 0,nl 為最後一個不為 0 的數字的位置
  for (; nl > 0 && n.charAt(--nl) == '0';);
  x.e = e - i - 1;
  x.c = [];

  // 傳進來的數字,排除掉前置 0 和尾隨 0 後,轉換為數字數組
  for (e = 0; i <= nl;) x.c[e++] = +n.charAt(i++);
}


Big('123400') 
//因為預設 e 是 n.length,而Big指數表示是 1.234 * 10^5,所以這裡 x.e 要減一
x.e = 6  =>  x.e = e - i - 1 = 6 - 0 - 1 = 5  
n = 123400
nl = 6  =>  排除尾隨 0,nl 為最後一個不為 0 的數字的位置  =>  nl = 3
i = 0
x.c => 選取 n 中從 i 到 nl 的數字組成數組 [1,2,3,4]

Big('-0.1234')
x.e = 1  =>  x.e = e - i - 1 = 1 - 1 - 1 = -1 
n = 01234
nl = 5  =>  排除尾隨 0,nl 為最後一個不為 0 的數字的位置  =>  nl = 4
i = 1
x.c => 選取 n 中從 i 到 nl 的數字組成數組 [1,2,3,4]

Big('100e2')
x.e = 5  =>  x.e = e - i - 1 = 5 - 0 - 1 = 4  
n = 100
nl = 3  =>  排除尾隨 0,nl 為最後一個不為 0 的數字的位置  =>  nl = 0
i = 0
x.c => 選取 n 中從 i 到 nl 的數字組成數組 [1]

最後 Big('123400'),Big('-0.1234'),Big('100e2') 將轉換為

Big('123400') 
x.s = 1
x.e = 5
x.c = [1,2,3,4]

Big('-0.1234') 
x.s = -1
x.e = -1
x.c = [1,2,3,4]

Big('100e2') 
x.s = 1
x.e = 4
x.c = [1]

至此 parse 函數邏輯結束,接下來分別剖析下加減乘除運算;

加法

源碼

P.plus = P.add = function (y) {
    var e, k, t,
      x = this,
      Big = x.constructor;

    y = new Big(y);

    // 校驗符號是否不同
    if (x.s != y.s) {
      y.s = -y.s;
      return x.minus(y);
    }

    var xe = x.e,
      xc = x.c,
      ye = y.e,
      yc = y.c;

    // 校驗是否是 0
    if (!xc[0] || !yc[0]) {
      if (!yc[0]) {
        if (xc[0]) {
          y = new Big(x);
        } else {
          y.s = x.s;
        }
      }
      return y;
    }

    xc = xc.slice();

    // 前面加上零使指數均衡
    // Note: reverse faster than unshifts.
    if (e = xe - ye) {
      if (e > 0) {
        ye = xe;
        t = yc;
      } else {
        e = -e;
        t = xc;
      }

      t.reverse();
      for (; e--;) t.push(0);
      t.reverse();
    }

    // 讓 xc 存放長度更長的數字
    if (xc.length - yc.length < 0) {
      t = yc;
      yc = xc;
      xc = t;
    }

    e = yc.length;

    for (k = 0; e; xc[e] %= 10) k = (xc[--e] = xc[e] + yc[e] + k) / 10 | 0;

    // No need to check for zero, as +x + +y != 0 && -x + -y != 0

    if (k) {
      xc.unshift(k);
      ++ye;
    }

    // 刪除尾隨 0
    for (e = xc.length; xc[--e] === 0;) xc.pop();

    y.c = xc;
    y.e = ye;

    return y;
  };
  1. 如果符號不同,則轉為減法運算;比如 -x + y 就是 y - x ,x + -y 就是 x - y
if (x.s != y.s) {
  y.s = -y.s;
  return x.minus(y);
}
  1. 其中兩個數字是不是 0,其中有一個為 0,則直接返回另外一個
if (!xc[0] || !yc[0]) {
  if (!yc[0]) {
    if (xc[0]) {
      y = new Big(x);
    } else {
      y.s = x.s;
    }
  }
  return y;
}
  1. 比較指數冪差,較小的一方,在前面補零,方便後續加法操作;並且將指數冪較大的一方,作為兩數相加的結果的指數冪的初始值。
if (e = xe - ye) {
  if (e > 0) {
    ye = xe; // 將指數冪較大的一方,作為兩數相加的結果的指數冪的初始值
    t = yc;
  } else {
    e = -e;
    t = xc;
  }

  t.reverse();
  for (; e--;) t.push(0);
  t.reverse();
}

比如 1234 + 12 
1234 在實例對象上是以數字數組形式表示 [1,2,3,4]
12 則是 [1,2]
為方便後續數組按照位置進行加法運算,這裡需要給 12 補零
[1,2,3,4]
    +
[0,0,1,2]
  1. xc 存放長度更長的數字
if (xc.length - yc.length < 0) {
  t = yc;
  yc = xc;
  xc = t;
}
  1. 接下來是加法邏輯
e = yc.length;

for (k = 0; e; xc[e] %= 10) k = (xc[--e] = xc[e] + yc[e] + k) / 10 | 0;

if (k) {
  xc.unshift(k);
  ++ye;
}

// 刪除尾隨 0
for (e = xc.length; xc[--e] === 0;) xc.pop();

k 保存進位的值

  • 初始化進位值為 0 ,e 為 yc 長度,執行下麵迴圈體
  • (xc[--e] = xc[e] + yc[e] + k) 計算 xc[e] 加上 yc[e] 加上上一次計算結果進位的值;
  • 隨後 xc[--e] 保存計算後的進位的數值,e--
  • 最後 xc[e] 保存計算後的個位數值

上面過程用圖例表示如下

plus.png

減法

源碼

P.minus = P.sub = function (y) {
    var i, j, t, xlty,
      x = this,
      Big = x.constructor,
      a = x.s,
      b = (y = new Big(y)).s;

    // 確定符號,x - (-y) = x + y    - x - y = -x + (-y)
    if (a != b) {
      y.s = -b;
      return x.plus(y);
    }

    var xc = x.c.slice(),
      xe = x.e,
      yc = y.c,
      ye = y.e;

    // 判斷是否為 0
    if (!xc[0] || !yc[0]) {
      if (yc[0]) {
        y.s = -b;
      } else if (xc[0]) {
        y = new Big(x);
      } else {
        y.s = 1;
      }
      return y;
    }

    // 比較兩數指數冪大小,給指數冪小的一方補零,方便後續相減;
    // 比如 1234 - 23  parse函數解析後 => [1,2,3,4] - [2,3]  為了使 [2,3] 對應十位,個位
    // 在前面補 0 ,即 [1,2,3,4] - [0,0,2,3]
    // 再比如 66 - 233  parse函數解析後 => [6,7] - [2,3,3],同樣為了使 6,7對應十位,個位
    // 在前面補 0 ,即 [0,6,7] - [2,3,3]
    if (a = xe - ye) {

      if (xlty = a < 0) {
        a = -a;
        t = xc;
      } else {
        ye = xe;
        t = yc;
      }

      t.reverse();
      for (b = a; b--;) t.push(0);  // 補零
      t.reverse();
    } else {

      // 若指數冪相等,不需要補零,則比較兩數大小,從最大位開始比較;
      // 比如 [2,3,4] 和 [1,2,3] 最大位是百位,若百位的數字不相等,則可得出孰大孰小
      j = ((xlty = xc.length < yc.length) ? xc : yc).length;

      for (a = b = 0; b < j; b++) {
        if (xc[b] != yc[b]) {
          xlty = xc[b] < yc[b];
          break;
        }
      }
    }
    
    // 對於被減數 x 和減數 y
    // 如果 x - y < 0,則交換兩數,並改變符號;比如 2 - 4 = -(4-2)
    if (xlty) {
      t = xc;
      xc = yc;
      yc = t;
      y.s = -y.s;
    }

    // 如果被減數的數字數組長度小於減數,則給被減數的末尾添加 0 
    // 比如 12 - 0.0009  parse函數解析後 => [1,2] - [0,0,0,0,9]
    // 因為 9 是小數後幾位,相應的需要給 [1,2]末尾補 0 ,即 [1,2,0,0,0] - [0,0,0,0,9]
    if ((b = (j = yc.length) - (i = xc.length)) > 0) for (; b--;) xc[i++] = 0;

    // 從 xc 中減去 yc
    for (b = i; j > a;) {
      if (xc[--j] < yc[j]) {
        for (i = j; i && !xc[--i];) xc[i] = 9;
        --xc[i];
        xc[j] += 10;
      }

      xc[j] -= yc[j];
    }

    // 去掉運算結果末尾 0 
    for (; xc[--b] === 0;) xc.pop();

    // 去掉運算結果前置 0 ,並減去相應指數冪
    for (; xc[0] === 0;) {
      xc.shift();
      --ye;
    }

    // 運算結果為 0 的情況
    if (!xc[0]) {

      // n - n = +0
      y.s = 1;

      xc = [ye = 0];
    }

    y.c = xc;
    y.e = ye;

    return y;
  };

減法前面的邏輯和加法類似,這裡不再贅述,已在上面代碼註釋中說明,下麵是減法的核心邏輯

// 從 被減數 xc 中減去減數 yc
// a 是 xc 和 yc 的冪的差值,j 是 yc 的長度,這裡迴圈條件用 j > a,表示迴圈 j-a 次
// 比如 120 - 9  =>  [1,2,0]-[0,0,9] 指數冪差是 2 ,減數數字數組長度是 3 ,則只需要迴圈 3-2=1 次
// 比如 120 - 0.009 => [1,2,0,0,0,0]-[0,0,0,0,0,9] 指數冪差是 5 ,減數數字數組長度是 6 ,則只需要迴圈 6-5=1 次
for (b = i; j > a;) { 
  if (xc[--j] < yc[j]) {
  //從後往前遍歷xc,當碰到值為0 ,將值改為 9;
  //比如 [1,0,0]-[0,0,9] => [0,9,10] -
    for (i = j; i && !xc[--i];) xc[i] = 9; 
    --xc[i];
    xc[j] += 10;
  }

  xc[j] -= yc[j];
}

上面過程用圖例表示如下,xc 表示被減數,yc 表示減數

1、若 xc 末尾項大於等於 yc 末尾項,比如 [1,2,3]和[0,0,2],則直接相減。

minus1.png

2、若 xc 末尾項小於 yc 末尾項,則執行以下邏輯

for (i = j; i && !xc[--i];) xc[i] = 9;

上面代碼表示從 當前進行相減運算的元素的位置(j) 往前遍歷被減數 xc 每個元素,當元素值為 0 時,將值改為 9,直至上一個元素值不為 0 ,迴圈結束。
minus2.png

至此,減法邏輯結束。

乘法

源碼

P.times = P.mul = function (y) {
    var c,
      x = this,
      Big = x.constructor,
      xc = x.c,
      yc = (y = new Big(y)).c,
      a = xc.length,
      b = yc.length,
      i = x.e,
      j = y.e;

    // 確定結果的符號
    y.s = x.s == y.s ? 1 : -1;

    // 其中一個為 0 ,返回結果為 0
    if (!xc[0] || !yc[0]) {
      y.c = [y.e = 0];
      return y;
    }

    // 初始化結果的指數
    y.e = i + j;

    // 對比 xc,yc 長度,xc 存放長度更長的一方
    if (a < b) {
      c = xc;
      xc = yc;
      yc = c;
      j = a;
      a = b;
      b = j;
    }

    // 用 0 初始化結果數組
    for (c = new Array(j = a + b); j--;) c[j] = 0;

    // i is initially xc.length.
    for (i = b; i--;) {
      b = 0;

      // a is yc.length.
      for (j = a + i; j > i;) {

        // Current sum of products at this digit position, plus carry.
        b = c[j] + yc[i] * xc[j - i - 1] + b;
        c[j--] = b % 10;

        // carry
        b = b / 10 | 0;
      }

      c[j] = b;
    }

    // 如果有最終進位,則增加結果的指數,否則刪除頭部的 0
    if (b) ++y.e;
    else c.shift();

    // 刪除尾部的 0
    for (i = c.length; !c[--i];) c.pop();
    y.c = c;

    return y;
  };

乘法源碼的主要邏輯是下麵這一段

for (i = b; i--;) {
  b = 0;

  for (j = a + i; j > i;) {

    // 當前數字位置的總和,加上進位
    b = c[j] + yc[i] * xc[j - i - 1] + b;
    c[j--] = b % 10;

    // 進位值
    b = b / 10 | 0;
  }

  c[j] = b;
}

描述的其實就是以前老師教我們在紙上乘法運算的過程:

times.png

123*12 來舉例子分析上面這段代碼

  • xc 是乘數 [1,2,3],yc 是被乘數 [1,2],b 是 yc 長度,a 是 xc 長度

  • c 是保存結果的數組,定義的長度是 a+b

兩個數相乘得到的結果長度可能是 a+b,也有可能是 a+b-1。所以後面需要刪除數組頭部的 0

  1. for (i = b; i--;) 首先是外層迴圈,從數組長度較短的被乘數開始迴圈,將 b 賦值給 i,i 充當 yc 的長度,而 b 用來保存進位的值
  2. b = 0 定義進位的值
  3. for (j = a + i; j > i;) 內層乘數(123)的迴圈,這裡的 j 表示在結果數組 c 中的位置

for (j = a + i; j > i;) 實際上就是 for ( j = 乘數長度 + 當前被乘數數字的位置 ),這裡是因為當第二輪外層迴圈時,123 * 1 的時候,1 是 12 的 十位,所以在 j 也應該從十位開始保存計算結果。

第一輪外層迴圈

times1.png

第二輪外層迴圈

times2.png

  1. b = c[j] + yc[i] * xc[j - i - 1] + b 當前數字位置的總和,加上進位

times3.png

  1. c[j--] = b % 10; 當前位置去整取餘
  2. b = b / 10 | 0; 進位值取整

至此乘法運算邏輯結束

除法

源碼

P.div = function (y) {
    var x = this,
      Big = x.constructor,
      a = x.c,                  // dividend
      b = (y = new Big(y)).c,   // divisor
      k = x.s == y.s ? 1 : -1,
      dp = Big.DP;

    if (dp !== ~~dp || dp < 0 || dp > MAX_DP) {
      throw Error(INVALID_DP);
    }

    // Divisor is zero?
    if (!b[0]) {
      throw Error(DIV_BY_ZERO);
    }

    // Dividend is 0? Return +-0.
    if (!a[0]) {
      y.s = k;
      y.c = [y.e = 0];
      return y;
    }

    var bl, bt, n, cmp, ri,
      bz = b.slice(),
      ai = bl = b.length,
      al = a.length,
      r = a.slice(0, bl),   // remainder
      rl = r.length,
      q = y,                // quotient
      qc = q.c = [],
      qi = 0,
      p = dp + (q.e = x.e - y.e) + 1;    // precision of the result

    q.s = k;
    k = p < 0 ? 0 : p;

    // Create version of divisor with leading zero.
    bz.unshift(0);

    // Add zeros to make remainder as long as divisor.
    for (; rl++ < bl;) r.push(0);

    do {

      // n is how many times the divisor goes into current remainder.
      for (n = 0; n < 10; n++) {

        // Compare divisor and remainder.
        if (bl != (rl = r.length)) {
          cmp = bl > rl ? 1 : -1;
        } else {
          for (ri = -1, cmp = 0; ++ri < bl;) {
            if (b[ri] != r[ri]) {
              cmp = b[ri] > r[ri] ? 1 : -1;
              break;
            }
          }
        }

        // If divisor < remainder, subtract divisor from remainder.
        if (cmp < 0) {

          // Remainder can't be more than 1 digit longer than divisor.
          // Equalise lengths using divisor with extra leading zero?
          for (bt = rl == bl ? b : bz; rl;) {
            if (r[--rl] < bt[rl]) {
              ri = rl;
              for (; ri && !r[--ri];) r[ri] = 9;
              --r[ri];
              r[rl] += 10;
            }
            r[rl] -= bt[rl];
          }

          for (; !r[0];) r.shift();
        } else {
          break;
        }
      }

      // Add the digit n to the result array.
      qc[qi++] = cmp ? n : ++n;

      // Update the remainder.
      if (r[0] && cmp) r[rl] = a[ai] || 0;
      else r = [a[ai]];

    } while ((ai++ < al || r[0] !== UNDEFINED) && k--);

    // Leading zero? Do not remove if result is simply zero (qi == 1).
    if (!qc[0] && qi != 1) {

      // There can't be more than one zero.
      qc.shift();
      q.e--;
      p--;
    }

    // Round?
    if (qi > p) round(q, p, Big.RM, r[0] !== UNDEFINED);

    return q;
  };

在除法運算中,對於 a/b , a 是被除數,b 是除數,下麵依次分析上面代碼

  1. if (dp !== ~~dp || dp < 0 || dp > MAX_DP) 判斷 dp 是不是大於 0 的整數,並且小於 MAX_DP,這裡的 dp 可以自己設置
Big.DP = 30
  1. 除數為 0 則拋出錯誤。
if (!b[0]) {
  throw Error(DIV_BY_ZERO);
}
  1. 被除數是 0 則返回值為 0 的實例對象
if (!a[0]) {
  y.s = k;
  y.c = [y.e = 0];
  return y;
}
  1. 接下來是除法運算邏輯,定義變數的那一段不貼了,直接看 do while 迴圈
do {

  // n 是迴圈次數,表示從當前位置的餘數中可以分出多少個除數來,也就是當前位置的商。
  for (n = 0; n < 10; n++) {

    // 比較除數和餘數大小
    if (bl != (rl = r.length)) {
      cmp = bl > rl ? 1 : -1;
    } else {
      for (ri = -1, cmp = 0; ++ri < bl;) {
        if (b[ri] != r[ri]) {
          cmp = b[ri] > r[ri] ? 1 : -1;
          break;
        }
      }
    }

    // 除數小於餘數,則繼續從餘數中減去除數
    if (cmp < 0) {

      // Remainder can't be more than 1 digit longer than divisor.
      // Equalise lengths using divisor with extra leading zero?
      for (bt = rl == bl ? b : bz; rl;) {
        if (r[--rl] < bt[rl]) {
          ri = rl;
          for (; ri && !r[--ri];) r[ri] = 9;
          --r[ri];
          r[rl] += 10;
        }
        r[rl] -= bt[rl];
      }

      for (; !r[0];) r.shift();
    } else {
      break;
    }
  }

  // qc 數組保存商
  qc[qi++] = cmp ? n : ++n;

  // 更新餘數
  if (r[0] && cmp) r[rl] = a[ai] || 0;
  else r = [a[ai]];

} while ((ai++ < al || r[0] !== UNDEFINED) && k--);

這個迴圈做了這些事情:以 1234 / 9 為例;

div1.png

  • 將當前位置的餘數 1 和除數 9 比較大小,(一開始餘數取的是被除數的前面 n 位,n 和除數的長度大小相同,所以取的是 1 )。先比較長度,長度相同再比較大小。
  • 若除數大於當前位置餘數,則跳出迴圈(當前迴圈次數即為當前位置的商,9 > 1 ,那麼當前迴圈次數為 0 ,即當前位置商為 0 )
  • 然後保存商 qc[qi++] = cmp ? n : ++n;
  • 最後更新當前餘數,除數大於餘數時,則當前餘數向後借一位,餘數就由 1 變為了 12
if (r[0] && cmp) r[rl] = a[ai] || 0;
  else r = [a[ai]];
  • 若除數小於當前餘數,則繼續從餘數中減去除數 (開始減法 for 迴圈)

div2.png

  • 然後再次保存商 qc[qi++] = cmp ? n : ++n;
  • 然後再次更新當前餘數,除數大於餘數時,則當前餘數向後借一位,餘數就由 3 變為了 33
  • 當商數組的長度沒有達到指定的精度總和,繼續上面的步驟,直至迴圈結束;

指定的精度總和指的是 Big.DP(預設20) + (被除數的指數-除數的指數); 1234 / 9 的指定精度總和是 23。

  1. 商數組長度大於 1 的情況下,刪除數組前面的 0 ;如果是 商就是 0 ,比如 0/1 = 0,這種情況不必刪除 0 了
 if (!qc[0] && qi != 1) {
  qc.shift();
  q.e--;
  p--;
}
  1. 舍入操作 if (qi > p) round(q, p, Big.RM, r[0] !== UNDEFINED);

至此除法邏輯結束

註意事項

big.js 用數組存儲值,類似 高精度計算,只不過 big.js 是數組中每個位置存儲一個值,然後對每個位置進行運算;而對超級大的數字(數百或數千位數值時),big.js 算術運算不如 bignumber.js 快。

例如,bignumber.js 將數字1234.56789的數字存儲為[1234,56789000000000] ,即以兩個1e14為基數的數組形式存儲,而 big.js 存儲的數字與[1,2,3,4.5,6,7,8,9]相同,即以9個10為基數的數組形式存儲。前者的算術運算可能更快,因為需要處理的元素較少。在實踐中,這可能只有在使用數百或數千位數值時才會有所不同。

在使用 big.js 進行運算時需要註意有時候沒有設置足夠大的精度,會導致結果不是想要的。

Big.DP = 20
+Big(1).div('11111111').times('11111111') // 0.9999999999999999
// 0.9999999999999999 在 Number 編碼的可以表示的準確精度範圍內

Big.DP = 30
+Big(1).div('11111111').times('11111111') // 1
// 而設置 Big.DP = 30 後
//結果數組保存的是 999999999999999999999999
//超過了 Number 編碼的可以表示的準確精度範圍,則會舍入為 1

總結

本文剖析了 big.js 解析函數源碼,四則運算源碼,分別用圖文詳細描述了運算過程,一步步還原了作者的構思。有不正確的地方或者不同見解還請各位大佬提出來。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一個變數如果聲明為聯合類型,而後續操作需要針對其具體的單一類型做不同處理,這個過程就叫做類型收窄(`Narrowing`) ...
  • 項目代碼同步至碼雲 weiz-vue3-template Vue Router 是 Vue.js 的官方路由。它與 Vue.js 核心深度集成,讓用 Vue.js 構建單頁應用變得輕而易舉。 1. 安裝 npm i vue-router@4 2. 集成 1. 新建兩頁面進行示例 在src/view下 ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 函數創建與定義的過程 函數定義階段 在堆記憶體中開闢一段空間 把函數體內的代碼一模一樣的存儲在這段空間內 把空間賦值給棧記憶體的變數中 函數調用階段 按照變數名內的存儲地址找到堆記憶體中對應的存儲空間 在調用棧中開闢一個新的函數執行空間 在執行 ...
  • TS官方Handbook: TypeScript: Handbook - The TypeScript Handbook (typescriptlang.org) 基礎 相關概念 運行時錯誤:JS 的大多數錯誤都只能在運行的過程中被髮現。 靜態類型系統:TS 可以在運行代碼之前發現錯誤。 非異常失敗 ...
  • 無論你做什麼,都要相信自己可以做到,因為你的潛力是無限的。 把父組件的狀態變成屬性傳遞給子組件,子組件接受這個屬性,聽命於父組件。這個子組件就是叫做受控組件。在受控與非受控組件有兩種理解方案,第一:狹義上的受控與非受控,就是我們在表單中的受控與非受控組件。第二:廣義上的受控與非受控組件,就是 Rea ...
  • 不要因為別人的評價而改變自己的想法,因為你的生活是你自己的。 1. React 中 Ref 的應用 1.1 給標簽設置 ref 給標簽設置 ref,ref="username", 通過 this.refs.username 可以獲取引用的標簽,ref 可以獲取到應用的真實 Dom 節點。但是 thi ...
  • 作為一個開放式的跨端跨框架解決方案,Taro 在大量的小程式和 H5 應用中得到了廣泛應用。本文將為大家提供一些小程式開發的最佳實踐,幫助大家最大程度地提升小程式應用的性能表現。 ...
  • typora-copy-images-to: media ES5和字元串 一、ES5的語法 js在產生的時候,年代比較早,當時的web需求比較少,所以剛開始的js功能比較少,語法沒有特別嚴謹。隨著時代的發展和web應用的普及,js需要更多的功能,以及更嚴謹的語法,所以,js會有版本的升級。第一版的j ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...