JavaScript BigIntを使用して小数を含む桁数上限のない四則演算

[小数の誤差を出さない四則演算](https://qiita.com/akebi_mh/items/93b211dc93fe41fa94f8)のBigInt使用バージョンです。
使用方法は[前回](https://qiita.com/akebi_mh/items/93b211dc93fe41fa94f8)と同じです。

#### **[動作確認フォーム](https://akebi.jp/temp/decimalcalc/usebigint/)** ####

#### 小数を扱えないBigIntで小数の計算を行う手順 ####

BigIntは整数しか扱えませんので整数になるよう桁をずらしてから計算するわけですが、そのずらす桁数をどう決めればよいか、例として**A**が**123.45**、**B**が**6.789**の場合で考えてみます。

**加算の場合**
AとBの和の小数部分の桁数は最大でもAとBどちらかの小数の桁数の大きい方の値になりますので、その値だけAとB双方の桁をずらします。

A **123.<font color='red'>45</font>** …小数部2桁
B **6.<font color='red'>789</font>** …小数部3桁 (こちらの桁数が大きいので3桁分ずらす)
　　↓
A **123<font color='blue'>45</font><font color='red'>0</font>** …桁が足りずずらしきれない分は0を追加する
B **6<font color='blue'>789</font>**

この値で和を求めます。
**130239**

ずらした桁数分、戻します。
**130<font color='blue'>.239</font>**

**減算の場合**
加算と同じ手順で、加算の代わりに減算するだけです。

**乗算の場合**
AとBの積の小数部の桁数は最大でAとBそれぞれの小数部の桁数を足した値になります。
乗算する際は単純に小数点を取り去った値で計算し、その後小数桁数分をずらします。

A **123.<font color='red'>45</font>** …小数部2桁
B **6.<font color='red'>789</font>** …小数部3桁
(2桁+3桁で、5桁分が積を求めたあとにずらす桁数になりますが、積を求める際は単純に小数点を取り去ります)
　　↓
A **123<font color='blue'>45</font>**
B **6<font color='blue'>789</font>**

この値で積を求めます。
**83810205**

最初に求めておいた5桁分戻します。
**838<font color='blue'>.10205</font>**

**除算の場合**
ずらす桁数は加算・減算と同様AとBの小数部の桁数の大きい方の値ですが、最終的な商で小数まで求めたい場合は確保したい小数部の桁数の分だけAを余分にずらす必要があります。
ここでは小数部を10桁確保するものとして考えます。

A **123.<font color='red'>45</font>** …小数部2桁
B **6.<font color='red'>789</font>** …小数部3桁
大きい方の桁数3がずらす値ですが、割られる数であるAはさらに必要小数桁数10を加えた13桁分ずらします。
丸めによる繰り上がりまで考慮したい場合はあらかじめ更に1桁余分にずらしておく必要がありますが、とりあえずここでは考えないものとします。
　　↓
A **123<font color='blue'>45</font><font color='red'>00000000000</font>** …13桁ずらした
B **6<font color='blue'>789</font>** …3桁ずらした

この値で商を求めます。
**181838267786.124613345117101...** ですが、BigIntで除算した商に小数部はありませんので
**181838267786** となります。
ここからAに対して余分にずらしておいた10桁分を戻して
**18<font color='blue'>.1838267786</font>**
が最終的な商になります。

#### スクリプト ####

~~~js:decimalcalc.js
'use strict';
const decimalCalc = (function() {
    if(typeof BigInt === 'undefined') {
        console.log('Your browser does not support BigInt.');
        return;
    }
    return {
        // 加算
        add: function(a, b) {
            a = this.toString(a);
            b = this.toString(b);
            if(isNaN(a) || isNaN(b)) return NaN;
            else if(a === 'Infinity' && b === '-Infinity') return NaN;
            else if(a === '-Infinity' && b === 'Infinity') return NaN;
            else if(/-?Infinity/.test(a)) return a;
            else if(/-?Infinity/.test(b)) return b;

            const p = Math.max(this.l(a), this.l(b));
            if(p) {
                if(!/\./.test(a)) a += '0'.repeat(p);
                else a += '0'.repeat(p - (a.length - a.indexOf('.') - 1));
                if(!/\./.test(b)) b += '0'.repeat(p);
                else b += '0'.repeat(p - (b.length - b.indexOf('.') - 1));
            }
            a = a.replace('.', '');
            b = b.replace('.', '');
            let c = String(BigInt(a) + BigInt(b));
            if(p) {
                c = c.replace(/^(-)?/, "$1" + '0'.repeat(p));
                c = (c.slice(0, -p) + '.' + c.slice(-p));
                c = this.resReplace(c);
            }
            return c;
        },

        // 減算
        sub: function(a, b) {
            a = this.toString(a);
            b = this.toString(b);
            if(isNaN(a) || isNaN(b)) return NaN;
            else if(a === 'Infinity' && b === 'Infinity') return NaN;
            else if(a === '-Infinity' && b === '-Infinity') return NaN;
            else if(/-?Infinity/.test(a)) return a;
            else if(/-?Infinity/.test(b)) return -b;

            const p = Math.max(this.l(a), this.l(b));
            if(p) {
                if(!/\./.test(a)) a += '0'.repeat(p);
                else a += '0'.repeat(p - (a.length - a.indexOf('.') - 1));
                if(!/\./.test(b)) b += '0'.repeat(p);
                else b += '0'.repeat(p - (b.length - b.indexOf('.') - 1));
            }
            a = a.replace('.', '');
            b = b.replace('.', '');
            let c = String(BigInt(a) - BigInt(b));
            if(p) {
                c = c.replace(/^(-)?/, "$1" + '0'.repeat(p));
                c = (c.slice(0, -p) + '.' + c.slice(-p));
                c = this.resReplace(c);
            }
            return c;
        },

        // 乗算
        mul: function(a, b) {
            a = this.toString(a);
            b = this.toString(b);
            if(isNaN(a) || isNaN(b)) return NaN;
            else if(/^-?[0.]+$/.test(a) && /-?Infinity/.test(b)) return NaN;
            else if(/-?Infinity/.test(a) && /^-?[0.]+$/.test(b)) return NaN;
            else if(a === 'Infinity' && b === 'Infinity') return Infinity;
            else if(a === '-Infinity' && b === '-Infinity') return Infinity;
            else if(/-?Infinity/.test(a) || /-?Infinity/.test(b)) return a * b;

            const p = this.l(a) + this.l(b);
            a = a.replace('.', '');
            b = b.replace('.', '');
            let c = String(BigInt(a) * BigInt(b));
            if(p) {
                c = c.replace(/^(-)?/, "$1" + '0'.repeat(p));
                c = (c.slice(0, -p) + '.' + c.slice(-p));
                c = this.resReplace(c);
            }
            return c;
        },

        // 除算
        div: function(a, b, m, truncate) {
            a = this.toString(a);
            b = this.toString(b);
            if(isNaN(a) || isNaN(b)) return NaN;
            else if(/-?Infinity/.test(a) && /-?Infinity/.test(b)) return NaN;
            else if(/-?Infinity/.test(a) && !/-?Infinity/.test(b)) return a / b;
            else if(/-?Infinity/.test(b)) return 0;
            else if(/^-?[0.]+$/.test(a) && /^-?[0.]+$/.test(b)) return NaN;
            else if(/^[0.]+$/.test(b) || b === '') return Infinity;
            else if(/^-[0.]+$/.test(b)) return -Infinity;

            if(m === undefined) m = 20;
            if(m < 0) m = 0;
            else m = Math.floor(m);

            if(truncate === undefined) truncate = false;
            truncate = Boolean(truncate);

            const p = Math.max(this.l(a), this.l(b)) + m + 1;
            if(!/\./.test(a)) a += '0'.repeat(p + m + 1);
            else a += '0'.repeat(p + m + 1 - (a.length - a.indexOf('.') - 1));

            if(!/\./.test(b)) b += '0'.repeat(p);
            else b += '0'.repeat(p - (b.length - b.indexOf('.') - 1));

            a = a.replace('.', '');
            b = b.replace('.', '');
            let c = String(BigInt(a) / BigInt(b));

            c = c.replace(/^(-)?/, "$1" + '0'.repeat(p));

            const
                n = c.slice(0, -(m + 1)).split(''),
                d = c.slice(-(m + 1)).split('');

            if(truncate) {
                // 切り捨て
                d[m] = 0;
            }
            else {
                // 小数部の必要桁数での丸め
                if(d[m] >= 5) {
                    if(m > 0) {
                        ++d[m - 1];
                    }
                    else {
                        ++n[n.length - 1];
                    }
                }
                d[m] = 0;
                // 丸めによる繰り上がり
                for(let i = 0; i < m; ++i) {
                    const p = m - i;
                    if(d[p] > 9) {
                        d[p] %= 10;
                        ++d[p - 1];
                    }
                }
                // 小数第一位の繰り上がりは整数部へ反映
                if(d[0] > 9) {
                    d[0] %= 10;
                    ++n[n.length - 1];
                }
                // 整数部の繰り上がり
                for(let i = 0; i < n.length - 1; ++i) {
                    const p = n.length - i - 1;
                    if(n[p] > 9) {
                        n[p] %= 10;
                        ++n[p - 1];
                    }
                }
            }
            c = n.join('') + '.' + d.join('');
            c = this.resReplace(c);
            return c;
        },

        // 小数部の桁数取得
        l: function(s) {
            return s.indexOf('.') !== -1 ? s.length - s.indexOf('.') - 1 : 0;
        },
        resReplace: function(s) {
            return s
                .replace(/0+$/, '').replace(/\.$/, '')
                .replace(/^(-)?0+/, "$1").replace(/^(-)?\./, "$1" + '0.')
                .replace(/^-?$/, '0');
        },
        // 引数のキャスト用
        toString: function(str) {
            if(typeof str !== 'string') str = String(str);
            str = str.replace(/^\++/, '');
            if(/^(0x[\da-f]+|0o[0-7]+|0b[01]+)$/i.test(str)) str = String(Number(str));
            str = str.trim();
            let tmp;
            // 指数表記だったらパース
            if(tmp = str.match(/^(-?)([\d.]+)(e)([+-]?)(\d+)$/i)) {
                let   n = tmp[2].split('.');
                const s = tmp[1],
                      f = tmp[4],
                      e = Number(tmp[5]);

                if(n[1] === undefined) n[1] = '';
                n[1] += '0'.repeat(e);

                if(f === '-') {
                    n[0] = '0'.repeat(e) + n[0];
                    n[1] = n[0].slice(-e) + n[1];
                    n[0] = n[0].slice(0, -e);
                }
                else {
                    n[0] += n[1].slice(0, e);
                    n[1] =  n[1].slice(e);
                }
                str = s + n[0].replace(/^0+$/, '') + '.' + n[1].replace(/0+$/, '');
                str = str.replace(/^\./, '0.').replace(/\.$/, '');
            }
            return str;
        },
    };
}());
~~~

#### 参考 ####
[BigInt | MDN](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/BigInt)