LoginSignup
4
0

More than 3 years have passed since last update.

JavaScript 小数の誤差を出さない四則演算

Last updated at Posted at 2020-11-04

桁ごとに分けた10進数で計算することで誤差を出さないようにすることが主旨です。
同様のライブラリも多数あるかと思いますが、自分で組んで理解を深めたかったので。

引数、戻り値の受け渡しは文字列で、内部の計算は桁ごとに配列に入れた数値で行なっています。
桁数上限はありませんが、高速化は重視していないので桁が多くなればそれなりに時間はかかるかと思います。

スクリプト

decimalcalc.js
'use strict';
const decimalCalc = (function() {
    return {
        // 加算
        add: function(_a, _b) {
            let a = _a,
                b = _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;

            let tmp = this.cvtArray(a);
            let aSign = tmp.sign,
                aDecimalPosition = tmp.decimalPosition;
            const ra = tmp.array;

            tmp = this.cvtArray(b);
            let bSign = tmp.sign,
                bDecimalPosition = tmp.decimalPosition;
            const rb = tmp.array;

            // bのみマイナスの場合はbのマイナス符号を削除して減算へ
            if(aSign === 1 && bSign === -1) {
                return this.sub(_a, _b.replace(/^-/, ''));
            }
            // aのみマイナスの場合はbにマイナス符号を付加して減算へ
            else if(aSign === -1 && bSign === 1) {
                return this.sub(_a, '-' + _b);
            }

            // 小数部桁揃え
            if(aDecimalPosition > bDecimalPosition) {
                for(let i = 0; i < aDecimalPosition - bDecimalPosition; i++) rb.push(0);
                bDecimalPosition = aDecimalPosition;
            }
            else if(bDecimalPosition > aDecimalPosition) {
                for(let i = 0; i < bDecimalPosition - aDecimalPosition; i++) ra.push(0);
                aDecimalPosition = bDecimalPosition;
            }

            // 整数部桁揃え
            if(ra.length > rb.length) {
                const n = ra.length - rb.length;
                for(let i = 0; i < n; i++) rb.unshift(0);
            }
            else if(rb.length > ra.length){
                const n = rb.length - ra.length;
                for(let i = 0; i < n; i++) ra.unshift(0);
            }

            // 加算
            for(let i = 0; i < ra.length; i++) {
                const p = ra.length - i - 1;
                rb[p] += ra[p];
                // 繰り上がり
                if(p > 0 && rb[p] >= 10) {
                    rb[p - 1]++;
                    rb[p] %= 10;
                }
            }

            const
                result = rb.join(''),
                n = aDecimalPosition === 0 ? result :
                    result.slice(0, -aDecimalPosition);

            return (aSign === -1 ? '-' : '') + ((n === '' ? '0' : n) +
                    ('.' + (aDecimalPosition ? result.slice(-aDecimalPosition) : '')).
                    replace(/0+$/, '').replace(/\.$/, ''));
        },

        // 減算
        sub: function(_a, _b) {
            let a = _a,
                b = _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;

            let tmp = this.cvtArray(a);
            let aSign = tmp.sign,
                aDecimalPosition = tmp.decimalPosition;
            const ra = tmp.array;

            tmp = this.cvtArray(b);
            let bSign = tmp.sign,
                bDecimalPosition = tmp.decimalPosition;
            const rb = tmp.array;

            // bのみマイナスの場合はbのマイナス符号を削除して加算へ
            if(aSign === 1 && bSign === -1) {
                return this.add(_a, _b.replace(/^-/, ''));
            }
            // aのみマイナスの場合はbにマイナス符号を付加して加算へ
            else if(aSign === -1 && bSign === 1) {
                return this.add(_a, '-' + _b);
            }

            // 小数部桁揃え
            if(aDecimalPosition > bDecimalPosition) {
                for(let i = 0; i < aDecimalPosition - bDecimalPosition; i++) rb.push(0);
                bDecimalPosition = aDecimalPosition;
            }
            else if(bDecimalPosition > aDecimalPosition) {
                for(let i = 0; i < bDecimalPosition - aDecimalPosition; i++) ra.push(0);
                aDecimalPosition = bDecimalPosition;
            }

            // 整数部桁揃え
            if(ra.length > rb.length) {
                const n = ra.length - rb.length;
                for(let i = 0; i < n; i++) rb.unshift(0);
            }
            else if(rb.length > ra.length){
                const n = rb.length - ra.length;
                for(let i = 0; i < n; i++) ra.unshift(0);
            }

            // aとbを入れ替える場合の符号反転
            let sign = '';
            if( aSign === 1 && ra.join('') < rb.join('') ||
                aSign === -1 && ra.join('') > rb.join('') ) sign = '-';

            // 絶対値でbのほうが大きい場合はaとbを交換
            if(rb.join('') > ra.join('')) {
                for(let i = 0; i < ra.length; i++) {
                    const tmp = ra[i];
                    ra[i] = rb[i];
                    rb[i] = tmp;
                }
            }

            // 減算
            for(let i = 0; i < ra.length; i++) {
                const p = ra.length - i - 1;
                ra[p] -= rb[p];
                // 繰り下がり
                if(ra[p] < 0) {
                    ra[p] += 10;
                    if(p > 0) ra[p - 1]--;
                }
            }

            const
                result = ra.join(''),
                n = (aDecimalPosition === 0 ? result :
                    result.slice(0, -aDecimalPosition)).replace(/^0+/, '');

            return sign + ((n === '' ? '0' : n) +
                ('.' + (aDecimalPosition ? result.slice(-aDecimalPosition) : '')).
                replace(/0+$/, '').replace(/\.$/, ''));
        },

        // 乗算
        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;

            let tmp = this.cvtArray(a);
            let aSign = tmp.sign,
                aDecimalPosition = tmp.decimalPosition;
            const ra = tmp.array;

            tmp = this.cvtArray(b);
            let bSign = tmp.sign,
                bDecimalPosition = tmp.decimalPosition;
            const rb = tmp.array;

            const rc = [],
                  rax = [],
                  rbx = [],
                  x = 7, // 一度に処理する桁数 1~7
                  dx = Math.pow(10, x),
                  dx2 = dx / 10;
            while(ra.length) rax.push(ra.splice(-x).join(''));
            while(rb.length) rbx.push(rb.splice(-x).join(''));

            // 乗算
            for(let l = 0; l < rax.length; l++) {
                for(let i = 0; i < rbx.length; i++) {
                    const p = i + l;
                    if(rc[p] === undefined) rc[p] = 0;
                    rc[p] += rax[l] * rbx[i];
                    if(rc[p] >= dx) {
                        if(rc[p + 1] === undefined) rc[p + 1] = 0;
                        rc[p + 1] += Math.floor(rc[p] / dx);
                        rc[p] %= dx;
                    }
                }
            }
            rc.push(0);
            for(let i = 0; i < rc.length; ++i) {
                if(rc[i] < dx2) rc[i] = (this.repeat('0', x) + rc[i]).slice(-x);
            }

            const
                result = rc.reverse().join(''),
                dPos = aDecimalPosition + bDecimalPosition,
                rn = dPos ?
                    result.slice(0, -dPos).replace(/^0+/, '') : result.replace(/^0+/, ''),
                rd = dPos ?
                    ('.' + result.slice(-dPos)).replace(/0+$/, '').replace(/\.$/, '') : '';

            return (aSign !== bSign ? '-' : '') +
                (rn === '' ? '0' : rn) +
                rd;
        },

        // 除算
        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;
            else if(m < 0) m = 0;
            m = Math.floor(m);

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

            let tmp = this.cvtArray(a);
            let aSign = tmp.sign,
                aDecimalPosition = tmp.decimalPosition,
                anL = tmp.length;
            const ra = tmp.array;
            a = tmp.str;

            tmp = this.cvtArray(b);
            let bSign = tmp.sign,
                bDecimalPosition = tmp.decimalPosition,
                bnL = tmp.length,
                bdzn = tmp.decimalZeroNum;
            const rb = tmp.array;
            b = tmp.str;

            const qdp = 1 + anL - bnL;

            // 小数部桁揃え
            if(aDecimalPosition > bDecimalPosition) {
                for(let i = 0; i < aDecimalPosition - bDecimalPosition; i++) rb.push(0);
            }
            else if(bDecimalPosition > aDecimalPosition) {
                for(let i = 0; i < bDecimalPosition - aDecimalPosition; i++) ra.push(0);
            }

            // 整数部桁揃え
            if(ra.length > rb.length) {
                const n = ra.length - rb.length;
                for(let i = 0; i < n; i++) rb.unshift(0);
            }
            else if(rb.length > ra.length){
                const n = rb.length - ra.length;
                for(let i = 0; i < n; i++) ra.unshift(0);
            }

            const lM = Math.max(ra.length, rb.length);

            const rc = new Array(lM);
            for(let i = 0; i < rc.length; i++) rc[i] = 0;

            b = b.replace(/^0+/, '');

            let k = b.length - 1;
            a = ra.slice(1 + k - b.length, 1 + k).join('');

            // 除算
            let sp = 0;
            do {
                if(ra[k + b.length] === undefined) {
                     ra.push(0);
                     rc.push(0);
                }
                let count = 0;
                while(('00' + a).slice(-(b.length + 1)) >=
                      ('00' + b).slice(-(b.length + 1)) ) {
                    a = this.sub(a, b);
                    rc[sp]++;
                    if(++count > 9) break;
                }
                sp ++;
                a += ra[k + 1];
            } while(++k < m + 1 + lM);

            // 商の配列を整数部と小数部に振り分け
            let an, ad;
            if(qdp > 0) {
                const p = qdp + (bnL === 0 ? bdzn : 0);
                an = rc.slice(0, p);
                ad = rc.slice(p);
            }
            else {
                an = [0];
                ad = rc.slice(1);
            }

            if(truncate) {
                for(let i = m; i < ad.length; i++) ad[i] = 0;
            }
            else {
                // 小数部の必要桁数での丸め
                if(ad[m] >= 5) {
                    if(m > 0) {
                        ad[m - 1]++;
                    }
                    else {
                        an[an.length - 1]++;
                    }
                }
                for(let i = m; i < ad.length; i++) ad[i] = 0;

                // 丸めによる繰り上がり
                for(let i = 0; i < m; i++) {
                    const p = m - i;
                    if(ad[p] > 9) {
                        ad[p] %= 10;
                        ad[p - 1]++;
                    }
                }
                // 小数第一位の繰り上がりは整数部へ反映
                if(ad[0] > 9) {
                    ad[0] %= 10;
                    an[an.length - 1]++;
                }
                // 整数部の繰り上がり
                for(let i = 0; i < an.length - 1; i++) {
                    const p = an.length - i - 1;
                    if(an[p] > 9) {
                        an[p] %= 10;
                        an[p - 1]++;
                    }
                }
            }

            return (aSign !== bSign ? '-' : '') +
                an.join('').replace(/^0+/, '').replace(/^$/, '0') +
                ('.' + ad.join('')).replace(/0+$/, '').replace(/\.$/, '');
        },

        // 引数のキャスト用
        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] += this.repeat('0', e);

                if(f === '-') {
                    n[0] = this.repeat('0', 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;
        },
        // repeat
        repeat: function(str, n) {
            if(String.prototype.repeat) {
                return String(str).repeat(n);
            }
            // ES6未満
            return Array(n + 1).join(str);
        },
        // 配列変換及び符号や小数点位置等の取得
        cvtArray: function(str) {
            let sgn = 1;
            if(str[0] === '-') {
                sgn = -1;
                str = str.slice(1);
            }
            str = str.replace(/^0+/, '').replace(/(\.\d*?)0*$/,"$1").replace(/\.$/, '');
            const len = str.replace(/\..*/, '').replace(/^0/, '').length;
            const dzn = str.match(/\.(0+)/);
            let dp = 0;
            if(str.indexOf('.') !== -1) {
                dp = str.replace(/^.*\./, '').length;
                str = str.replace('.', '');
            }
            const r = str.split('');
            for(let i = 0; i < r.length; ++i) r[i] = Number(r[i]);
            return {
                sign: sgn,
                length: len,
                decimalPosition: dp,
                array: r,
                decimalZeroNum: dzn !== null ? dzn[1].length : 0,
                str: str,
            };
        },
    };
}());

明らかな計算違い等の不具合は可能な限り都度修正します。

使用例

各演算メソッドの引き数は文字列型と数値型どちらでも構いませんが、数値型だと値によっては渡す時点で丸められることもあります。

<script src='./decimalcalc.js'></script>
<script>
const c = decimalCalc;
const
    a = '9876543210987654321098765432109876543210',
    b = '1234512345123451234512345123451234512345';

console.log('加算');
console.log(c.add(a, b));
console.log(Number(a) + Number(b));

console.log('減算');
console.log(c.sub(a, b));
console.log(a - b);

console.log('乗算');
console.log(c.mul(a, b));
console.log(a * b);

console.log('除算');
console.log(c.div(a, b));
console.log(c.div(a, b, 49)); // 第3引数 小数部桁数(デフォルト:20)
console.log(c.div(a, b, 49, true)); // 第4引数 切り捨てモード(デフォルト:false)
console.log(a / b);

console.log(c.add('1e-20', '1e20'));
</script>

結果

加算
11111055556111105555611110555561111055555
1.1111055556111106e+40
減算
8642030865864203086586420308658642030865
8.642030865864204e+39
乗算
12192714521109470354099966925608898681578341524155845132525385611263518670927450
1.219271452110947e+79
除算
8.00036002070112956222
8.0003600207011295622159209756626613563755006435355
8.0003600207011295622159209756626613563755006435354
8.00036002070113
100000000000000000000.00000000000000000001

引き数は指数表記も受け取れますが、戻り値は桁数の多い値を指数表記に変換するような機能は今のところ実装していません。

簡易動作確認フォーム

当スクリプトを使用した階乗計算のサンプル


4
0
0

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
4
0