LoginSignup
0

More than 1 year has passed since last update.

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

Last updated at Posted at 2020-11-23

小数の誤差を出さない四則演算のBigInt使用バージョンです。
使用方法は前回と同じです。

動作確認フォーム

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

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

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

A 123.45 …小数部2桁
B 6.789 …小数部3桁 (こちらの桁数が大きいので3桁分ずらす)
  ↓
A 123450 …桁が足りずずらしきれない分は0を追加する
B 6789

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

ずらした桁数分、戻します。
130.239

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

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

A 123.45 …小数部2桁
B 6.789 …小数部3桁
(2桁+3桁で、5桁分が積を求めたあとにずらす桁数になりますが、積を求める際は単純に小数点を取り去ります)
  ↓
A 12345
B 6789

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

最初に求めておいた5桁分戻します。
838.10205

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

A 123.45 …小数部2桁
B 6.789 …小数部3桁
大きい方の桁数3がずらす値ですが、割られる数であるAはさらに必要小数桁数10を加えた13桁分ずらします。
丸めによる繰り上がりまで考慮したい場合はあらかじめ更に1桁余分にずらしておく必要がありますが、とりあえずここでは考えないものとします。
  ↓
A 1234500000000000 …13桁ずらした
B 6789 …3桁ずらした

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

スクリプト

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0