这篇文章主要介绍公式编辑器核心包部分的实现,包括词法分析、语法分析、语法检查、计算表达式
相关文章
上一篇文章中介绍了公式编辑器的基本使用方法:(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'],
},
},
}
语法检查
检查语法树中是否存在语法错误,例如变量未定义、函数未定义、函数参数类型不匹配、表达式规则不符合等;这一步可以保证公式的正确性,避免在计算表达式时出现错误
如何确定变量是否定义:遍历语法树时,若遇到变量类型的语法节点,则可以获取到变量的路径,再到上下文中查找该路径的变量是否存在即可
如何确定函数是否定义:遍历语法树时,若遇到函数类型的语法节点,则可以获取到函数的名称,再到上下文中查找该名称的函数是否存在即可
如何确定函数参数类型是否匹配:先计算出函数参数的类型,再获取到函数的定义,再对比函数参数的类型是否匹配函数定义的参数类型即可
如何确定表达式规则是否符合:这里的表达式指加减乘除等运算表达式,只需要在语法树中比对其子节点是否可以被改运算符计算即可
若语法检查通过,会返回每一个函数、表达式等的计算结果的数据类型
计算表达式
根据语法树以及上下文(变量、函数)来确定公式最终的计算结果
如何获取到变量的值:语法树中可以获取到变量的路径,进而可以在上下文中查找该路径的变量的值
如何获取到函数的值:语法树中可以获取到函数的名称,进而可以在上下文中查找该名称的函数的值
结语
通过以上的四个过程,可以轻松实现一个易扩展的公式编辑器,可以分析出公式中使用的变量及其类型,通过语法检查,确保公式的正确性,最后计算出表达式的值。
最后推荐一下低代码平台我的应用,可以直接去下载后私有化部署且完全免费。开源不易,望多多支持,也可通过平台提出宝贵意见,感谢!