【JS】详解JS精度丢失原理以及业务中价格计算引入数学库 Math.js的使用场景

一、JS为什么会出现精度丢失问题

    1、JS基本数据类型 Number 在内存中是怎么存储的?

        JS中的Number类型使用的是双精度浮点型,也就是其他语言中的double类型。在计算机内存中,单精度数是用32个 bit 来存储的浮点数,双精度数是使用64个 bit 来存储的浮点数。其中有1位符号位 (+/-),11位表示指数位(次方数),52位表示数值位(精确度) 内存结构如下:

【JS】详解JS精度丢失原理以及业务中价格计算引入数学库 Math.js的使用场景_第1张图片

        在ES规范中规定e的范围在-1074 ~ 971,而m能表示的最大数是52个1,最小能表示的是1 。计算机中存储小数是先转换成二进制进行存储的,二进制的第一位有效数字必定是1,因此这个1不会被存储,可以节省一个存储位,因此尾数部分可以存储的范围是1 ~ 2^(52+1),也就是说Number能表示的最大数字绝对值范围是 2^-1074 ~ 2^(53+971) ->5e-324~1.7976931348623157e+308,可以通过Number.MAX_VALUENumber.MIN_VALUE来得到证实。同样可以通过Number.MIN_SAFE_INTEGERNumber.MAX_SAFE_INTEGER 证实安全整数的范围。

    2、三个存在的问题

         1、在四则运算中存在精度丢失的问题,比如: 0.1 + 0.2       //0.30000000000000004

        精度缺失不是小于或者大于某个值就会出现精度缺失,而是落在固定间隔长度的数值中就会精度缺失,精度缺失的原因是该数值在内存中不能完全记录下来。

        前面提到,计算机中存储小数是先转换成二进制进行存储的,我们来看一下0.1和0.2转换成二进制的结果:

        十进制(0.1) =>二进制 (00011001100110011001(1001)...)

        十进制(0.2) => 二进制(00110011001100110011(0011)...)

        可以发现,0.1和0.2转成二进制之后都是一个无限循环的数,前面提到尾数位只能存储最多53位有效数字,这时候就必须来进行四舍五入了,最终的这个二进制数转成十进制就是0.30000000000000004。

        至此,这个精度丢失的问题已经解释清楚了,用一句话来概括就是,计算机中用二进制来存储小数,而大部分小数转成二进制之后都是无限循环的值,因此存在取舍问题,也就是精度丢失。

    2、超过最大安全整数的运算是不安全的,比如:9007199254740991 + 2        //9007199254740992

        最大安全整数9007199254740991对应的二进制数如图:

        53位有效数字都存储满了之后,想要表示更大的数字,就只能往指数数加一位,这时候尾数因为没有多余的存储空间,因此只能补0。 可以看出在指数位为53的情况下,最后一位尾数位为0的数字可以被精确表示,而最后一位尾数为为1的数字都不能被精确表示。也就是可以被精确表示和不能被精确表示的比例是1:1

【JS】详解JS精度丢失原理以及业务中价格计算引入数学库 Math.js的使用场景_第2张图片

        小结:之所以会有最大安全整数这个概念,本质上还是因为数字类型在计算机中的存储结构。在尾数位不够补零之后,只要是多余的尾数为1所对应的整数都不能被精确表示。 

        可以发现,不管是浮点数计算的计算结果错误和大整数的计算结果错误,最终都可以归结到JS的精度只有53位(尾数只能存储53位的有效数字)。那么我们在日常工作中碰到这两个问题该如何解决呢? 

        3、JS的 toFixed() 方法错误问题

        JS的 toFixed() 四舍五入规则与数学中的规则不同,而是采用银行家算法,俗称『四舍六入五成双』。对于位数很多的近似数,当有效位数确定后,其后面多余的数字应该舍去,只保留有效数字最末一位。

        『四』:≤4 时舍去

        『六』:≥6时进1

        『五』:根据5后面的数字来定,当5后有数字时舍5进1;当5后无数字时分两种情况,①5前为奇数,舍5入1;②5前为偶数,舍5不进。

二、精度丢失问题在业务里的体现和隐患

        1、精度丢失在具体计算的体现:

// 加法 =====================
0.1 + 0.2 = 0.30000000000000004
0.7 + 0.1 = 0.7999999999999999
0.2 + 0.4 = 0.6000000000000001

// 减法 =====================
1.5 - 1.2 = 0.30000000000000004
0.3 - 0.2 = 0.09999999999999998
 
// 乘法 =====================
19.9 * 100 = 1989.9999999999998
0.8 * 3 = 2.4000000000000004
35.41 * 100 = 3540.9999999999995

// 除法 =====================
0.3 / 0.1 = 2.9999999999999996
0.69 / 10 = 0.06899999999999999

        2、超过最大安全整数的运算或传参等场景

      整数会出现误差,小数超出长度会被丢弃。(如绿谷后管入组号为number类型长度超出js安全数字,因不涉及到计算,所以改为string类型解决详情跳转异常)

var numLen16 = '999999999666666'
numLen16.length // 16  长度16位 正整数
+numLen16 // '999999999666666' 没有误差

var numLen17 = '9999999999555557'
numLen17.length // 17  长度17位 正整数
+numLen16 // '9999999999555556' 从第17位开始出现误差

//json 化时的问题
var json = JSON.stringify({a:999999999955555777,b:true})
json // "{"a":999999999955555800,"b":true}"  也会出现误差

//小数超过18位左右,小数部分超过位数会被丢掉
9555555555555.34243535  // 9555555555555.342

        3、四舍五入算法问题

let num = 1.234
num.toFixed(2) //1.23 正确
 
let num = 1.235
num.toFixed(2) //1.24 正确
 
let num = 1.236
num.toFixed(2) //1.24 正确
 
let num = 0.234
num.toFixed(2) //0.23 正确
 
let num = 0.235
num.toFixed(2) //0.23 错误 X
 
let num = 0.236
num.toFixed(2) //0.24 正确

三、Math.js

  1. 配置   math.js | an extensive math library for JavaScript and Node.js
            Math.js 包含许多配置选项。这些选项可以应用于创建的 mathjs 实例并在之后更改。
    import { create, all } from 'mathjs'
    
    // create a mathjs instance with configuration
    const config = {
      epsilon: 1e-12,
      matrix: 'Matrix',
      number: 'number',
      precision: 64,
      predictable: false,
      randomSeed: null
    }
    const math = create(all, config)
    
    // read the applied configuration
    console.log(math.config())
    
    // change the configuration
    math.config({
      number: 'BigNumber'
    })
  2. 链接   math.js | an extensive math library for JavaScript and Node.js

    可以使用函数math.chain(value) 创建链。
    math.chain(3)
        .add(4)
        .subtract(2)
        .done() // 5
    
    math.chain( [[1, 2], [3, 4]] )
        .subset(math.index(0, 0), 8)
        .multiply(3)
        .done() // [[24, 6], [9, 12]]

    done() 完成链并返回链的值。
    valueOf() 与 相同done(),返回链的值。
    toString()math.format(value)在链的值上 执行,返回值的字符串表示。
    1. 扩展   math.js | an extensive math library for JavaScript and Node.js

              可以使用该import函数轻松地使用函数和变量扩展该库 。该import函数在 mathjs 实例上可用,可以使用该create函数创建。
      该函数import接受一个带有函数和变量的对象,或者一个带有工厂函数的数组。它具有以下语法:
      math.import(functions: Object [, options: Object])

          > functions是包含要导入的函数和/或值的对象或数组。import支持常规值和函数、类型函数(参见部分类型函数)和工厂函数(参见部分工厂函数)。数组仅适用于包含工厂函数的情况。

          > options是带有选项的可选第二个参数。以下选项可用:

              · override 如果true,现有函数将被覆盖。默认值为false。
              · silent 如果true,该函数不会在重复或无效类型上引发错误。默认值为false。
              · wrap 如果true,则函数将被包装在一个包装函数中,该函数将 Matrix 等数据类型转换为 Array 等原始数据类型。使用不支持 math.js 数据类型的库扩展 math.js 时需要包装器。默认值为false。

      // define new functions and variables
      math.import({
        myvalue: 42,
        hello: function (name) {
          return 'hello, ' + name + '!'
        }
      })
      
      // defined functions can be used in both JavaScript as well as the parser
      math.myvalue * 2                 // 84
      math.hello('user')               // 'hello, user!'
      
      const parser = math.parser()
      parser.evaluate('myvalue + 10')  // 52
      parser.evaluate('hello("user")') // 'hello, user!'

      扩展之导入外部库
            可以按如下方式导入numbers.js和 numeric.js等外部库 。必须使用 npm 安装这些库:

      $ npm install numbers
      $ npm install numeric
      import { create, all } from 'mathjs'
      import * as numbers from 'numbers'
      import * as numeric from 'numeric'
      
      // create a mathjs instance and import the numbers.js and numeric.js libraries
      const math = create(all)
      math.import(numbers, {wrap: true, silent: true})
      math.import(numeric, {wrap: true, silent: true})
      
      // use functions from numbers.js
      math.fibonacci(7)                           // 13
      math.evaluate('fibonacci(7)')               // 13
      
      // use functions from numeric.js
      math.evaluate('eig([1, 2; 4, 3])').lambda.x // [5, -1]

       工厂函数:可以使用math.import以下命令在 math.js 中导入常规 JavaScript 函数:

      export function myFunction (a, b) {
        // ...
      }
      import { myFunction } from './myFunction.js'
      
      math.import({
        myFunction
      })

              当myFunction需要来自 math.js 的功能时会出现一个问题:当在单独的文件中时,它无法访问 math.js 的当前实例。可以使用工厂函数来解决这个问题。工厂函数允许在创建函数时将依赖项注入到函数中。

      工厂函数的语法是:

      factory(name: string, dependencies: string[], create: function, meta?: Object): function

              > name 是创建的函数的名称。
              > dependencies 是一个带有依赖函数名称的数组。
              > create是创建函数的函数。具有依赖项的对象作为第一个参数传递。
              > meta一个可选对象,可以包含您想要的任何元数据。这将作为meta已创建函数的属性附加。mathjs 实例使用的已知元数据属性是:
                      · isClass: boolean 如果为 true,则创建的函数应该是一个类,例如出于安全原因不会在表达式解析器中公开。
                      · lazy: boolean. 默认情况下,所有内容都由import. 只有在实际使用导入的函数或常量时,才会构造它。可以通过lazy: false在元数据中设置来强制立即创建函数。
                      · isTransformFunction: boolean. 如果为 true,则创建的函数将作为转换函数导入。它math本身不会被导入,只会在mathWithTransform表达式解析器使用的内部命名空间中导入。
                      · recreateOnConfigChange: boolean. 如果为 true,则在配置发生更改时将再次创建导入的工厂。例如,这用于诸如 之类的常量pi,这取决于number可以是数字或 BigNumber 的配置设置。 
      这里是一个工厂函数的例子,它依赖于multiply:

      import { factory, create, all } from 'mathjs'
      
      // create a factory function
      const name = 'negativeSquare'
      const dependencies = ['multiply', 'unaryMinus']
      const createNegativeSquare = factory(name, dependencies, function ({ multiply, unaryMinus }) {
          return function negativeSquare (x) {
            return unaryMinus(multiply(x, x))
          }
        })
      
      // create an instance of the function yourself:
      const multiply = (a, b) => a * b
      const unaryMinus = (a) => -a
      const negativeSquare = createNegativeSquare({ multiply, unaryMinus })
      console.log(negativeSquare(3)) // -9
      
      // or import the factory in a mathjs instance and use it there
      const math = create(all)
      math.import(createNegativeSquare)
      console.log(math.negativeSquare(4)) // -16
      console.log(math.evaluate('negativeSquare(5)')) // -25
      1. 表达式解析   math.js | an extensive math library for JavaScript and Node.js

        · 评估
        Math.js 带有一个math.evaluate评估表达式的函数。句法:
        math.evaluate(expr)
        math.evaluate(expr, scope)
        math.evaluate([expr1, expr2, expr3, ...])
        math.evaluate([expr1, expr2, expr3, ...], scope)

        函数evaluate接受一个单一的表达式或一个带有表达式的数组作为第一个参数,并有一个可选的第二个参数,其中包含一个包含变量和函数的范围。scope可以是常规的 JavaScript 对象或 Map。scope将用于解析符号,并写入分配的变量或函数。

        以下代码演示了如何计算表达式。

        // evaluate expressions
        math.evaluate('sqrt(3^2 + 4^2)')        // 5
        math.evaluate('sqrt(-4)')               // 2i
        math.evaluate('2 inch to cm')           // 5.08 cm
        math.evaluate('cos(45 deg)')            // 0.7071067811865476
        
        // provide a scope
        let scope = {
            a: 3,
            b: 4
        }
        math.evaluate('a * b', scope)           // 12
        math.evaluate('c = 2.3 + 4.5', scope)   // 6.8
        scope.c                                 // 6.8

         表达式语法:math.js | an extensive math library for JavaScript and Node.js
        简化、推导数学表达式:math.js | an extensive math library for JavaScript and Node.js

        · 编译
        Math.js 包含一个math.compile将表达式编译为 JavaScript 代码的函数。这是首先解析然后编译表达式的快捷方式。语法是:

        math.compile(expr)
        math.compile([expr1, expr2, expr3, ...])

        一个表达式只需要编译一次,之后就可以针对不同的作用域重复计算该表达式。可选作用域用于解析符号和编写分配的变量或函数。参数scope可以是一个普通的 Object 或 Map。

        // 语法
        const code = math.compile(expr)       // compile an expression
        const result = code.evaluate([scope]) // evaluate the code with an optional scope
        
        // 用法示例
        // parse an expression into a node, and evaluate the node
        const code1 = math.compile('sqrt(3^2 + 4^2)')
        code1.evaluate()  // 5
        
        

        · 解析
        Math.js 包含一个math.parse将表达式解析为 表达式树(https://mathjs.org/docs/expressions/expression_trees.html)的函数。语法是:

        math.parse(expr)
        math.parse([expr1, expr2, expr3, ...])

        用法示例:

        // parse an expression into a node, and evaluate the node
        const node1 = math.parse('sqrt(3^2 + 4^2)')
        const code1 = node1.compile()
        code1.evaluate() // 5
        
        // provide a scope
        const node2 = math.parse('x^a')
        const code2 = node2.compile()
        let scope = {
            x: 3,
            a: 2
        }
        code2.evaluate(scope) // 9
        
        // change a value in the scope and re-evaluate the node
        scope.a = 3
        code2.evaluate(scope) // 27

        · 解析器
                 除了静态函数math.evaluate 之外 ,math.js 还包含一个带有函数evaluate的解析器parse,它会自动在内存中保留一个具有分配变量的作用域。解析器还包含一些方便的函数,用于从内存中获取、设置和删除变量。
        解析器可以通过以下方式创建:

        const parser = math.parser()

        解析器包含以下功能:

                > clear() 完全清除解析器的范围。
                > evaluate(expr) 评估表达式。返回表达式的结果。
                > get(name) 从解析器的作用域中检索变量或函数。
                > getAll() 从解析器的作用域中检索包含所有已定义变量的映射。
                > remove(name) 从解析器的范围中删除变量或函数。
                > set(name, value) 在解析器的范围内设置变量或函数。
                以下代码显示了如何创建和使用解析器:

        // create a parser
        const parser = math.parser()
        
        // evaluate expressions
        parser.evaluate('sqrt(3^2 + 4^2)')      // 5
        parser.evaluate('sqrt(-4)')             // 2i
        parser.evaluate('2 inch to cm')         // 5.08 cm
        parser.evaluate('cos(45 deg)')          // 0.7071067811865476
        
        // define variables and functions
        parser.evaluate('x = 7 / 2')            // 3.5
        parser.evaluate('x + 3')                // 6.5
        parser.evaluate('f(x, y) = x^y')        // f(x, y)
        parser.evaluate('f(2, 3)')              // 8
        
        // get and set variables and functions
        const x = parser.get('x')               // x = 7
        const f = parser.get('f')               // function
        const g = f(3, 3)                       // g = 27
        parser.set('h', 500)
        parser.evaluate('h / 2')                // 250
        parser.set('hello', function (name) {
            return 'hello, ' + name + '!'
        })
        parser.evaluate('hello("user")')        // "hello, user!"
        
        // clear defined functions and variables
        parser.clear()

  3. 数据类型

    1.   Math.js 支持三种类型的数字:

              > 快速浮点运算的数字
              > BigNumber 用于任意精度算术
              > 分数,它根据分子和分母存储数字
       · Number  https://mathjs.org/docs/datatypes/numbers.html
         Math.js 使用内置的 JavaScript Number 类型。Number 是一个浮点数,精度有限,为 64 位,大约 16 位。
         舍入误差的解决方案:
            >> 一种解决方案是将精度限制在显示输出中 16 位的实际精度以下:

      // prevent round-off errors showing up in output
      const ans = math.add(0.1, 0.2)     //  0.30000000000000004
      math.format(ans, {precision: 14})  // '0.3'
         >> 替代方法是使用Fractions将数字存储为分子和分母,或使用BigNumbers存储具有更高精度的数字。

              由于计算中的舍入错误,比较 JavaScript 数字是不安全的。例如,0.1 + 0.2 == 0.3在 JavaScript 中执行将返回 false,因为添加会0.1 + 0.2引入舍入错误并且不会准确返回0.3。为了解决这个问题,math.js 的关系函数会检查比较值之间的相对差异是否小于配置的 option epsilon
      // compare values having a round-off error
      console.log(0.1 + 0.2 === 0.3)           // false
      console.log(math.equal(0.1 + 0.2, 0.3))  // true
      
      // small values (< 2.22e-16) cannot be compared
      console.log(3e-20 === 3.1e-20)           // false
      console.log(math.equal(3e-20, 3.1e-20))  // true
      

      · BigNumber  math.js | an extensive math library for JavaScript and Node.js
      对于任意精度的计算,math.js 支持BigNumber 数据类型。BigNumber 支持由decimal.js提供支持。
      可以使用以下函数创建 BigNumber bignumber

      math.bignumber('2.3e+500') // BigNumber, 2.3e+500

      可以在实例化 math.js 时配置: 

      math.config({
        number: 'BigNumber',      // Default type of number:
                                  // 'number' (default), 'BigNumber', or 'Fraction'
        precision: 64             // Number of significant digits for BigNumbers
      })
      
      // use math
      math.evaluate('0.1 + 0.2')  // BigNumber, 0.3

      math.js 中的大多数函数都支持 BigNumbers,但不是全部。例如,该函数random不支持 BigNumbers。
      BigNumber 的计算比 Number 的计算慢得多,但它们可以以任意精度执行。通过使用更高的精度,出现舍入误差的可能性更小:

      // round-off errors with numbers
      math.add(0.1, 0.2)                                     // Number, 0.30000000000000004
      math.divide(0.3, 0.2)                                  // Number, 1.4999999999999998
      
      // no round-off errors with BigNumbers :)
      math.add(math.bignumber(0.1), math.bignumber(0.2))     // BigNumber, 0.3
      math.divide(math.bignumber(0.3), math.bignumber(0.2))  // BigNumber, 1.5

      BigNumber 并不能解决与精度和舍入误差相关的所有问题。具有无限位数的数字不能用常规数字或 BigNumber 表示。虽然 BigNumber 可以存储更多的数字,但如果只是为了保持计算速度足够快以保持实用性,数字的数量仍然有限。
      BigNumber 转换为数字时,BigNumber 的高精度将丢失。当 BigNumber 太大而无法表示为 Number 时,它将被初始化为Infinity

      const one = math.bignumber(1)
      const three = math.bignumber(3)
      const third = math.divide(one, three)
      console.log(third.toString())
      // outputs 0.3333333333333333333333333333333333333333333333333333333333333333
      
      const ans = math.multiply(third, three)
      console.log(ans.toString())
      // outputs 0.9999999999999999999999999999999999999999999999999999999999999999
      // this should be 1 again, but `third` is rounded to a limited number of digits 3
      

      · Fraction
      math.js 支持一种Fraction数据类型。分数支持由fraction.js提供支持。与numbers和BigNumbers不同,分数可以存储具有无限重复小数的数字,例如1/3 = 0.3333333...,可以表示为0.(3),或者2/7可以表示为0.(285714)

      // 可以使用以下函数创建分数fraction
      math.fraction('1/3')   // Fraction, 1/3
      math.fraction(2, 3)    // Fraction, 2/3
      math.fraction('0.(3)') // Fraction, 1/3
      
      // 也可以在函数中使用fraction
      math.add(math.fraction('1/3'), math.fraction('1/6'))      // Fraction, 1/2
      math.multiply(math.fraction('1/4'), math.fraction('1/2')) // Fraction, 1/8
      
      // 可以在实例化 math.js 时配置
      // Configure the default type of number: 'number' (default), 'BigNumber', or 'Fraction'
      math.config({
        number: 'Fraction'
      })
      
      // use the expression parser
      math.evaluate('0.32 + 0.08') // Fraction, 2/5

      · Matrices  math.js | an extensive math library for JavaScript and Node.js
      Math.js 支持多维矩阵和数组。矩阵可以被创建、操作和用于计算。常规 JavaScript 数组以及由 math.js 实现的矩阵类型都可以在所有相关的 math.js 函数中互换使用。math.js 支持密集和稀疏矩阵。
      Math.js 支持两种类型的矩阵:
           > Array,一个常规的 JavaScript 数组。可以通过嵌套数组来创建多维数组。
           > Matrix,由 math.js 实现的矩阵。阿Matrix是缠绕在常规的JavaScript对象Array,提供效用函数,便于矩阵运算例如subset,size,resize,clone,等等。

      // create an array and a matrix
      const array = [[2, 0], [-1, 3]]               // Array
      const matrix = math.matrix([[7, 1], [-2, 3]]) // Matrix
      
      // perform a calculation on an array and matrix
      math.square(array)                            // Array,  [[4, 0], [1, 9]]
      math.square(matrix)                           // Matrix, [[49, 1], [4, 9]]
      
      // perform calculations with mixed array and matrix input
      math.add(array, matrix)                       // Matrix, [[9, 1], [-3, 6]]
      math.multiply(array, matrix)                  // Matrix, [[14, 2], [-13, 8]]
      
      // create a matrix. Type of output of function ones is determined by the
      // configuration option `matrix`
      math.ones(2, 3)                               // Matrix, [[1, 1, 1], [1, 1, 1]]
      
      

      有许多函数可以创建具有特定大小和内容的矩阵:oneszerosidentity

      // zeros creates a matrix filled with zeros
      math.zeros(3)           // Matrix, size [3],    [0, 0, 0]
      math.zeros(3, 2)        // Matrix, size [3, 2], [[0, 0], [0, 0], [0, 0]]
      math.zeros(2, 2, 2)     // Matrix, size [2, 2, 2],
                              //   [[[0, 0], [0, 0]], [[0, 0], [0, 0]]]
      
      // ones creates a matrix filled with ones
      math.ones(3)                        // Matrix, size [3],    [1, 1, 1]
      math.multiply(math.ones(2, 2), 5)   // Matrix, size [2, 2], [[5, 5], [5, 5]]
      
      // identity creates an identity matrix
      math.identity(3)      // Matrix, size [3, 3], [[1, 0, 0], [0, 1, 0], [0, 0, 1]]
      math.identity(2, 3)   // Matrix, size [2, 3], [[1, 0, 0], [0, 1, 0]]

      · Units  math.js | an extensive math library for JavaScript and Node.js
      Math.js 支持单位。单位可用于计算和转换。

      const a = math.unit(45, 'cm')             // Unit 450 mm
      const b = math.unit('0.1 kilogram')       // Unit 100 gram
      const c = math.unit('2 inch')             // Unit 2 inch
      const d = math.unit('90 km/h')            // Unit 90 km/h
      const e = math.unit('101325 kg/(m s^2)')  // Unit 101325 kg / (m s^2)
      
      const d = c.to('cm')                      // Unit 5.08 cm
      b.toNumber('gram')                        // Number 100
      math.number(b, 'gram')                    // Number 100
      
      c.equals(a)                               // false
      c.equals(d)                               // true
      c.equalBase(a)                            // true
      c.equalBase(b)                            // false
      
      d.toString()          

      可以使用该math.createUnit函数将自己的单位添加到 Math.js。以下示例定义了一个新单位furlong,然后在计算中使用用户定义的单位:

      math.createUnit('furlong', '220 yards') 
      math.evaluate('1 mile to furlong')            // 8 furlong
      

  4. Functions math.js | an extensive math library for JavaScript and Node.js

      format()
      将任何类型的值格式化为字符串。句法:
    math.format(value)
    math.format(value, options)
    math.format(value, precision)
    math.format(value, callback)

       > value: * 要格式化的值
       > options: Object 具有格式选项的对象。可用选项:
              · notation: string 数字符号。从中选择:
                      · 'fixed' 始终使用常规数字表示法。例如“123.40”和“14000000”
                      · 'exponential' 总是使用指数表示法。例如“1.234e+2”和“1.4e+7”
                      · 'engineering' 始终使用工程表示法:始终使用指数表示法,并选择指数为 3 的倍数。例如 '123.4e+0' 和 '14.0e+6'
                      · 'auto'(默认值)用于在lower和之间具有绝对值的数字的常规数字符号 upper,并在其他地方使用指数符号。包括下限,排除上限。例如“123.4”和“1.4e7”。
                      · 'bin'、'oct' 或 'hex' 使用二进制、八进制或十六进制表示法格式化数字。例如“0b1101”和“0x10fe”。
              · wordSize: number 用于以二进制、八进制或十六进制表示法进行格式化的字长(以位为单位)。仅与 'notation' 选项的 'bin'、'oct' 或 'hex' 值一起使用。定义此选项后,该值将格式化为给定字大小的有符号二进制补码整数,并将大小后缀附加到输出。例如 format(-1, {notation: 'hex', wordSize: 8}) === '0xffi8'。默认值未定义。
              · precision: number 一个介于 0 和 16 之间的数字,用于舍入数字的数字。在符号“指数”、“工程”和“自动”的情况下,precision 定义返回的有效数字的总数。在符号“固定”的情况下,precision定义小数点后的有效位数。 precision默认情况下未定义。
              · lowerExp: number 指数确定在 时用指数格式化值的下限notation='auto。默认值为-3。
              · upperExp: number 确定在 时用指数格式化值的上限的指数notation='auto。默认值为5。
              · fraction: string. 可用值:“比率”(默认)或“十进制”。例如format(fraction(1, 3))配置'ratio'时输出'1/3',配置0.(3)'decimal'时输出。
       > callback: function 自定义格式化函数,为 中的所有数字元素调用value,例如矩阵的所有元素,或复数的实部和虚部。此回调可用于覆盖具有任何类型格式的内置数字符号。函数callback 使用value作为参数调用并且必须返回一个字符串。

四、满足业务需求的解决方案

import { create, all } from 'mathjs';

const config = {
  epsilon: 1e-12,
  matrix: 'Matrix', // 函数的默认矩阵输出类型。
  number: 'BigNumber', // BigNumbers比 JavaScript 的默认数字具有更高的精度
  precision: 64, // BigNumbers 的最大有效数字位数。此设置仅适用于 BigNumber,不适用于数字。
  predictable: false, // 可预测的输出类型函数。当为真时,输出类型仅取决于输入类型。当为 false(默认)时,输出类型可能因输入值而异。
  randomSeed: null // 设置为null使用随机种子为伪随机数生成器提供种子。
};
const math = create(all, config);
math.import({
  forEg: function (number, n) {}
});

// tofixed
export function forEg (number, digits) {
  return math.forEg(number, digits);
}

// +
export function add (a, b, digits) {
  let num1 = a ? math.bignumber(a) : 0;
  let num2 = b ? math.bignumber(b) : 0; // bignumber 保证计算精度 format 返回计算的数字表示法
  let digit = digits || 64; 
  return math
    .format(math
      .chain(math
        .add(num1, num2)
      )
      .done()
      , {notation: 'fixed', precision: digit});
}

// -
export function subtract (a, b, digits) {
  let num1 = a ? math.bignumber(a) : 0;
  let num2 = b ? math.bignumber(b) : 0; // bignumber 保证计算精度 format 返回计算的数字表示法
  let digit = digits || 64; 
  return math
    .format(math
      .chain(math
        .subtract(num1, num2)
      )
      .done()
      , {notation: 'fixed', precision: digit});
}

// x
export function multiply (a, b, digits) {
  let num1 = a ? math.bignumber(a) : 0;
  let num2 = b ? math.bignumber(b) : 0; // bignumber 保证计算精度 format 返回计算的数字表示法
  let digit = digits || 64; 
  return math
    .format(math
      .chain(math
        .multiply(num1, num2)
      )
      .done()
      , {notation: 'fixed', precision: digit});
}

// ÷
export function divide (a, b, digits) {
  let num1 = a ? math.bignumber(a) : 0;
  let num2 = b ? math.bignumber(b) : 0; // bignumber 保证计算精度 format 返回计算的数字表示法
  let digit = digits || 64; 
  return math
    .format(math
      .chain(math
        .divide(num1, num2)
      )
      .done()
      , {notation: 'fixed', precision: digit});
} 

// 总价=数量*进价
export function TotalPrice (num, price, digits) {
  let digit = digits || 64;
  return math
    .format(math.chain(num)
      .multiply(price)
      .done()
      , {notation: 'fixed', precision: digit});
}

// 无税金额=数量*进价/(1+进项税率)
export function NoTaxTotalPrice (num, price, tax, digits) {
  let digit = digits || 64;
  return math
    .format(math.chain(
      multiply(num, price))
      .divide(add(1, tax))
      .done(), {notation: 'fixed', precision: digit});
}

// 税额=金额-无税金额
export function TaxTotalPrice (num, price, tax, digits) {
  let digit = digits || 64;
  let TotalPrice = multiply(num, price); // 金额
  let Tax = add(1, tax); // 税率
  let NoTaxTotalPrice = divide(TotalPrice, Tax); // 无税金额
  return math
    .format(math.chain(TotalPrice)
      .subtract(NoTaxTotalPrice)
      .done(), {notation: 'fixed', precision: digit});
}

    // 总价=数量*进价  计算结果 1851.8518368->1851.85
    console.log('总价1851.85', mathSelf.TotalPrice(15, 123.45678912, 2)); 

    // 无税金额=数量*进价/(1+进项税率)计算结果:1833.51667009901->1833.52
    console.log('无税金额1833.52', mathSelf.NoTaxTotalPrice(15, 123.45678912, 0.01, 2)); 

    // 税额=金额-无税金额 计算结果 18.33516670099->18.34
    console.log('税额18.34', mathSelf.TaxTotalPrice(15, 123.45678912, 0.01, 2)); 


// 计算结果 0.30
    console.log('精度验证0.1+0.2', mathSelf.add(0.1, 0.2, 2)); 

// 计算结果 1990.00
    console.log('精度验证19.9*100', mathSelf.multiply(19.9, 100, 2)); 

你可能感兴趣的:(Javascript,基本功,javascript)