renrenche-fe / everything-about-front-end

探讨所有前端相关的知识点
MIT License
10 stars 0 forks source link

javascript 实现精度运算 #8

Open sunhengzhe opened 7 years ago

sunhengzhe commented 7 years ago

用 js 实现高精度运算,这个问题是老生常谈了。如果真的要前端做一个高精度运算(比如钱),还是要注意这个问题的。

function add (numLeft, numRight){
    // todo
}

ex: add(0.1, 0.2) => 0.3

更一般地,实现四则运算的精度运算

function operation (numLeft, numRight, character){
    // todo
}

ex: operation(0.1, 0.2, '+') => 0.3
sunhengzhe commented 7 years ago

这个问题是在一个前端群里讨论的,放在 这里。后续可以去那里讨论

keer3 commented 7 years ago

感觉写得不好

function add() {

    const result = [...arguments].reduce((x, y) => {
        y = y.toString();

        // 兼容整数
        y = y.indexOf('.') == -1 ? y = y + '.0' : y;

        let [intX, floatX] = x.split('.');
        let [intY, floatY] = y.split('.');

        let floatRet = addStr(floatX, floatY);

        floatRet = floatRet.indexOf('.') != -1 ? floatRet.split('.') : (floatRet + '.0').split('.') ;
        let intRet = parseInt(intX) + parseInt(intY) + parseInt(floatRet[0]);

        return intRet + '.' + floatRet[1];
    }, '0.0');
    return result;
}

const addStr = (x, y) => {

    const xLength = x.length,
          yLength = y.length,
          maxLength = Math.max(xLength, yLength);

    // 填充长度
    x = x + Array(maxLength - xLength).fill(0).join('');
    y = y + Array(maxLength - yLength).fill(0).join('');

    let result = [],
          j = 0,
          flag = 0;
    for(let i = maxLength - 1; i >= 0; i--) {
        let r = 0;
        r = parseInt(x[i]) + parseInt(y[i]) + flag;
        flag = Math.floor(r / 10);
        r = r < 10 ? r : r % 10;
        result[j++] = r;
    }

    flag ? result.push(flag) : '';
    let ret = '';
    if (result.length - maxLength ===  0) {
        result.reverse().splice(result.length - maxLength, 0, '0.');
    } else {
        result.reverse().splice(result.length - maxLength, 0, '.');
    }
    return result.join('');

}
sunhengzhe commented 7 years ago

@sunshumin

x = x + Array(maxLength - xLength).fill(0).join('');

这里可以使用 es6 的 padEnd() 方法代替:x = x.padEnd(maxLength, 0)

sunhengzhe commented 7 years ago

@sunshumin

if (result.length - maxLength ===  0) {
result.reverse().splice(result.length - maxLength, 0, '0.');
} else {
result.reverse().splice(result.length - maxLength, 0, '.');
}

这里的判断实际上就是有无进位,我感觉这个方法返回 是否进位小数部分 两个东西比较好。然后在 add 方法里,根据你的做法是把整数部分加起来(其实这个地方可能溢出,最好是像处理小数一样处理成字符串)。把你代码简化了一下:

function add(/* number1, number2, number3, ... */) {
    const result = [...arguments].reduce((x, y) => {
        // ...
        const { isCarry, decimals } = addStr(floatX, floatY);

        const intRet = parseInt(intX) + parseInt(intY) + isCarry; // 自动转型

        return intRet + '.' + decimals;
    }, '0.0');
    return result;
}

const addStr = (x, y) => {
    //...
    flag ? result.push(flag) : '';

    const isCarry = result.length !== maxLength;

    if (isCarry) {
        result.pop();
    }

    return {
        isCarry,
        decimals: result.reverse().join('')
    };
}
sunhengzhe commented 7 years ago

还有两个精度有关的补充:

  1. 使用 ES6 引入的 Number.EPSILON
    x = 0.2;
    y = 0.3;
    z = 0.1;
    equal = (Math.abs(x - y + z) < Number.EPSILON);
  2. parseInt(0.0000008) === 8 结果为 true

因为 0.0000008 会先被转成 8e-7

ShaofeiZi commented 6 years ago

EMM 我又碰到了 不过是四舍五入 https://juejin.im/post/5ad5c104518825558002bdc0

ShaofeiZi commented 6 years ago

     * 四舍五入
     * @param number 要四舍五入的数字
     * @param precision 精度 保留小数点位数
     * @returns {*}
     */
    function round(number,precision) {
      const enlargeDigits = function enlargeDigits(times) {
        return function (number) {
          return +(String(number) + "e" + String(times));
        };
      };
      const toFixed = function toFixed(precision) {
        return function (number) {
          return number.toFixed(precision);
        };
      };
      const compose = function compose() {
        for (var _len = arguments.length, functions = Array(_len), _key = 0; _key < _len; _key++) {
          functions[_key] = arguments[_key];
        }

        var nonFunctionTypeLength = functions.filter(function (item) {
          return typeof item !== 'function';
        }).length;
        if (nonFunctionTypeLength > 0) {
          throw new Error("compose's params must be functions");
        }
        if (functions.length === 0) {
          return function (arg) {
            return arg;
          };
        }
        if (functions.length === 1) {
          return functions[0];
        }
        return functions.reduce(function (a, b) {
          return function () {
            return a(b.apply(undefined, arguments));
          };
        });
      };
      var precision = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 2;

      if (Number.isNaN(+number)) {
        throw new Error("number's type must be Number");
      }
      if (Number.isNaN(+precision)) {
        throw new Error("precision's type must be Number");
      }
      return compose(toFixed(precision), enlargeDigits(-precision), Math.round, enlargeDigits(precision))(number)
    }```