(2)公式编辑器: shuttle-formula

137 阅读7分钟

这篇文章主要介绍公式编辑器核心包部分的实现,包括词法分析、语法分析、语法检查、计算表达式

相关文章

上一篇文章中介绍了公式编辑器的基本使用方法:(1)公式编辑器: shuttle-formula

词法分析

token:指在公式中出现的特殊字符,如运算符、括号、引号等。

思路:词法分析仅仅将公式以 token 分割,最后输出 token 的列表,同时记录每个 token 的类型和源码及其位置(方便后续报错提示定位),不去校验 token 所在位置的正确性。

shuttle-formula 提供的 token:布尔(解析 true 或 false)、数字(解析数字)、字符串(被'或"包裹);空格、制表符、换行符;加(+)、减(-)、乘(*)、除(/)、取余(%)、大于(>)、小于(<)、大于等于(>=)、小于等于(<=)、等于(==)、不等于(!=)、 且(&&)、或(||)、逗号(,)、左括号(()、右括号())、左中括号([)、右中括号(])、唤起函数(@)、唤起变量($)、变量下一层级(.)

例如公式:$a.b.c + @sum(10, $a.d) >= 10.8 经过词法分析后,输出的 token 列表如下:

const tokens = [
  {
    name: 'token',
    id: 'm9gk10h7wzsm7nx7tnk',
    type: 'TOKEN_DOLLER',
    row: 0,
    start: 0,
    end: 1,
    code: '$',
  },
  {
    name: 'token',
    id: 'm9gk10h892wqi3h7yn',
    type: 'TOKEN_STRING',
    row: 0,
    start: 1,
    end: 2,
    code: 'a',
  },
  {
    name: 'token',
    id: 'm9gk10h8wxoofndhfx',
    type: 'TOKEN_DOT',
    row: 0,
    start: 2,
    end: 3,
    code: '.',
  },
  {
    name: 'token',
    id: 'm9gk10h8xs5ofzvt8v',
    type: 'TOKEN_STRING',
    row: 0,
    start: 3,
    end: 4,
    code: 'b',
  },
  {
    name: 'token',
    id: 'm9gk10h8g24emkr9ava',
    type: 'TOKEN_DOT',
    row: 0,
    start: 4,
    end: 5,
    code: '.',
  },
  {
    name: 'token',
    id: 'm9gk10h8vkef7wj07vk',
    type: 'TOKEN_STRING',
    row: 0,
    start: 5,
    end: 6,
    code: 'c',
  },
  {
    name: 'token',
    id: 'm9gk10h8m3jghl352z',
    type: 'TOKEN_SPACE',
    row: 0,
    start: 6,
    end: 7,
    code: ' ',
  },
  {
    name: 'token',
    id: 'm9gk10h84q4kjv9ov2p',
    type: 'TOKEN_ADD',
    row: 0,
    start: 7,
    end: 8,
    code: '+',
  },
  {
    name: 'token',
    id: 'm9gk10h8v7cbfy65icr',
    type: 'TOKEN_SPACE',
    row: 0,
    start: 8,
    end: 9,
    code: ' ',
  },
  {
    name: 'token',
    id: 'm9gk10h8a8j2fj50ic8',
    type: 'TOKEN_AT',
    row: 0,
    start: 9,
    end: 10,
    code: '@',
  },
  {
    name: 'token',
    id: 'm9gk10h8gr83yuyvg9l',
    type: 'TOKEN_STRING',
    row: 0,
    start: 10,
    end: 13,
    code: 'sum',
  },
  {
    name: 'token',
    id: 'm9gk10h8wx8csbna3wm',
    type: 'TOKEN_LEFT_SMALL_BRACKET',
    row: 0,
    start: 13,
    end: 14,
    code: '(',
  },
  {
    name: 'token',
    id: 'm9gk10h8u7aikznn2e',
    type: 'TOKEN_NUMBER',
    row: 0,
    start: 14,
    end: 16,
    code: '10',
    value: 10,
  },
  {
    name: 'token',
    id: 'm9gk10h8bc34atgmiwi',
    type: 'TOKEN_COMMA',
    row: 0,
    start: 16,
    end: 17,
    code: ',',
  },
  {
    name: 'token',
    id: 'm9gk10h8enrqfy4rg7',
    type: 'TOKEN_SPACE',
    row: 0,
    start: 17,
    end: 18,
    code: ' ',
  },
  {
    name: 'token',
    id: 'm9gk10h8m4uahtt40c',
    type: 'TOKEN_DOLLER',
    row: 0,
    start: 18,
    end: 19,
    code: '$',
  },
  {
    name: 'token',
    id: 'm9gk10h8tn5jb5vxd2',
    type: 'TOKEN_STRING',
    row: 0,
    start: 19,
    end: 20,
    code: 'a',
  },
  {
    name: 'token',
    id: 'm9gk10h8bwp45ysp8cs',
    type: 'TOKEN_DOT',
    row: 0,
    start: 20,
    end: 21,
    code: '.',
  },
  {
    name: 'token',
    id: 'm9gk10h8az8mkt0n588',
    type: 'TOKEN_STRING',
    row: 0,
    start: 21,
    end: 22,
    code: 'd',
  },
  {
    name: 'token',
    id: 'm9gk10h8n95pq56ffv',
    type: 'TOKEN_RIGHT_SMALL_BRACKET',
    row: 0,
    start: 22,
    end: 23,
    code: ')',
  },
  {
    name: 'token',
    id: 'm9gk10h8xbdirmgv2rs',
    type: 'TOKEN_SPACE',
    row: 0,
    start: 23,
    end: 24,
    code: ' ',
  },
  {
    name: 'token',
    id: 'm9gk10h8hvzcdsealwq',
    type: 'TOKEN_GTE',
    row: 0,
    start: 24,
    end: 26,
    code: '>=',
  },
  {
    name: 'token',
    id: 'm9gk10h8z7f6aszcuz',
    type: 'TOKEN_SPACE',
    row: 0,
    start: 26,
    end: 27,
    code: ' ',
  },
  {
    name: 'token',
    id: 'm9gk10h8hlpdhxuozv6',
    type: 'TOKEN_NUMBER',
    row: 0,
    start: 27,
    end: 31,
    code: '10.8',
    value: 10.8,
  },
]

语法分析

思路:分析词法分析的 token 列表,根据不同 token 的计算规则,生成抽象语法树(ast)

例如第一步中的词法分析结果经过语法分析后,输出的 ast 列表如下:

const ast = {
  syntaxRootIds: ['m9gkd1r1o4psgj8vfr'],
  syntaxMap: {
    m9gkd1r0xefkfoctoig: {
      name: 'syntax',
      id: 'm9gkd1r0xefkfoctoig',
      type: 'variable',
      triggerToken: {
        name: 'token',
        id: 'm9gkd1qz9lzyq3qo109',
        type: 'TOKEN_DOLLER',
        row: 0,
        start: 0,
        end: 1,
        code: '$',
      },
      pathTokens: [
        {
          name: 'token',
          id: 'm9gkd1qzdjcnnxjr8n4',
          type: 'TOKEN_STRING',
          row: 0,
          start: 1,
          end: 2,
          code: 'a',
        },
        {
          name: 'token',
          id: 'm9gkd1qzc3mk55coz6r',
          type: 'TOKEN_DOT',
          row: 0,
          start: 2,
          end: 3,
          code: '.',
        },
        {
          name: 'token',
          id: 'm9gkd1qzmellcc9mrrl',
          type: 'TOKEN_STRING',
          row: 0,
          start: 3,
          end: 4,
          code: 'b',
        },
        {
          name: 'token',
          id: 'm9gkd1qzfon4mgqqq5n',
          type: 'TOKEN_DOT',
          row: 0,
          start: 4,
          end: 5,
          code: '.',
        },
        {
          name: 'token',
          id: 'm9gkd1qzs4yu1mtyx9',
          type: 'TOKEN_STRING',
          row: 0,
          start: 5,
          end: 6,
          code: 'c',
        },
      ],
    },
    m9gkd1r0cusp2zfvd8l: {
      name: 'syntax',
      id: 'm9gkd1r0cusp2zfvd8l',
      type: 'function',
      triggerToken: {
        name: 'token',
        id: 'm9gkd1qzen8bysq2l2',
        type: 'TOKEN_AT',
        row: 0,
        start: 9,
        end: 10,
        code: '@',
      },
      nameTokens: [
        {
          name: 'token',
          id: 'm9gkd1qzo1dlf15smkc',
          type: 'TOKEN_STRING',
          row: 0,
          start: 10,
          end: 13,
          code: 'sum',
        },
      ],
      params: ['m9gkd1r0yap31mplokg', 'm9gkd1r0x6dl3vz469b', 'm9gkd1r039p01rtxgoo'],
    },
    m9gkd1r0yap31mplokg: {
      name: 'syntax',
      id: 'm9gkd1r0yap31mplokg',
      type: 'const',
      valueTokens: [
        {
          name: 'token',
          id: 'm9gkd1qzk0z4544992',
          type: 'TOKEN_NUMBER',
          row: 0,
          start: 14,
          end: 16,
          code: '10',
          value: 10,
        },
      ],
      constType: 'number',
    },
    m9gkd1r0x6dl3vz469b: {
      name: 'syntax',
      id: 'm9gkd1r0x6dl3vz469b',
      type: 'expression',
      token: {
        name: 'token',
        id: 'm9gkd1qznbtvay43ivt',
        type: 'TOKEN_COMMA',
        row: 0,
        start: 16,
        end: 17,
        code: ',',
      },
      children: [],
    },
    m9gkd1r039p01rtxgoo: {
      name: 'syntax',
      id: 'm9gkd1r039p01rtxgoo',
      type: 'variable',
      triggerToken: {
        name: 'token',
        id: 'm9gkd1qz3amjrxs8of1',
        type: 'TOKEN_DOLLER',
        row: 0,
        start: 18,
        end: 19,
        code: '$',
      },
      pathTokens: [
        {
          name: 'token',
          id: 'm9gkd1qz4g8w25a0ppj',
          type: 'TOKEN_STRING',
          row: 0,
          start: 19,
          end: 20,
          code: 'a',
        },
        {
          name: 'token',
          id: 'm9gkd1qz2vsxlg43zj',
          type: 'TOKEN_DOT',
          row: 0,
          start: 20,
          end: 21,
          code: '.',
        },
        {
          name: 'token',
          id: 'm9gkd1qzff1rexp4c3',
          type: 'TOKEN_STRING',
          row: 0,
          start: 21,
          end: 22,
          code: 'd',
        },
      ],
    },
    m9gkd1r0tx9gp3eh789: {
      name: 'syntax',
      id: 'm9gkd1r0tx9gp3eh789',
      type: 'expression',
      token: {
        name: 'token',
        id: 'm9gkd1qzv436woz7vm',
        type: 'TOKEN_LEFT_SMALL_BRACKET',
        row: 0,
        start: 13,
        end: 14,
        code: '(',
      },
      children: ['m9gkd1r0yap31mplokg', 'm9gkd1r0x6dl3vz469b', 'm9gkd1r039p01rtxgoo'],
    },
    m9gkd1r0qrqktl1wodi: {
      name: 'syntax',
      id: 'm9gkd1r0qrqktl1wodi',
      type: 'const',
      valueTokens: [
        {
          name: 'token',
          id: 'm9gkd1qzmwpzuwknnys',
          type: 'TOKEN_NUMBER',
          row: 0,
          start: 27,
          end: 31,
          code: '10.8',
          value: 10.8,
        },
      ],
      constType: 'number',
    },
    m9gkd1r18l46ifwtb0q: {
      name: 'syntax',
      id: 'm9gkd1r18l46ifwtb0q',
      type: 'expression',
      token: {
        name: 'token',
        id: 'm9gkd1qzl9v38nbyarh',
        type: 'TOKEN_ADD',
        row: 0,
        start: 7,
        end: 8,
        code: '+',
      },
      children: ['m9gkd1r0xefkfoctoig', 'm9gkd1r0cusp2zfvd8l'],
    },
    m9gkd1r1o4psgj8vfr: {
      name: 'syntax',
      id: 'm9gkd1r1o4psgj8vfr',
      type: 'expression',
      token: {
        name: 'token',
        id: 'm9gkd1qzbxmtv49kbld',
        type: 'TOKEN_GTE',
        row: 0,
        start: 24,
        end: 26,
        code: '>=',
      },
      children: ['m9gkd1r18l46ifwtb0q', 'm9gkd1r0qrqktl1wodi'],
    },
  },
}

语法检查

检查语法树中是否存在语法错误,例如变量未定义、函数未定义、函数参数类型不匹配、表达式规则不符合等;这一步可以保证公式的正确性,避免在计算表达式时出现错误

如何确定变量是否定义:遍历语法树时,若遇到变量类型的语法节点,则可以获取到变量的路径,再到上下文中查找该路径的变量是否存在即可

如何确定函数是否定义:遍历语法树时,若遇到函数类型的语法节点,则可以获取到函数的名称,再到上下文中查找该名称的函数是否存在即可

如何确定函数参数类型是否匹配:先计算出函数参数的类型,再获取到函数的定义,再对比函数参数的类型是否匹配函数定义的参数类型即可

如何确定表达式规则是否符合:这里的表达式指加减乘除等运算表达式,只需要在语法树中比对其子节点是否可以被改运算符计算即可

若语法检查通过,会返回每一个函数、表达式等的计算结果的数据类型

计算表达式

根据语法树以及上下文(变量、函数)来确定公式最终的计算结果

如何获取到变量的值:语法树中可以获取到变量的路径,进而可以在上下文中查找该路径的变量的值

如何获取到函数的值:语法树中可以获取到函数的名称,进而可以在上下文中查找该名称的函数的值

结语

通过以上的四个过程,可以轻松实现一个易扩展的公式编辑器,可以分析出公式中使用的变量及其类型,通过语法检查,确保公式的正确性,最后计算出表达式的值。

最后推荐一下低代码平台我的应用,可以直接去下载后私有化部署且完全免费。开源不易,望多多支持,也可通过平台提出宝贵意见,感谢!