|
|
@@ -0,0 +1,314 @@
|
|
|
+import type { Item, EvaluateDetail, EvaluateResult } from './types';
|
|
|
+import { parseExpression } from './parser';
|
|
|
+
|
|
|
+/** 运算符 → 中文描述 */
|
|
|
+const OPERATOR_DISPLAY: Record<string, string> = {
|
|
|
+ '>': '大于',
|
|
|
+ '>=': '大于等于',
|
|
|
+ '<': '小于',
|
|
|
+ '<=': '小于等于',
|
|
|
+ '==': '等于',
|
|
|
+ '!=': '不等于',
|
|
|
+};
|
|
|
+
|
|
|
+/**
|
|
|
+ * 将表达式中的变量名替换为实际数值,返回新的 token 列表。
|
|
|
+ * 若提供了 displayMap,会用中文标签标记替换后的数字 token。
|
|
|
+ */
|
|
|
+function substituteValues(
|
|
|
+ tokens: Item[],
|
|
|
+ variableValues: Record<string, number>,
|
|
|
+ displayMap?: Record<string, string>,
|
|
|
+): Item[] {
|
|
|
+ return tokens.map((t) => {
|
|
|
+ if (t.type === 'operand' && t.value in variableValues) {
|
|
|
+ const label = displayMap?.[t.value] || t.label || t.value;
|
|
|
+ return { type: 'number', value: String(variableValues[t.value]), label };
|
|
|
+ }
|
|
|
+ return { ...t };
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 简易算术求值 — 支持 + - * / 和括号(不含函数调用)
|
|
|
+ */
|
|
|
+class ArithmeticEvaluator {
|
|
|
+ private pos: number;
|
|
|
+ private tokens: Item[];
|
|
|
+
|
|
|
+ constructor(tokens: Item[]) {
|
|
|
+ this.tokens = tokens;
|
|
|
+ this.pos = 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ private peek(): Item | null {
|
|
|
+ return this.pos < this.tokens.length ? this.tokens[this.pos] : null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private consume(): Item {
|
|
|
+ return this.tokens[this.pos++];
|
|
|
+ }
|
|
|
+
|
|
|
+ parseAddSub(): number {
|
|
|
+ let left = this.parseMulDiv();
|
|
|
+ while (this.peek() && (this.peek()!.value === '+' || this.peek()!.value === '-')) {
|
|
|
+ const op = this.consume().value;
|
|
|
+ const right = this.parseMulDiv();
|
|
|
+ if (op === '+') left += right;
|
|
|
+ else left -= right;
|
|
|
+ }
|
|
|
+ return left;
|
|
|
+ }
|
|
|
+
|
|
|
+ private parseMulDiv(): number {
|
|
|
+ let left = this.parseAtom();
|
|
|
+ while (this.peek() && (this.peek()!.value === '*' || this.peek()!.value === '/')) {
|
|
|
+ const op = this.consume().value;
|
|
|
+ const right = this.parseAtom();
|
|
|
+ if (op === '*') left *= right;
|
|
|
+ else left /= right;
|
|
|
+ }
|
|
|
+ return left;
|
|
|
+ }
|
|
|
+
|
|
|
+ private parseAtom(): number {
|
|
|
+ const token = this.peek();
|
|
|
+ if (!token) throw new Error('意外的表达式结尾');
|
|
|
+
|
|
|
+ if (token.type === 'number') {
|
|
|
+ this.consume();
|
|
|
+ return parseFloat(token.value);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (token.type === 'leftParen') {
|
|
|
+ this.consume();
|
|
|
+ const val = this.parseAddSub();
|
|
|
+ if (this.peek()?.type !== 'rightParen') throw new Error('缺少右括号');
|
|
|
+ this.consume();
|
|
|
+ return val;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (token.type === 'operator' && token.value === '-') {
|
|
|
+ this.consume();
|
|
|
+ return -this.parseAtom();
|
|
|
+ }
|
|
|
+
|
|
|
+ throw new Error(`无法识别的操作数: ${token.value}`);
|
|
|
+ }
|
|
|
+
|
|
|
+ evaluate(): number {
|
|
|
+ return this.parseAddSub();
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function evaluateComparison(left: number, operator: string, right: number): boolean {
|
|
|
+ switch (operator) {
|
|
|
+ case '>': return left > right;
|
|
|
+ case '>=': return left >= right;
|
|
|
+ case '<': return left < right;
|
|
|
+ case '<=': return left <= right;
|
|
|
+ case '==': return left === right;
|
|
|
+ case '!=': return left !== right;
|
|
|
+ default: throw new Error(`不支持的比较运算符: ${operator}`);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 比较运算求值器 — 同时记录每个原子条件的求值详情(含可读原因)。
|
|
|
+ */
|
|
|
+class ComparisonExtractor {
|
|
|
+ private tokens: Item[];
|
|
|
+ private pos: number;
|
|
|
+ private details: EvaluateDetail[];
|
|
|
+ private displayMap: Record<string, string>;
|
|
|
+
|
|
|
+ constructor(tokens: Item[], displayMap: Record<string, string>) {
|
|
|
+ this.tokens = tokens;
|
|
|
+ this.pos = 0;
|
|
|
+ this.details = [];
|
|
|
+ this.displayMap = displayMap;
|
|
|
+ }
|
|
|
+
|
|
|
+ private peek(): Item | null {
|
|
|
+ return this.pos < this.tokens.length ? this.tokens[this.pos] : null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private consume(): Item {
|
|
|
+ return this.tokens[this.pos++];
|
|
|
+ }
|
|
|
+
|
|
|
+ private hasMore(): boolean {
|
|
|
+ return this.pos < this.tokens.length;
|
|
|
+ }
|
|
|
+
|
|
|
+ parseOr(): boolean {
|
|
|
+ let left = this.parseAnd();
|
|
|
+ while (this.peek() && this.peek()!.type === 'logic' && this.peek()!.value === '||') {
|
|
|
+ this.consume();
|
|
|
+ const right = this.parseAnd();
|
|
|
+ left = left || right;
|
|
|
+ }
|
|
|
+ return left;
|
|
|
+ }
|
|
|
+
|
|
|
+ private parseAnd(): boolean {
|
|
|
+ let left = this.parseRelation();
|
|
|
+ while (this.peek() && this.peek()!.type === 'logic' && this.peek()!.value === '&&') {
|
|
|
+ this.consume();
|
|
|
+ const right = this.parseRelation();
|
|
|
+ left = left && right;
|
|
|
+ }
|
|
|
+ return left;
|
|
|
+ }
|
|
|
+
|
|
|
+ private parseRelation(): boolean {
|
|
|
+ const token = this.peek();
|
|
|
+ if (!token) throw new Error('意外的表达式结尾');
|
|
|
+
|
|
|
+ if (token.type === 'leftParen') {
|
|
|
+ this.consume();
|
|
|
+ const val = this.parseOr();
|
|
|
+ if (this.peek()?.type !== 'rightParen') throw new Error('缺少右括号');
|
|
|
+ this.consume();
|
|
|
+ return val;
|
|
|
+ }
|
|
|
+
|
|
|
+ return this.parseAtomicComparison();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 将 token 列表渲染为人类可读的文本
|
|
|
+ * - 数字 token 优先使用其 label(替换变量时设的中文名,如"压差")
|
|
|
+ * - 运算符、逻辑符、操作数均尝试通过 displayMap 转为中文(如 >= → ≥, && → 且)
|
|
|
+ * - 无映射的 token 回退显示原文
|
|
|
+ */
|
|
|
+ private renderTokens(tokens: Item[]): string {
|
|
|
+ return tokens
|
|
|
+ .map((t) => {
|
|
|
+ // 数字 token 优先使用 label(替换变量时设的中文名)
|
|
|
+ if (t.type === 'number' && t.label) return t.label;
|
|
|
+ // 运算符尝试走 displayMap(>= → ≥)
|
|
|
+ if (t.type === 'operator') return this.displayMap[t.value] || t.value;
|
|
|
+ // 逻辑符
|
|
|
+ if (t.type === 'logic') return this.displayMap[t.value] || t.value;
|
|
|
+ // 操作数
|
|
|
+ if (t.type === 'operand') return this.displayMap[t.value] || t.label || t.value;
|
|
|
+ return t.value;
|
|
|
+ })
|
|
|
+ .join(' ');
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 解析并求值一个原子比较条件,同时记录可读的 condition 和失败原因 */
|
|
|
+ private parseAtomicComparison(): boolean {
|
|
|
+ const comparisonOps = new Set(['>', '>=', '<', '<=', '==', '!=']);
|
|
|
+
|
|
|
+ // 读取左侧算术表达式
|
|
|
+ const leftTokens: Item[] = [];
|
|
|
+ while (this.hasMore()) {
|
|
|
+ const t = this.peek()!;
|
|
|
+ if (t.type === 'logic' && (t.value === '&&' || t.value === '||')) break;
|
|
|
+ if (t.type === 'leftParen' || t.type === 'rightParen') break;
|
|
|
+ if (t.type === 'operator' && comparisonOps.has(t.value)) break;
|
|
|
+ leftTokens.push(this.consume());
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!this.peek() || !(this.peek()!.type === 'operator' && comparisonOps.has(this.peek()!.value))) {
|
|
|
+ const leftVal = new ArithmeticEvaluator(leftTokens).evaluate();
|
|
|
+ return leftVal !== 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ const opToken = this.consume();
|
|
|
+ const operator = opToken.value;
|
|
|
+
|
|
|
+ // 读取右侧算术表达式
|
|
|
+ const rightTokens: Item[] = [];
|
|
|
+ while (this.hasMore()) {
|
|
|
+ const t = this.peek()!;
|
|
|
+ if (t.type === 'logic' && (t.value === '&&' || t.value === '||')) break;
|
|
|
+ if (t.type === 'leftParen' || t.type === 'rightParen') break;
|
|
|
+ rightTokens.push(this.consume());
|
|
|
+ }
|
|
|
+
|
|
|
+ // 渲染为可读文本
|
|
|
+ const leftStr = this.renderTokens(leftTokens);
|
|
|
+ const rightStr = this.renderTokens(rightTokens);
|
|
|
+ const operatorStr = OPERATOR_DISPLAY[operator] || operator;
|
|
|
+
|
|
|
+ // 求值
|
|
|
+ const leftVal = new ArithmeticEvaluator(leftTokens).evaluate();
|
|
|
+ const rightVal = new ArithmeticEvaluator(rightTokens).evaluate();
|
|
|
+ const passes = evaluateComparison(leftVal, operator, rightVal);
|
|
|
+
|
|
|
+ const condition = `${leftStr} ${this.displayMap[operator] || operator} ${rightStr}`;
|
|
|
+
|
|
|
+ if (passes) {
|
|
|
+ this.details.push({ condition, passes: true });
|
|
|
+ } else {
|
|
|
+ this.details.push({
|
|
|
+ condition,
|
|
|
+ passes: false,
|
|
|
+ reason: `${leftStr}当前为 ${leftVal},不满足${operatorStr}${rightStr}的条件`,
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ return passes;
|
|
|
+ }
|
|
|
+
|
|
|
+ getDetails(): EvaluateDetail[] {
|
|
|
+ return this.details;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 根据传入的变量值,判断表达式是否能通过,返回每个条件的详细原因。
|
|
|
+ *
|
|
|
+ * @param expr - 表达式字符串,例如 "sourcePressure > 500 && sourcePressure < 2000"
|
|
|
+ * @param variableValues - 变量名到数值的映射,例如 { sourcePressure: 1000, coVal: 5 }
|
|
|
+ * @param operandSet - 已知操作数集合(由 buildOperandSet 生成)
|
|
|
+ * @param displayMap - 可选的变量名/运算符 → 中文标签映射,用于生成可读原因
|
|
|
+ * @returns 求值结果,含每个条件的通过状态和可读原因
|
|
|
+ *
|
|
|
+ * @example
|
|
|
+ * // displayMap 传入 DEFAULT_DISPLAY_MAP 可使原因使用中文描述
|
|
|
+ * evaluateExpression("sourcePressure > 500", { sourcePressure: 100 }, operandSet, displayMap)
|
|
|
+ * // => {
|
|
|
+ * // passes: false,
|
|
|
+ * // details: [{ condition: "压差 > 500", passes: false, reason: "压差当前为 100,不满足大于 500 的条件" }],
|
|
|
+ * // summary: "条件不满足(0/1): 压差当前为 100,不满足大于 500 的条件"
|
|
|
+ * // }
|
|
|
+ */
|
|
|
+export function evaluateExpression(
|
|
|
+ expr: string,
|
|
|
+ variableValues: Record<string, number>,
|
|
|
+ operandSet: Set<string>,
|
|
|
+ displayMap?: Record<string, string>,
|
|
|
+): EvaluateResult {
|
|
|
+ const tokens = parseExpression(expr, operandSet);
|
|
|
+ if (tokens.length === 0) {
|
|
|
+ return { passes: false, details: [], summary: '表达式为空或无法解析' };
|
|
|
+ }
|
|
|
+
|
|
|
+ const substituted = substituteValues(tokens, variableValues, displayMap);
|
|
|
+
|
|
|
+ try {
|
|
|
+ const extractor = new ComparisonExtractor(substituted, displayMap || {});
|
|
|
+ const passes = extractor.parseOr();
|
|
|
+ const details = extractor.getDetails();
|
|
|
+
|
|
|
+ const passedCount = details.filter((d) => d.passes).length;
|
|
|
+ const failedDetails = details.filter((d) => !d.passes);
|
|
|
+
|
|
|
+ let summary: string;
|
|
|
+ if (passes) {
|
|
|
+ summary = `全部 ${details.length} 个条件满足`;
|
|
|
+ } else {
|
|
|
+ const reasons = failedDetails.map((d) => d.reason || d.condition).join(';');
|
|
|
+ summary = `条件不满足(${passedCount}/${details.length}): ${reasons}`;
|
|
|
+ }
|
|
|
+
|
|
|
+ return { passes, details, summary };
|
|
|
+ } catch (e: unknown) {
|
|
|
+ const message = e instanceof Error ? e.message : String(e);
|
|
|
+ return { passes: false, details: [], summary: `表达式求值出错: ${message}` };
|
|
|
+ }
|
|
|
+}
|