expression-evaluator.js 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  1. /**
  2. * 表达式与条件求值:算术表达式、条件表达式(不依赖 eval)
  3. */
  4. const { parseValue, resolveValue } = require('./actions/set-parser.js')
  5. /** 在 str 中查找 needle 的起始下标,忽略单/双引号字符串内的匹配(支持 \\ 转义) */
  6. function findOutsideQuotes(str, needle, fromIndex = 0) {
  7. let i = fromIndex
  8. let inSingle = false
  9. let inDouble = false
  10. while (i < str.length) {
  11. const c = str[i]
  12. if (inSingle) {
  13. if (c === '\\' && i + 1 < str.length) {
  14. i += 2
  15. continue
  16. }
  17. if (c === "'") inSingle = false
  18. i++
  19. continue
  20. }
  21. if (inDouble) {
  22. if (c === '\\' && i + 1 < str.length) {
  23. i += 2
  24. continue
  25. }
  26. if (c === '"') inDouble = false
  27. i++
  28. continue
  29. }
  30. if (c === "'") {
  31. inSingle = true
  32. i++
  33. continue
  34. }
  35. if (c === '"') {
  36. inDouble = true
  37. i++
  38. continue
  39. }
  40. if (str.startsWith(needle, i)) return i
  41. i++
  42. }
  43. return -1
  44. }
  45. function splitTopLevelFirst(expr, delimiter) {
  46. const idx = findOutsideQuotes(expr, delimiter)
  47. if (idx === -1) return null
  48. return [expr.slice(0, idx).trim(), expr.slice(idx + delimiter.length).trim()]
  49. }
  50. /**
  51. * 在引号外查找第一个比较运算符(长运算符优先,避免 >= 被拆成 >)
  52. */
  53. function findBinaryComparisonOp(expr, operators) {
  54. const sorted = [...operators].sort((a, b) => b.op.length - a.op.length)
  55. let i = 0
  56. let inSingle = false
  57. let inDouble = false
  58. while (i < expr.length) {
  59. const c = expr[i]
  60. if (inSingle) {
  61. if (c === '\\' && i + 1 < expr.length) {
  62. i += 2
  63. continue
  64. }
  65. if (c === "'") inSingle = false
  66. i++
  67. continue
  68. }
  69. if (inDouble) {
  70. if (c === '\\' && i + 1 < expr.length) {
  71. i += 2
  72. continue
  73. }
  74. if (c === '"') inDouble = false
  75. i++
  76. continue
  77. }
  78. if (c === "'") {
  79. inSingle = true
  80. i++
  81. continue
  82. }
  83. if (c === '"') {
  84. inDouble = true
  85. i++
  86. continue
  87. }
  88. for (const row of sorted) {
  89. if (expr.startsWith(row.op, i)) return { index: i, op: row.op, fn: row.fn }
  90. }
  91. i++
  92. }
  93. return null
  94. }
  95. function parseArithmeticExpression(expr) {
  96. let index = 0
  97. const skipWhitespace = () => {
  98. while (index < expr.length && /\s/.test(expr[index])) index++
  99. }
  100. const parseNumber = () => {
  101. skipWhitespace()
  102. let numStr = ''
  103. let hasDot = false
  104. while (index < expr.length) {
  105. const c = expr[index]
  106. if (c >= '0' && c <= '9') { numStr += c; index++ }
  107. else if (c === '.' && !hasDot) { numStr += c; hasDot = true; index++ }
  108. else break
  109. }
  110. if (!numStr) throw new Error('期望数字')
  111. const n = parseFloat(numStr)
  112. if (isNaN(n)) throw new Error(`无效的数字: ${numStr}`)
  113. return n
  114. }
  115. const parseFactor = () => {
  116. skipWhitespace()
  117. if (index >= expr.length) throw new Error('表达式不完整')
  118. let neg = false
  119. if (expr[index] === '-') { neg = true; index++; skipWhitespace() }
  120. else if (expr[index] === '+') { index++; skipWhitespace() }
  121. let r
  122. if (expr[index] === '(') {
  123. index++
  124. r = parseExpression()
  125. skipWhitespace()
  126. if (index >= expr.length || expr[index] !== ')') throw new Error('缺少右括号')
  127. index++
  128. } else {
  129. r = parseNumber()
  130. }
  131. return neg ? -r : r
  132. }
  133. const parseTerm = () => {
  134. let r = parseFactor()
  135. skipWhitespace()
  136. while (index < expr.length) {
  137. const op = expr[index]
  138. if (op === '*') { index++; r *= parseFactor() }
  139. else if (op === '/') { index++; const d = parseFactor(); if (d === 0) throw new Error('除以零'); r /= d }
  140. else break
  141. skipWhitespace()
  142. }
  143. return r
  144. }
  145. const parseExpression = () => {
  146. let r = parseTerm()
  147. skipWhitespace()
  148. while (index < expr.length) {
  149. const op = expr[index]
  150. if (op === '+') { index++; r += parseTerm() }
  151. else if (op === '-') { index++; r -= parseTerm() }
  152. else break
  153. skipWhitespace()
  154. }
  155. return r
  156. }
  157. const r = parseExpression()
  158. skipWhitespace()
  159. if (index < expr.length) throw new Error(`表达式解析不完整: ${expr.substring(index)}`)
  160. return r
  161. }
  162. function evaluateExpression(expression, context) {
  163. if (typeof expression !== 'string') return expression
  164. try {
  165. let expr = expression.trim()
  166. const varPattern = /\{(\w+)\}(?!\})/g
  167. const originalExpr = expr
  168. let hasVars = false
  169. expr = expr.replace(varPattern, (match, varName) => {
  170. hasVars = true
  171. const v = context[varName]
  172. if (v === undefined || v === null) return '0'
  173. if (typeof v === 'number') return String(v)
  174. if (typeof v === 'boolean') return v ? '1' : '0'
  175. if (typeof v === 'string') {
  176. const n = Number(v)
  177. if (!isNaN(n) && v.trim() !== '') return String(n)
  178. return '0'
  179. }
  180. const n = Number(v)
  181. return !isNaN(n) ? String(n) : '0'
  182. })
  183. if (!hasVars && !/[+\-*/]/.test(expr)) {
  184. const n = Number(expr)
  185. if (!isNaN(n) && expr.trim() !== '') return n
  186. return expr
  187. }
  188. if (!/[+\-*/]/.test(expr)) {
  189. const n = Number(expr)
  190. if (!isNaN(n) && expr.trim() !== '') return n
  191. return expr
  192. }
  193. expr = expr.replace(/\s+/g, '')
  194. if (!/^[0-9+\-*/().]+$/.test(expr)) return resolveValue(originalExpr, context)
  195. if (!/^[0-9(]/.test(expr) || !/[0-9)]$/.test(expr)) return resolveValue(originalExpr, context)
  196. const result = parseArithmeticExpression(expr)
  197. if (typeof result === 'number' && !isNaN(result) && isFinite(result)) return result
  198. return resolveValue(originalExpr, context)
  199. } catch (e) {
  200. return resolveValue(expression, context)
  201. }
  202. }
  203. function parseConditionExpression(expr) {
  204. expr = expr.trim()
  205. const orPart = splitTopLevelFirst(expr, '||')
  206. if (orPart) {
  207. return parseConditionExpression(orPart[0]) || parseConditionExpression(orPart[1])
  208. }
  209. const andPart = splitTopLevelFirst(expr, '&&')
  210. if (andPart) {
  211. return parseConditionExpression(andPart[0]) && parseConditionExpression(andPart[1])
  212. }
  213. const operators = [
  214. { op: '!=', fn: (a, b) => a != b },
  215. { op: '==', fn: (a, b) => {
  216. if (typeof a === 'string' && typeof b !== 'string') b = String(b)
  217. else if (typeof b === 'string' && typeof a !== 'string') a = String(a)
  218. if (a === '' && b === '') return true
  219. if ((a === '' && typeof b === 'string') || (b === '' && typeof a === 'string')) {
  220. try {
  221. const p = JSON.parse(a === '' ? b : a)
  222. if (Array.isArray(p)) return p.length === 0
  223. } catch (e) {}
  224. }
  225. return a == b
  226. }},
  227. { op: '>=', fn: (a, b) => Number(a) >= Number(b) },
  228. { op: '<=', fn: (a, b) => Number(a) <= Number(b) },
  229. { op: '>', fn: (a, b) => Number(a) > Number(b) },
  230. { op: '<', fn: (a, b) => Number(a) < Number(b) },
  231. ]
  232. const binary = findBinaryComparisonOp(expr, operators)
  233. if (binary) {
  234. const left = expr.slice(0, binary.index).trim()
  235. const right = expr.slice(binary.index + binary.op.length).trim()
  236. return binary.fn(parseValue(left), parseValue(right))
  237. }
  238. const value = parseValue(expr)
  239. if (typeof value === 'boolean') return value
  240. if (typeof value === 'string') {
  241. if (value === '' || value === 'undefined') return false
  242. try {
  243. const p = JSON.parse(value)
  244. if (Array.isArray(p)) return p.length > 0
  245. if (typeof p === 'object' && Object.keys(p).length === 0) return false
  246. } catch (e) {}
  247. return true
  248. }
  249. if (typeof value === 'number') return value !== 0
  250. return Boolean(value)
  251. }
  252. function evaluateCondition(condition, context) {
  253. if (!condition) return true
  254. try {
  255. let expr = condition
  256. const varPattern = /\{([\w-]+)\}/g
  257. expr = expr.replace(varPattern, (match, varName) => {
  258. const v = context[varName]
  259. if (v === undefined || v === null || v === '' || v === 'undefined' || v === 'null') return '""'
  260. if (typeof v === 'string') {
  261. const trimmed = v.trim()
  262. if (trimmed === '' || trimmed === 'undefined' || trimmed === 'null') return '""'
  263. try {
  264. const p = JSON.parse(trimmed)
  265. if (Array.isArray(p)) return `"${v.replace(/"/g, '\\"')}"`
  266. } catch (e) {}
  267. return `"${v.replace(/"/g, '\\"')}"`
  268. }
  269. if (Array.isArray(v)) {
  270. try {
  271. return `"${JSON.stringify(v).replace(/"/g, '\\"')}"`
  272. } catch (e) { return '"[]"' }
  273. }
  274. if (typeof v === 'number' || typeof v === 'boolean') return v
  275. return `"${String(v)}"`
  276. })
  277. return parseConditionExpression(expr)
  278. } catch (e) {
  279. return false
  280. }
  281. }
  282. module.exports = {
  283. parseArithmeticExpression,
  284. evaluateExpression,
  285. evaluateCondition,
  286. parseConditionExpression,
  287. }