Mathjs单位模块设计探索记录

713 阅读6分钟

本文用于记录探索带中文单位算式计算的预研,学习mathjs的设计实现。

功能介绍

mathjs中单位模块提供了带(英文)单位的运算、对带单位值进行(英文)单位转换等功能。

官方文档

// 基本使用
const math = require('mathjs');
 
// 单位转换
const unitValue1 = math.unit('24 hours');
console.log(unitValue1.toNumber('minutes')); // ===> 1440
 
// 带单位计算
const unitValue2 = math.unit(2, 'km/s');
console.log(math.multiply(unitValue1, unitValue2).toNumber('m')); // ===> 172800000
 
// 自定义单位
math.createUnit('ly', math.multiply(math.evaluate('speedOfLight'), math.unit('1 year'))); // 光年
console.log(math.unit('1 ly').toNumber('km')); // ===> 9460730472580.8
 
// 判断字符串是否为单位
console.log(math.Unit.isValuelessUnit('ly')); // ===> true
console.log(math.Unit.isValuelessUnit('hello')); // ===> false
 
// 判断是否同类型单位
const unitValue3 = math.unit('1 mile/year');
console.log(unitValue2.equalBase(unitValue3)); // ===> true
const unitValue4 = math.multiply(math.unit('1 kW'), math.unit('1 hour')); // 千瓦时
const unitValue5 = math.multiply(math.unit('1 N'), math.unit('1 m')); // 焦耳
console.log(unitValue4.equalBase(unitValue5)); // ===> true
console.log(unitValue2.equalBase(unitValue5)); // ===> false

单位的格式

mathjs中的Unit类主要分为数值与单位两部分 mathjs对Unit类的输入数据有如下要求:

// A unit should follow this pattern:
// [number] ...[ [/] unit[^number] ]
// unit[^number] ... [ [
/] unit[^number] ]
// Rules:
// number is any floating point number.
// unit is any alphanumeric string beginning with an alpha. Units with names like e3 should be avoided because they look like the exponent of a floating point number!
// The string may optionally begin with a number.
// Each unit may optionally be followed by ^number.
// Whitespace or a forward slash is recommended between consecutive units, although the following technically is parseable:
// 2m^2kg/s^2
// it is not good form. If a unit starts with e, then it could be confused as a floating point number:
// 4erg

数值为浮点数值(在实际应用中可以使用mathjs提供的BigNumber、Fraction等类型)

单位为一个英文字母开头的,由英文字母与数字组成的字符串,此处的数字允许带有部分数学运算符号,如"^"、"/"、“(”、“)”等

单位的解析

在解析的时候,mathjs先解析数值部分,根据配置转换为数值类型(BigNumber、Fraction等)。而对于后面的单位,mathjs会将其分割成一个一个的基本单位进行解析
比如上面的"2m^2kg/s^2",单位部分为m^2kg/s^2,会被分成m^2、kg、s^2三个部分,我们可以通过如下代码查看其数据结构。

const unitValue = math.unit('1', 'm^2kg/s^2');
console.log(JSON.stringify(unitValue.units, null, 2));

得到如下输出

[
  {
    "unit": {
      "name": "m",
      "base": {
        "dimensions": [ 0, 1, 0, 0, 0, 0, 0, 0, 0 ],
        "key": "LENGTH"
      },
      "prefixes": { ... },
      "value": 1,
      "offset": 0,
      "dimensions": [ 0, 1, 0, 0, 0, 0, 0, 0, 0 ]
    },
    "prefix": {
      "name": "",
      "value": 1,
      "scientific": true
    },
    "power": 2
  },
  {
    "unit": {
      "name": "g",
      "base": {
        "dimensions": [ 1, 0, 0, 0, 0, 0, 0, 0, 0 ],
        "key": "MASS"
      },
      "prefixes": { ... },
      "value": 0.001,
      "offset": 0,
      "dimensions": [ 1, 0, 0, 0, 0, 0, 0, 0, 0 ]
    },
    "prefix": {
      "name": "k",
      "value": 1000,
      "scientific": true
    },
    "power": 1
  },
  {
    "unit": {
      "name": "s",
      "base": {
        "dimensions": [ 0, 0, 1, 0, 0, 0, 0, 0, 0 ],
        "key": "TIME"
      },
      "prefixes": { ... },
      "value": 1,
      "offset": 0,
      "dimensions": [ 0, 0, 1, 0, 0, 0, 0, 0, 0 ]
    },
    "prefix": {
      "name": "",
      "value": 1,
      "scientific": true
    },
    "power": -2
  }
]

属性解析

每个单位被分为prefix、unit、power三个部分:
prefix 单位的前缀,比如“kg”中的k就是前缀部分,这代表它的值是单位g的1000倍,所以它的prefix中的value为1000
power是单位的幂次数,比如平方米m2(或者用m^2表示),它的次数就是2,但是我们看到上面的输出中,s^2的power值为-2,这是因为它前面有一个反斜杠“/”,所以它的次数被乘了-1
unit 部分代表了一个基本单位,有如下的属性

unit

name 单位的名称,由英文字母组成
base 单位的基础,其中的key表示了单位的衡量类型,mathjs提供了如下类型的单位

MASS:质量
LENGTH:长度
TIME:时间
CURRENT:电流(安培等)
TEMPERATURE:温度
LUMINOUS_INTENSITY:光度
AMOUNT_OF_SUBSTANCE:物质的量(摩尔)
ANGLE:角度
BIT:数据量

以上的为基本类型,每个作为一个基础维度Dimension,后面的单位都是由它们通过乘除运算得出

FORCE:力
SURFACE:面积
VOLUME:体积
ENERGY:能量/功
POWER:功率
PRESSURE:压强
ELECTRIC_CHARGE
ELECTRIC_CAPACITANCE
ELECTRIC_POTENTIAL
ELECTRIC_RESISTANCE
ELECTRIC_INDUCTANCE
ELECTRIC_CONDUCTANCE
MAGNETIC_FLUX
MAGNETIC_FLUX_DENSITY
FREQUENCY:频率

value 对于该类型的基本单位的值
比如mathjs中质量的标准单位为kg(千克),而克与千克的转换公式为 g = 0.001 kg,所以克的单位g的value值为0.001

offset 相对于该类型基本单位的偏移值
比如mathjs中温度的标准单位为K(开尔文),而摄氏度与开尔文的转换公式为 C = K + 273.15, 所以摄氏度的单位degC的offset值为273.15

prefixes单位支持的前缀列表,针对不同类型的单位,提供了不同的前缀集
比如单位缩写中常见的前缀有k(千)、m(毫)、M(兆), 而它们在完整的单位中,则实用kilo、milli、mega表示

dimensions单位的维度向量,与base中的dimensions是一样的,是一个数组,数组中的每一个元素代表一种基本单位类型在该单位中的次数
从左到右分别代表:

  1. 质量
  2. 长度
  3. 时间
  4. 电流
  5. 温度
  6. 光度
  7. 物质的量
  8. 角度
  9. 数据量

在基本单位类型中,只有自身对应的维的值为1,其它维的值都是0
当两个单位相乘时,它们的维度向量相加,当一个单位除以另一个单位时,它们的维度向量相减
面积单位 = 长度单位 * 长度单位,那么面积单位的维度向量就是 [0, 2, 0, 0, 0, 0, 0, 0, 0],其中的2就是长度维度的次数
又比如速度单位 = 长度单位 / 时间单位,name速度单位的维度向量就是 [0, 1, -1, 0, 0, 0, 0, 0, 0],其中的1是长度维度次数,-1是时间维度次数

通过维度值,可以计算两个单位是否同一类型,如果是同一类型的单位,那么它们可以互相转化,如:
J(焦耳) = N(牛顿) * m(米)
kWh(千瓦时) = kW(千瓦) * h(小时)

N(牛顿)的单位类型是力,维度向量为 [1, 1, -2, 0, 0, 0, 0, 0, 0]
m(米)的单位类型是长度,维度向量为 [0, 1, 0, 0, 0, 0, 0, 0, 0]
它们相乘,得到J(焦耳),J(焦耳)的维度向量为它们两者的维度向量相加,即 [1, 2, -2, 0, 0, 0, 0, 0, 0]

kW(千瓦)是带前缀的W(瓦特),单位类型是功率,维度向量为 [1, 2, -3, 0, 0, 0, 0, 0, 0]
h(小时)的单位类型是时间,维度向量为 [0, 0, 1, 0, 0, 0, 0, 0, 0]
它们相乘,得到kWh(千瓦时),kWh(千瓦时)的维度向量为它们两者的维度向量相加,即 [1, 2, -2, 0, 0, 0, 0, 0, 0]

可以看到两者虽然计算得到的过程不一样,但是最终它们的维度向量是一样的,它们都是能量的单位,可以互相转换,实现1kWh = 3600000 J