/** * S-FEEL 子集的求值 —— 纯函数,零 IO、零外部调用(编码规范 §8.3;定义域 OS 03)。 * * 求值输入(scope):网关处 = 流转载荷与流程变量;触发器处 = 事件信封与载荷字段; * 监听处同触发器。**后端裁决为权威**,前端用同一份代码做即时预览。 */ import type { Node, UnaryTest } from './ast.ts'; import { parse, parseUnaryTest } from './parser.ts'; export type Scope = Readonly>; export class SfeelEvalError extends Error { constructor(message: string) { super(`S-FEEL 求值错误:${message}`); this.name = 'SfeelEvalError'; } } /** 内建函数表 —— 封闭集合,**不可扩展**(无外部调用,静态可检) */ const FUNCTIONS: Readonly unknown>> = { // 字符串 'upper case': ([s]) => String(s ?? '').toUpperCase(), 'lower case': ([s]) => String(s ?? '').toLowerCase(), contains: ([s, sub]) => String(s ?? '').includes(String(sub ?? '')), 'starts with': ([s, sub]) => String(s ?? '').startsWith(String(sub ?? '')), 'ends with': ([s, sub]) => String(s ?? '').endsWith(String(sub ?? '')), 'string length': ([s]) => String(s ?? '').length, substring: ([s, from, len]) => { const text = String(s ?? ''); const start = Math.max(Number(from ?? 1) - 1, 0); return len === undefined ? text.slice(start) : text.slice(start, start + Number(len)); }, string: ([v]) => (v === null || v === undefined ? '' : String(v)), // 数字 abs: ([n]) => Math.abs(toNumber(n)), floor: ([n]) => Math.floor(toNumber(n)), ceiling: ([n]) => Math.ceil(toNumber(n)), round: ([n, d]) => { const factor = 10 ** Number(d ?? 0); return Math.round(toNumber(n) * factor) / factor; }, min: (args) => Math.min(...flatten(args).map(toNumber)), max: (args) => Math.max(...flatten(args).map(toNumber)), sum: (args) => flatten(args).reduce((acc, v) => acc + toNumber(v), 0), count: (args) => flatten(args).length, number: ([v]) => toNumber(v), // 日期:date("2026-07-27") / date and time("2026-07-27T10:00:00Z") date: ([v]) => toDate(v), 'date and time': ([v]) => toDate(v), // 天数差(整天),用于时限与偏移计算 'days and time duration': ([a, b]) => (toDate(b).getTime() - toDate(a).getTime()) / 86_400_000, // 列表 list: (args) => flatten(args), }; export const BUILTIN_FUNCTIONS = Object.keys(FUNCTIONS); export function evaluateNode(node: Node, scope: Scope): unknown { switch (node.kind) { case 'literal': return node.value; case 'path': { let current: unknown = scope; for (const segment of node.segments) { if (current === null || current === undefined) return undefined; if (typeof current !== 'object') return undefined; current = (current as Record)[segment]; } return current; } case 'unary': if (node.op === '-') return -toNumber(evaluateNode(node.operand, scope)); return !truthy(evaluateNode(node.operand, scope)); case 'binary': return evaluateBinary(node, scope); case 'list': return node.items.map((item) => evaluateNode(item, scope)); case 'range': return { __range: true, startOpen: node.startOpen, endOpen: node.endOpen, start: evaluateNode(node.start, scope), end: evaluateNode(node.end, scope), }; case 'in': { const value = evaluateNode(node.value, scope); const candidates = evaluateNode(node.candidates, scope); return inCandidates(value, candidates); } case 'call': { const fn = FUNCTIONS[node.name]; if (fn === undefined) throw new SfeelEvalError(`未知函数 ${node.name}(子集封顶,不可扩展)`); return fn(node.args.map((arg) => evaluateNode(arg, scope))); } } } function evaluateBinary(node: Extract, scope: Scope): unknown { // 短路:and / or if (node.op === 'and') { return truthy(evaluateNode(node.left, scope)) ? truthy(evaluateNode(node.right, scope)) : false; } if (node.op === 'or') { return truthy(evaluateNode(node.left, scope)) ? true : truthy(evaluateNode(node.right, scope)); } const left = evaluateNode(node.left, scope); const right = evaluateNode(node.right, scope); switch (node.op) { case '+': return typeof left === 'string' || typeof right === 'string' ? `${String(left ?? '')}${String(right ?? '')}` : toNumber(left) + toNumber(right); case '-': return toNumber(left) - toNumber(right); case '*': return toNumber(left) * toNumber(right); case '/': { const divisor = toNumber(right); if (divisor === 0) throw new SfeelEvalError('除零'); return toNumber(left) / divisor; } case '=': return equals(left, right); case '!=': return !equals(left, right); case '<': return compare(left, right) < 0; case '<=': return compare(left, right) <= 0; case '>': return compare(left, right) > 0; case '>=': return compare(left, right) >= 0; } } /** 布尔求值:表达式作条件时用它;非布尔一律按 JS 真值,但 null / undefined 为假 */ export function truthy(value: unknown): boolean { if (value === null || value === undefined) return false; if (typeof value === 'boolean') return value; if (typeof value === 'number') return !Number.isNaN(value); if (typeof value === 'string') return value !== ''; return true; } function equals(a: unknown, b: unknown): boolean { if (a === b) return true; if (a === null || a === undefined || b === null || b === undefined) return false; if (a instanceof Date || b instanceof Date) return toDate(a).getTime() === toDate(b).getTime(); if (typeof a === 'number' || typeof b === 'number') { const na = Number(a); const nb = Number(b); if (!Number.isNaN(na) && !Number.isNaN(nb)) return na === nb; } return String(a) === String(b); } function compare(a: unknown, b: unknown): number { if (a instanceof Date || b instanceof Date) { return toDate(a).getTime() - toDate(b).getTime(); } if (typeof a === 'string' && typeof b === 'string' && Number.isNaN(Number(a))) { return a < b ? -1 : a > b ? 1 : 0; } return toNumber(a) - toNumber(b); } function inCandidates(value: unknown, candidates: unknown): boolean { if (isRange(candidates)) return withinRange(value, candidates); if (Array.isArray(candidates)) return candidates.some((c) => (isRange(c) ? withinRange(value, c) : equals(value, c))); return equals(value, candidates); } type RangeValue = { __range: true; startOpen: boolean; endOpen: boolean; start: unknown; end: unknown }; function isRange(value: unknown): value is RangeValue { return typeof value === 'object' && value !== null && (value as { __range?: boolean }).__range === true; } function withinRange(value: unknown, range: RangeValue): boolean { const low = compare(value, range.start); const high = compare(value, range.end); return (range.startOpen ? low > 0 : low >= 0) && (range.endOpen ? high < 0 : high <= 0); } function toNumber(value: unknown): number { if (typeof value === 'number') return value; if (typeof value === 'boolean') return value ? 1 : 0; if (value === null || value === undefined || value === '') return Number.NaN; const n = Number(value); return n; } function toDate(value: unknown): Date { if (value instanceof Date) return value; const date = new Date(String(value)); if (Number.isNaN(date.getTime())) throw new SfeelEvalError(`不是合法日期:${String(value)}`); return date; } function flatten(args: readonly unknown[]): unknown[] { return args.flatMap((a) => (Array.isArray(a) ? a : [a])); } /* ── 对外三个入口 ──────────────────────────────────────────────────── */ /** 求值任意表达式 */ export function evaluate(expression: string, scope: Scope): unknown { return evaluateNode(parse(expression), scope); } /** 条件求值 —— 网关、监听、触发器都用它;非布尔结果按 truthy 归一 */ export function evaluateCondition(expression: string, scope: Scope): boolean { return truthy(evaluate(expression, scope)); } /** DMN 单元格谓词:输入值是否命中该格 */ export function evaluateUnaryTest(cell: string, value: unknown, scope: Scope = {}): boolean { return testMatches(parseUnaryTest(cell), value, scope); } export function testMatches(test: UnaryTest, value: unknown, scope: Scope): boolean { switch (test.kind) { case 'any': return true; case 'disjunction': return test.tests.some((t) => testMatches(t, value, scope)); case 'range': return withinRange(value, evaluateNode(test.node, scope) as RangeValue); case 'compare': { const operand = evaluateNode(test.operand, scope); if (test.op === '=') return isRange(operand) ? withinRange(value, operand) : equals(value, operand); if (test.op === '!=') return !equals(value, operand); const c = compare(value, operand); return test.op === '<' ? c < 0 : test.op === '<=' ? c <= 0 : test.op === '>' ? c > 0 : c >= 0; } } }