JavaScriptで10進数文字列を使用した正確な計算

Author:

JavaScriptで正確に計算する

JavaScriptは、IEEE 754準拠の倍精度浮動小数点(double型)を使用しています。この形式は広く使われている一方で、double型で正確に表せない小数を扱う際に、わずかな誤差が生じるという問題があります。
たとえば、0.1 + 0.2が0.3にならず0.30000000000000004といった結果になることは、JavaScriptを使用している多くの人が一度は直面する問題です。
このような誤差は、正確性が求められるシステムにおいて致命的なバグに繋がる可能性があります。

本記事では、このような誤差を回避し、高い精度で演算を行う方法として10進数文字列を使った正確な計算を実施するJavaScriptの例を紹介します。
数値から変換した文字列を使用することで、内部的なdouble型への変換を回避し、足し算、引き算、掛け算、割り算を正確に演算し、意図したとおりの結果が得られます。

演算精度が求められる場面でどのようにJavaScriptの弱点を克服するか、そのヒントをこの記事で学びましょう。


数値を10進数文字列を含むオブジェクトに変換する

この記事では、数値を10進数文字列(decimal)、小数点位置(point)、符号(sign)からなるオブジェクトに変換します。
正の整数:12345 => { decimal: '12345', point: 0, sign: '' }
負の整数:-67890 => { decimal: '67890', point: 0, sign: '-' }
正の小数:3.14195265359 => { decimal: 314159265359, point: 11, sign: '' }
負の小数:-0.141421356 => { decimal: 141421356, point: 9, sign: '-' }

オブジェクトに変換変換することで、以下の利点があります。
1. 精度の完全なコントロール
数値ではなく文字列として数値を保持することで、JavaScriptの浮動小数点の誤差(例:0.1 + 0.2 = 0.30000000000000004)を完全に回避できます。
また小数点位置により、演算精度を細かく制御できるようになります。

2. 演算ロジックの独立性・再利用性 オブジェクトに変換することで、足し算、引き算、掛け算、割り算などの演算を一貫したロジックで実装できます。
小数点位置および符号の情報が明確に分かれていることで、小数点の位取り、桁揃え(ゼロパディング)、負数処理(符号反転)の処理が簡単になります。
本記事のJavaScriptの例では、引き算、掛け算、割り算の演算は、10進数文字列による足し算に集約されるため、プロブラムの保守性が大きく向上します。

本記事のJavaScriptの例は、複雑になりがちな計算を、一貫したロジックによりできるだけわかりやすく記載しました。


使用例

const ds = DecimalString();

// 引数が数値の場合
// 返り値は文字列となります。
console.log(ds.add(12345, 67890));    // '80235'
console.log(ds.subtract(12345, 67890));    // '-55545'
console.log(ds.multiply(12345, 67890));    // '838102050'
console.log(ds.divide(12345, 67890));   // '0.18183826778612461335'

// 引数が10進数文字列の場合
// JavaScriptの数値の最大値 9007199254740991を超える数値が扱えます。
console.log(ds.add('12345123451234512345', '67890678906789067890'));    // '80235802358023580235'
console.log(ds.subtract('12345123451234512345', '67890678906789067890'));    // '-55545555455554555545'
console.log(ds.multiply('12345123451234512345', '67890678906789067890'));    // '838118812292433967433343229121248102050'
console.log(ds.divide('12345123451234512345', '67890678906789067890'));    // '0.18183826778612461335'

// 小数点位置または有効数字桁数の指定
// 割り算では、3番目の引数により、小数点位置または有効数字桁数の指定が可能です。
// デフォルトは20桁となります。
// 整数部がある場合、小数点位置を指定
// 整数部がない場合、有効数字桁数を指定
console.log(ds.divide(67890, 12345, 5));    // '5.49939'    整数部があるため、小数点以下第6位で四捨五入され、小数点以下5桁の文字列を返す
console.log(ds.divide(123, 67890, 5));    // '0.0018118'    整数部がないため、有効数字6桁目で四捨五入され、有効数字5桁の文字列を返す
console.log(ds.divide(100, 10000, 5));    // '0.01'    有効数字5桁を指定していますが、小数点以下末尾のゼロを削除した文字列を返す

ソース

'use strict'
// @ts-check

/**
 * 数値を10進数文字列、小数点位置および符号に分解したオブジェクト
 * @typedef {Object} DecimalString
 * @property {string} decimal 10進数文字列
 * @property {number} point 小数点位置
 * @property {string} sign 符号
 */

function DecimalString() {
  const object = {
    //************************************************************
    // 1. 主要なメソッド
    // 1-1. 数値または文字列からDecimalStringオブジェクトに変換
    // 1-2. DecimalStringオブジェクトから文字列に変換
    // 1-3. 足し算
    // 1-4. 引き算
    // 1-5. 掛け算
    // 1-6. 割り算
    //************************************************************
    /**
     * 1-1. 数値または文字列からDecimalStringオブジェクトに変換
     * @param {number|string} value
     * @return {DecimalString}
     */
    toObject(value) {
      let string = '';
      // 数値の場合、文字列に変換
      if (typeof value !== 'string') {
        string = value.toString();
      } else {
        string = value;
      }

      // 文字列の先頭に+が付いている場合、除去
      string = string.replace(/^[\x2b]+/, '');
      // 2進数、8進数、16進数の整数リテラルによる文字列の場合、10進数の文字列に変換
      if (/^(?:0b[01]+|0o[0-7]+|0x[0-9A-Fa-f]+)$/.test(string)) {
        string = parseInt(string).toString();
      }
      // 文字列の両端からホワイトスペースを除去
      string = string.trim();

      // Infinityの場合
      if (/^[\x2d]?Infinity$/.test(string)) {
        const result = /^([\x2d]?)Infinity$/.exec(string);
        return { decimal: 'Infinity', point: 0, sign: result ? result[1] === '-' ? '-' : '' : '' };
      }

      // 浮動小数点リテラルの指数表記の場合
      if (/^[\x2d]?[\x2e0-9]+[Ee][\x2b\x2d]?[0-9]+$/.test(string)) {
        const result = /^([\x2d]?)([\x2e0-9]+)[Ee]([\x2b\x2d]?)([0-9]+)$/.exec(string);
        // 仮数部符号
        const sign1 = result ? result[1] === '-' ? '-' : '' : '';
        // 仮数部
        const significand = result ? result[2] : '';
        // 仮数部の整数部
        let n = significand.split('.')[0] ? significand.split('.')[0] : '';
        // 仮数部の小数部
        let p = significand.split('.')[1] ? significand.split('.')[1] : '';
        // 指数部符号
        const sign2 = result ? result[3] === '-' ? '-' : '' : '';
        // 指数部
        const exponent = result ? parseInt(result[4]) : 0;
        // 整数部の先頭が0の場合、除去
        n = n.replace(/^[0]+/, '');
        // 指数部符号が-の場合
        if (sign2 === '-') {
          // 小数部の末尾が0の場合、除去
          p = p.replace(/[0]+$/, '');
          // 0 の場合
          if (p === '' && n === '') {
            return { decimal: '0', point: 0, sign: '' };
          }
          // オブジェクトにして、返す
          return { decimal: n + p, point: p.length + exponent, sign: sign1 };
        }
        // point を計算
        let point = p.length - exponent > 0 ? p.length - exponent : 0;
        // 小数部に0を追加
        p = point > 0 ? p : p + '0'.repeat(exponent - p.length);
        // 0 の場合
        if (p === '' && n === '') {
          return { decimal: '0', point: 0, sign: '' };
        }
        // オブジェクトにして、返す
        return { decimal: n + p, point, sign: sign1 };
      }
      // 浮動小数点リテラルの指数表記でない場合
      const result = /^([\x2d]?)([\x2e0-9]+)$/.exec(string);
      // 符号
      const sign = result ? result[1] === '-' ? '-' : '' : '';
      // 数字部
      const significand = result ? result[2] : '';
      // 数字部の整数部
      let n = significand.split('.')[0] ? significand.split('.')[0] : '';
      // 数字部の小数部
      let p = significand.split('.')[1] ? significand.split('.')[1] : '';
      // 整数部の先頭が0の場合、除去
      n = n.replace(/^[0]+/, '');
      // 小数部の末尾が0の場合、除去
      p = p.replace(/[0]+$/, '');
      // 0 の場合
      if (p === '' && n === '') {
        return { decimal: '0', point: 0, sign: '' };
      }
      // 小数点位置を取得
      let point = p.length;
      // 整数部が0、小数部の先頭が0の場合、除去
      if (p === '') {
        // 整数部の先頭が0の場合、除去
        p = p.replace(/^[0]+/, '');
      }
      // オブジェクトにして、返す
      return { decimal: n + p, point, sign };
    },
    /**
     * 1-2. DecimalStringオブジェクトから文字列に変換
     * @param {DecimalString} object
     * @return {string}
     */
    objectToString(object) {
      // pointが0の場合、stringをそのまま返す
      if (object.point === 0) {
        return object.sign + object.decimal;
      }

      // 先頭が0の場合、除去
      object.decimal = object.decimal.replace(/^[0]+/, '');
      // pointの値に合わせて、先頭に0を追加
      if (object.decimal.length < object.point + 1) {
        object.decimal = '0'.repeat(object.point + 1 - object.decimal.length) + object.decimal;
      }
      // 整数部
      let n = object.decimal.slice(0, -1 * object.point);
      // 小数部
      let p = object.decimal.slice(-1 * object.point);
      // 小数部の末尾が0の場合、除去
      p = p.replace(/[0]+$/, '');
      // 0の場合
      if ((n === '0' || n === '') && p === '') {
        return '0';
      }
      // 文字列を返す
      return object.sign + n + (p !== '' ? '.' : '') + p;
    },
    /**
     * 1-3. 足し算
     * @param {number|string} inputA
     * @param {number|string} inputB
     * @return {NaN|string}
     */
    add(inputA = '0', inputB = '0') {
      // a, bをDecimalStringオブジェクトに変換
      const a = this.toObject(inputA);
      const b = this.toObject(inputB);

      // 計算が必要ない場合
      if ((a.decimal === 'Infinity' && b.decimal === 'Infinity') && ((a.sign === '' && b.sign === '-') || (a.sign === '-' && b.sign === ''))) {
        return NaN;
      } else if (a.decimal === '0' && b.decimal === '0') {
        return '0';
      } else if (a.decimal === '0') {
        return this.objectToString(b);
      } else if (b.decimal === '0') {
        return this.objectToString(a);
      } else if (a.decimal === 'Infinity') {
        return a.sign + 'Infinity';
      } else if (b.decimal === 'Infinity') {
        return b.sign + 'Infinity';
      } if (isNaN(parseInt(a.decimal)) || isNaN(parseInt(b.decimal))) {
        return NaN;
      }

      // aの符号が正、bの符号が負の場合、引き算
      if (a.sign === '' && b.sign === '-') {
        return this.subtract(this.objectToString(a), this.objectToString(b));
      }
      // aの符号が負、bの符号が正の場合、引き算
      if (a.sign === '-' && b.sign === '') {
        return this.subtract(this.objectToString(b), this.objectToString(a));
      }

      // DecimalObjectによる足し算
      // 文字列にして返す
      return this.objectToString(this.addObjects(a, b));
    },
    /**
     * 1-4. 引き算
     * @param {number|string} inputA
     * @param {number|string} inputB
     * @return {NaN|string}
     */
    subtract(inputA = '0', inputB = '0') {
      // a, bをDecimalStringオブジェクトに変換
      const a = this.toObject(inputA);
      const b = this.toObject(inputB);

      // 計算が必要ない場合
      if ((a.decimal === 'Infinity' && b.decimal === 'Infinity') && ((a.sign === '' && b.sign === '') || (a.sign === '-' && b.sign === '-'))) {
        return NaN;
      } else if (a.decimal === '0' && b.decimal === '0') {
        return '0';
      } else if (a.decimal === '0') {
        return this.objectToString({ decimal: b.decimal, point:b.point, sign: b.sign === '' ? '-' : '' });
      } else if (b.decimal === '0') {
        return this.objectToString(a);
      } else if (a.decimal === 'Infinity') {
        return a.sign + 'Infinity';
      } else if (b.decimal === 'Infinity') {
        return (b.sign === '-' ? '' : '-') + 'Infinity';
      } else if (isNaN(parseInt(a.decimal)) || isNaN(parseInt(b.decimal))) {
        return NaN;
      }

      // aの符号が正、bの符号が負の場合、bの符号を変えて足し算へ
      if (a.sign === '' && b.sign === '-') {
        b.sign = '';
        return this.add(this.objectToString(a), this.objectToString(b));
      }
      // aの符号が負、bの符号が正の場合、bの符号を変えて足し算へ
      if (a.sign === '-' && b.sign === '') {
        b.sign = '-';
        return this.add(this.objectToString(a), this.objectToString(b));
      }

      // DecimalObjectによる引き算
      // 文字列にして返す
      return this.objectToString(this.subtractObjects(a, b));
    },
    /**
     * 1-5. 掛け算
     * @param {number|string} inputA
     * @param {number|string} inputB
     * @return {NaN|string}
     */
    multiply(inputA = '0', inputB = '1') {
      // a, bをDecimalStringオブジェクトに変換
      const a = this.toObject(inputA);
      const b = this.toObject(inputB);

      // 計算が必要ない場合
      if ((a.decimal === '0' && b.decimal === 'Infinity') || (a.decimal === 'Infinity' && b.decimal === '0')) {
        return NaN;
      } else if (a.decimal === '0' || b.decimal === '0') {
        return '0';
      } else if (a.decimal === '1') {
        return this.objectToString({ decimal: b.decimal, point: b.point, sign: a.sign === '' ? b.sign : b.sign === '' ? '-' : '' });
      } else if (b.decimal === '1') {
        return this.objectToString({ decimal: a.decimal, point: a.point, sign: b.sign === '' ? a.sign : a.sign === '' ? '-' : '' });
      } else if ((a.decimal === 'Infinity' || b.decimal === 'Infinity') && ((a.sign === '' && b.sign === '') || (a.sign === '-' && b.sign === '-'))) {
        return 'Infinity';
      } else if ((a.decimal === 'Infinity' || b.decimal === 'Infinity') && ((a.sign === '' && b.sign === '-') || (a.sign === '-' && b.sign === ''))) {
        return '-Infinity';
      } if (isNaN(parseInt(a.decimal)) || isNaN(parseInt(b.decimal))) {
        return NaN;
      }

      // DecimalObjectによる掛け算
      // 文字列にして返す
      return this.objectToString(this.multiplyObjects(a, b));
    },
    /**
     * 1-6. 割り算
     * @param {number|string} inputA
     * @param {number|string} inputB
     * @param {number} d
     * @return {NaN|string}
     */
    divide(inputA = '0', inputB = '1', d = 20) {
      // a, bをDecimalStringオブジェクトに変換
      const a = this.toObject(inputA);
      const b = this.toObject(inputB);

      // 計算が必要ない場合
      if ((a.decimal === '0' && b.decimal === '0') || (a.decimal === 'Infinity' && b.decimal === 'Infinity')) {
        return NaN;
      } else if (a.decimal === '0') {
        return '0';
      } else if (b.decimal === '1') {
        return this.objectToString({ decimal: a.decimal, point: a.point, sign: b.sign === '' ? a.sign : a.sign === '' ? '-' : '' });
      } else if ((a.decimal === 'Infinity ' || b.decimal === '0') && ((a.sign === '' && b.sign === '') || (a.sign === '-' && b.sign === '-'))) {
        return 'Infinity';
      } else if ((a.decimal === 'Infinity ' || b.decimal === '0') && ((a.sign === '' && b.sign === '-') || (a.sign === '-' && b.sign === ''))) {
        return '-Infinity';
      } else if (b.decimal === 'Infinity') {
        return '0';
      } if (isNaN(parseInt(a.decimal)) || isNaN(parseInt(b.decimal))) {
        return NaN;
      }

      // 小数点位置または有効数字桁数
      if (d < 0) {
        d = 0;
      } else {
        d = Math.floor(d);
      }

      // DecimalObjectによる割り算
      // 文字列にして返す
      return this.objectToString(this.divideObjects(a, b, d));
    },

    //************************************************************
    // 2. DecimalStringオブジェクトによる四則演算
    // 2-1. DecimalStringオブジェクトによる足し算
    // 2-2. DecimalStringオブジェクトによる引き算
    // 2-3. DecimalStringオブジェクトによる掛け算
    // 2-4. DecimalStringオブジェクトによる割り算
    //************************************************************
    /**
     *2-1. DecimalStringオブジェクトによる足し算
     * @param {DecimalString} a
     * @param {DecimalString} b
     * @return {DecimalString}
     */
    addObjects(a = { decimal: '0', point: 0, sign: '' }, b = { decimal: '0', point: 0, sign: '' }) {
      // 符号
      let sign = '';
      // aおよびbの符号が負の場合、符号を負にする
      if (a.sign === '-' && b.sign === '-') {
        sign = '-';
      }

      // 計算時の桁数はpointの大きい方
      const digitLength = Math.max(a.point, b.point);
      // 末尾に0を追加
      a.decimal += '0'.repeat(digitLength - a.point);
      b.decimal += '0'.repeat(digitLength - b.point);

      // 10進数文字列による足し算
      const decimal = this.addDecimalStrings(a.decimal, b.decimal);
      // DecimalStringオブジェクトで返す
      return { decimal, point: digitLength, sign }
    },
    /**
     *2-2. DecimalStringオブジェクトによる引き算
     * @param {DecimalString} a
     * @param {DecimalString} b
     * @return {DecimalString}
     */
    subtractObjects(a = { decimal: '0', point: 0, sign: '' }, b = { decimal: '0', point: 0, sign: '' }) {
      // 符号
      let sign = '';

      // 計算時の桁数はpointの大きい方
      const digitLength = Math.max(a.point, b.point);
      // 末尾に0を追加
      a.decimal += '0'.repeat(digitLength - a.point);
      b.decimal += '0'.repeat(digitLength - b.point);

      // 10進数文字列による引き算
      const result = this.subtractDecimalStrings(a.decimal, b.decimal);

      // 引き算の結果が負ならびにaおよびbの符号が正もしくは負の場合、符号を負にする
      if ((a.sign === '' && b.sign === '' && result.sign === '-') || (a.sign === '-' && b.sign === '-' && result.sign === '')) {
        sign = '-';
      }

      // DecimalStringオブジェクトで返す
      return { decimal: result.decimal, point: digitLength, sign };
    },
    /**
     *2-3. DecimalStringオブジェクトによる掛け算
     * @param {DecimalString} a
     * @param {DecimalString} b
     * @return {DecimalString}
     */
    multiplyObjects(a = { decimal: '0', point: 0, sign: '' }, b = { decimal: '1', point: 0, sign: '' }) {
      // 符号
      let sign = '';
      // aの符号が正、bの符号が負の場合、またはaの符号が負、bの符号が正の場合、符号に-をセット
      if ((a.sign === '' && b.sign === '-') || (a.sign === '-' && b.sign === '')) {
        sign = '-';
      }

      // 計算時の桁数はpointを足す
      const digitLength = a.point + b.point;
      // 10進数文字列による掛け算
      const decimal = this.multiplyDecimalStrings(a.decimal, b.decimal);

      // DecimalStringオブジェクトで返す
      return { decimal, point: digitLength, sign };
    },
    /**
     *2-4. DecimalStringオブジェクトによる割り算
     * @param {DecimalString} a
     * @param {DecimalString} b
     * @param {number} d
     * @return {DecimalString}
     */
    divideObjects(a = { decimal: '0', point: 0, sign: '' }, b = { decimal: '1', point: 0, sign: '' }, d = 20) {
      // 符号
      let sign = '';
      // aの符号が正、bの符号が負の場合、またはaの符号が負、bの符号が正の場合、符号に-をセット
      if ((a.sign === '' && b.sign === '-') || (a.sign === '-' && b.sign === '')) {
        sign = '-';
      }

      // 計算時の桁数は桁数の大きい方、pointの大きい方、小数点位置|有効数字の桁数、四捨五入用の1桁を足した数
      const digitLength = Math.max(a.decimal.length, b.decimal.length) + Math.max(a.point, b.point) + d + 1;
      // 末尾に0を追加
      a.decimal += '0'.repeat(digitLength - a.point);

      // 10進数文字列による割り算
      const decimal = this.divideDecimalStrings(a.decimal, b.decimal);
      // 小数部の桁数
      let pDigitLength = digitLength - b.point;
      // 整数部の桁数
      let nDigitLength = decimal.length - digitLength + b.point;

      // 四捨五入する桁を決定
      let point = 0;
      let roundDigit = 0;
      if (nDigitLength >= 0) {
        // 小数点位置d + 1桁で四捨五入
        point = d;
        roundDigit = nDigitLength + d + 1;
      } else {
        // 有効数字d + 1桁で四捨五入
        point = -1 * nDigitLength + d;
        roundDigit = d + 1;
      }

      // 小数点位置|有効数字の桁数で四捨五入
      if (decimal[roundDigit - 1] >= '5') {
        // 切り上げの場合
        // 10進数文字列に1を足す
        // DecimalStringオブジェクトで返す
        return { decimal: this.addOneToDecimalString(decimal.slice(0, roundDigit - 1)), point, sign };
      }
      // 切り捨ての場合
      // DecimalStringオブジェクトで返す
      return { decimal: decimal.slice(0, roundDigit - 1), point, sign };
    },

    //************************************************************
    // 3. 10進数文字列による四則演算
    // 3-1. 1桁同士の10進数文字列の足し算の結果をオブジェクトとして管理
    // 3-2. 1桁の10進数文字列のインバート結果をオブジェクトとして管理
    // 3-3. 10進数文字列による足し算
    // 3-4. 10進数文字列に1を足す
    // 3-5. 10進数文字列による引き算
    // 3-6. 10進数文字列による掛け算
    // 3-7. 10進数文字列による割り算
    // 3-8. 10進数文字列の比較
    //************************************************************
    // 3-1. 1桁同士の10進数文字列の足し算の結果をオブジェクトとして管理
    // 足し算で使用
    // this.addOneDigitDecimalStrings[a][b][carry]の形で1の位と10の位の値を計算せずに取得
    addOneDigitDecimalStrings: {
      '0': { '0': { '': { carry: '', result: '0' }, '1': { carry: '', result: '1' } }, '1': { '': { carry: '', result: '1' }, '1': { carry: '', result: '2' } }, '2': { '': { carry: '', result: '2' }, '1': { carry: '', result: '3' } }, '3': { '': { carry: '', result: '3' }, '1': { carry: '', result: '4' } }, '4': { '': { carry: '', result: '4' }, '1': { carry: '', result: '5' } }, '5': { '': { carry: '', result: '5' }, '1': { carry: '', result: '6' } }, '6': { '': { carry: '', result: '6' }, '1': { carry: '', result: '7' } }, '7': { '': { carry: '', result: '7' }, '1': { carry: '', result: '8' } }, '8': { '': { carry: '', result: '8' }, '1': { carry: '', result: '9' } }, '9': { '': { carry: '', result: '9' }, '1': { carry: '1', result: '0' } } },
      '1': { '0': { '': { carry: '', result: '1' }, '1': { carry: '', result: '2' } }, '1': { '': { carry: '', result: '2' }, '1': { carry: '', result: '3' } }, '2': { '': { carry: '', result: '3' }, '1': { carry: '', result: '4' } }, '3': { '': { carry: '', result: '4' }, '1': { carry: '', result: '5' } }, '4': { '': { carry: '', result: '5' }, '1': { carry: '', result: '6' } }, '5': { '': { carry: '', result: '6' }, '1': { carry: '', result: '7' } }, '6': { '': { carry: '', result: '7' }, '1': { carry: '', result: '8' } }, '7': { '': { carry: '', result: '8' }, '1': { carry: '', result: '9' } }, '8': { '': { carry: '', result: '9' }, '1': { carry: '1', result: '0' } }, '9': { '': { carry: '1', result: '0' }, '1': { carry: '1', result: '1' } } },
      '2': { '0': { '': { carry: '', result: '2' }, '1': { carry: '', result: '3' } }, '1': { '': { carry: '', result: '3' }, '1': { carry: '', result: '4' } }, '2': { '': { carry: '', result: '4' }, '1': { carry: '', result: '5' } }, '3': { '': { carry: '', result: '5' }, '1': { carry: '', result: '6' } }, '4': { '': { carry: '', result: '6' }, '1': { carry: '', result: '7' } }, '5': { '': { carry: '', result: '7' }, '1': { carry: '', result: '8' } }, '6': { '': { carry: '', result: '8' }, '1': { carry: '', result: '9' } }, '7': { '': { carry: '', result: '9' }, '1': { carry: '1', result: '0' } }, '8': { '': { carry: '1', result: '0' }, '1': { carry: '1', result: '1' } }, '9': { '': { carry: '1', result: '1' }, '1': { carry: '1', result: '2' } } },
      '3': { '0': { '': { carry: '', result: '3' }, '1': { carry: '', result: '4' } }, '1': { '': { carry: '', result: '4' }, '1': { carry: '', result: '5' } }, '2': { '': { carry: '', result: '5' }, '1': { carry: '', result: '6' } }, '3': { '': { carry: '', result: '6' }, '1': { carry: '', result: '7' } }, '4': { '': { carry: '', result: '7' }, '1': { carry: '', result: '8' } }, '5': { '': { carry: '', result: '8' }, '1': { carry: '', result: '9' } }, '6': { '': { carry: '', result: '9' }, '1': { carry: '1', result: '0' } }, '7': { '': { carry: '1', result: '0' }, '1': { carry: '1', result: '1' } }, '8': { '': { carry: '1', result: '1' }, '1': { carry: '1', result: '2' } }, '9': { '': { carry: '1', result: '2' }, '1': { carry: '1', result: '3' } } },
      '4': { '0': { '': { carry: '', result: '4' }, '1': { carry: '', result: '5' } }, '1': { '': { carry: '', result: '5' }, '1': { carry: '', result: '6' } }, '2': { '': { carry: '', result: '6' }, '1': { carry: '', result: '7' } }, '3': { '': { carry: '', result: '7' }, '1': { carry: '', result: '8' } }, '4': { '': { carry: '', result: '8' }, '1': { carry: '', result: '9' } }, '5': { '': { carry: '', result: '9' }, '1': { carry: '1', result: '0' } }, '6': { '': { carry: '1', result: '0' }, '1': { carry: '1', result: '1' } }, '7': { '': { carry: '1', result: '1' }, '1': { carry: '1', result: '2' } }, '8': { '': { carry: '1', result: '2' }, '1': { carry: '1', result: '3' } }, '9': { '': { carry: '1', result: '3' }, '1': { carry: '1', result: '4' } } },
      '5': { '0': { '': { carry: '', result: '5' }, '1': { carry: '', result: '6' } }, '1': { '': { carry: '', result: '6' }, '1': { carry: '', result: '7' } }, '2': { '': { carry: '', result: '7' }, '1': { carry: '', result: '8' } }, '3': { '': { carry: '', result: '8' }, '1': { carry: '', result: '9' } }, '4': { '': { carry: '', result: '9' }, '1': { carry: '1', result: '0' } }, '5': { '': { carry: '1', result: '0' }, '1': { carry: '1', result: '1' } }, '6': { '': { carry: '1', result: '1' }, '1': { carry: '1', result: '2' } }, '7': { '': { carry: '1', result: '2' }, '1': { carry: '1', result: '3' } }, '8': { '': { carry: '1', result: '3' }, '1': { carry: '1', result: '4' } }, '9': { '': { carry: '1', result: '4' }, '1': { carry: '1', result: '5' } } },
      '6': { '0': { '': { carry: '', result: '6' }, '1': { carry: '', result: '7' } }, '1': { '': { carry: '', result: '7' }, '1': { carry: '', result: '8' } }, '2': { '': { carry: '', result: '8' }, '1': { carry: '', result: '9' } }, '3': { '': { carry: '', result: '9' }, '1': { carry: '1', result: '0' } }, '4': { '': { carry: '1', result: '0' }, '1': { carry: '1', result: '1' } }, '5': { '': { carry: '1', result: '1' }, '1': { carry: '1', result: '2' } }, '6': { '': { carry: '1', result: '2' }, '1': { carry: '1', result: '3' } }, '7': { '': { carry: '1', result: '3' }, '1': { carry: '1', result: '4' } }, '8': { '': { carry: '1', result: '4' }, '1': { carry: '1', result: '5' } }, '9': { '': { carry: '1', result: '5' }, '1': { carry: '1', result: '6' } } },
      '7': { '0': { '': { carry: '', result: '7' }, '1': { carry: '', result: '8' } }, '1': { '': { carry: '', result: '8' }, '1': { carry: '', result: '9' } }, '2': { '': { carry: '', result: '9' }, '1': { carry: '1', result: '0' } }, '3': { '': { carry: '1', result: '0' }, '1': { carry: '1', result: '1' } }, '4': { '': { carry: '1', result: '1' }, '1': { carry: '1', result: '2' } }, '5': { '': { carry: '1', result: '2' }, '1': { carry: '1', result: '3' } }, '6': { '': { carry: '1', result: '3' }, '1': { carry: '1', result: '4' } }, '7': { '': { carry: '1', result: '4' }, '1': { carry: '1', result: '5' } }, '8': { '': { carry: '1', result: '5' }, '1': { carry: '1', result: '6' } }, '9': { '': { carry: '1', result: '6' }, '1': { carry: '1', result: '7' } } },
      '8': { '0': { '': { carry: '', result: '8' }, '1': { carry: '', result: '9' } }, '1': { '': { carry: '', result: '9' }, '1': { carry: '1', result: '0' } }, '2': { '': { carry: '1', result: '0' }, '1': { carry: '1', result: '1' } }, '3': { '': { carry: '1', result: '1' }, '1': { carry: '1', result: '2' } }, '4': { '': { carry: '1', result: '2' }, '1': { carry: '1', result: '3' } }, '5': { '': { carry: '1', result: '3' }, '1': { carry: '1', result: '4' } }, '6': { '': { carry: '1', result: '4' }, '1': { carry: '1', result: '5' } }, '7': { '': { carry: '1', result: '5' }, '1': { carry: '1', result: '6' } }, '8': { '': { carry: '1', result: '6' }, '1': { carry: '1', result: '7' } }, '9': { '': { carry: '1', result: '7' }, '1': { carry: '1', result: '8' } } },
      '9': { '0': { '': { carry: '', result: '9' }, '1': { carry: '1', result: '0' } }, '1': { '': { carry: '1', result: '0' }, '1': { carry: '1', result: '1' } }, '2': { '': { carry: '1', result: '1' }, '1': { carry: '1', result: '2' } }, '3': { '': { carry: '1', result: '2' }, '1': { carry: '1', result: '3' } }, '4': { '': { carry: '1', result: '3' }, '1': { carry: '1', result: '4' } }, '5': { '': { carry: '1', result: '4' }, '1': { carry: '1', result: '5' } }, '6': { '': { carry: '1', result: '5' }, '1': { carry: '1', result: '6' } }, '7': { '': { carry: '1', result: '6' }, '1': { carry: '1', result: '7' } }, '8': { '': { carry: '1', result: '7' }, '1': { carry: '1', result: '8' } }, '9': { '': { carry: '1', result: '8' }, '1': { carry: '1', result: '9' } } }
    },
    // 3-2. 1桁の10進数文字列のインバート結果をオブジェクトとして管理
    // 引き算で使用
    invertOneDigitDecimalString: { '0': '9', '1': '8', '2': '7', '3': '6', '4': '5', '5': '4', '6': '3', '7': '2', '8': '1', '9': '0' },
    /**
     *3-3. 10進数文字列による足し算
     * @param {string} a
     * @param {string} b
     * @return {string}
     */
    addDecimalStrings(a = '0', b = '0') {
      let result = '';
      let sum = { carry: '', result: '' };
      // 文字列の長さを揃える
      let digitLength = Math.max(a.length, b.length);
      a = '0'.repeat(digitLength - a.length) + a;
      b = '0'.repeat(digitLength - b.length) + b;
      // 右から左へ桁ごとに足し算
      for (let i = digitLength - 1; i >=0; i --) {
        // 1桁同士の10進数文字列の足し算
        // 結果はオブジェクトから取得
        sum = this.addOneDigitDecimalStrings[a[i]][b[i]][sum.carry];
        // 和に結果を追加
        result = sum.result + result;
      }
      // 結果を返す
      return sum.carry + result;
    },
    /**
     * 3-4. 10進数文字列に1を足す
     * @param {string} a
     * @return {string}
     */
    addOneToDecimalString(a = '0') {
      let result = '';
      let sum = { carry: '', result: '' };
      // 右から左へ桁ごとに足し算
      for (let fi = a.length - 1, i = fi; i >=0; i --) {
        if (i === fi) {
          // 1桁同士の10進数文字列の足し算
          // 結果はオブジェクトから取得
          sum = this.addOneDigitDecimalStrings[a[i]]['1'][''];
          result = sum.result;
        } else {
          // 1桁同士の10進数文字列の足し算
          // 結果はオブジェクトから取得
          sum = this.addOneDigitDecimalStrings[a[i]]['0'][sum.carry];
          // 和に結果を追加
          result = sum.result + result;
        }
        if (sum.carry === '') {
          result = a.slice(0, i) + result;
          break;
        }
      }
      // 結果を返す
      return sum.carry + result;
    },
    /**
     * 3-5. 10進数文字列による引き算
     * @param {string} a
     * @param {string} b
     * @return {DecimalString}
     */
    subtractDecimalStrings(a = '0', b = '0') {
      // 文字列の長さを揃える
      // 桁の長さ、桁の先頭に符号用の1桁分を加える
      const digitLength = Math.max(a.length, b.length) + 1;
      a = '0'.repeat(digitLength - a.length) + a;
      b = '0'.repeat(digitLength - b.length) + b;
      // bを符号無し整数から負の数の符号付き整数に変換
      // 1. 桁を反転
      let invertedB = '';
      for (let i = 0; i < b.length; i ++) {
        // インバートの値はオブジェクトから取得
        invertedB += this.invertOneDigitDecimalString[b[i]];
      }
      // 2. +1する
      const negativeB = this.addOneToDecimalString(invertedB);
      // 足し算、さらに足し算後、桁の長さ分だけ取得
      let diff = this.addDecimalStrings(a, negativeB).slice(-1 * digitLength);
      // 判定:最上位桁が9の場合、負の数
      if (diff[0] === '9') {
        // 負の数の場合、負の数の符号付き整数から符号無し整数に変換
        // 1. 桁を反転
        let invertedDiff = '';
        for (let i = 0; i < diff.length; i ++) {
          // インバートの値はオブジェクトから取得
          invertedDiff += this.invertOneDigitDecimalString[diff[i]];
        }
        // 2. +1する
        let decimal = this.addOneToDecimalString(invertedDiff);
        // 先頭の0を削除する
        // 結果をDecimalStringオブジェクトにして返す(符号は負となる)
        return { decimal: decimal.replace(/^[0]+/, ''), point: 0, sign: '-' };
      }
      // 先頭の0を削除する
      // 結果をDecimalStringオブジェクトにして返す
      return { decimal: diff.replace(/^[0]+/, '') || '0', point: 0, sign: '' };
    },
    /**
     * 3-6. 10進数文字列による掛け算
     * @param {string} a
     * @param {string} b
     * @return {string}
     */
    multiplyDecimalStrings(a = '0', b = '1') {
      let result = '';
      // a × 1 ~ a × 9の値をあらかじめ計算
      let prod = [];
      prod['1'] = a;
      for (let i = 2; i <= 9; i ++) {
        prod[i.toString()] = this.addDecimalStrings(prod[(i - 1).toString()], a);
      }
      // bの左の桁から計算
      for (let i = 0; i < b.length; i ++) {
        // b[i]が0の場合、計算不要
        if (b[i] === '0') {
          continue;
        }
        // a × b[i]にbの右からの桁分0を追加
        const shiftedProd = prod[b[i]] + '0'.repeat(b.length - 1 - i);
        // 10進数文字列による足し算
        result = this.addDecimalStrings(result, shiftedProd);
      }
      // 結果を返す
      return result;
    },
    /**
     * 3-7. 10進数文字列による割り算
     * @param {string} a
     * @param {string} b
     * @return {string}
     */
    divideDecimalStrings(a = '0', b = '1') {
      let quot = '';
      let mod = '';
      // 先頭の0を削除
      b = b.replace(/^[0]+/, '');
      // b × 1 ~ b × 9の値をあらかじめ計算
      let prod = [];
      prod[1] = b;
      for (let i = 2; i <= 9; i ++) {
        prod[i] = this.addDecimalStrings(prod[i - 1], b);
      }
      // aの左の桁から計算
      for (let i = 0; i < a.length; i ++) {
        mod += a[i];
        mod = mod.replace(/^[0]+/, '');
        // 余りの桁数がbの桁数より短い場合、商に0を追加し、次の桁へ
        if (mod.length < b.length) {
          quot += '0';
          continue;
        }
        for (let j = 9; j >= 0; j --) {
          // jが0の場合、商に0を追加、計算不要
          if (j === 0) {
            quot += '0';
            break;
          }
          // b × jを取得
          const prodB = prod[j];
          // 余りとb × jの大きさを比較
          const compare = this.compareDecimalStrings(mod, prodB);
          // 余りとb × jが一致する場合、計算不要
          if (compare === 0) {
            quot += j.toString();
            mod = '';
            break;
          }
          // 余りがb × jより大きい場合、10進数文字列による引き算
          if (compare === 1) {
            // 商にjを追加
            quot += j.toString();
            // 余りからb × jを引く
            const { decimal, point, sign } = this.subtractDecimalStrings(mod, prodB);
            // 差が余りとなる
            mod = decimal;
            break;
          }
        }
      }
      // 商を返す
      return quot.replace(/^[0]+/, '') || '0';
    },
    /**
     * 3-8. 10進数文字列の比較
     * @param {string} a
     * @param {string} b
     * @return {number}
     */
    compareDecimalStrings(a = '0', b = '0') {
      // 先頭の0を削除
      a = a.replace(/^[0]+/, '');
      b = b.replace(/^[0]+/, '');

      // 桁数の比較
      if (a.length < b.length) {
        return -1;
      }
      if (a.length > b.length) {
        return 1;
      }
      // 桁数が同じ場合、各桁ごとに比較
      // 左の桁から比較
      for (let i = 0; i < a.length; i ++) {
        if (a[i] < b[i]) {
          return -1;
        }
        if (a[i] > b[i]) {
          return 1;
        }
      }
      // 完全一致
      return 0;
    }
  };
  return object;
}

関連情報


コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です