|
|
@@ -1,285 +1,110 @@
|
|
|
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 };
|
|
|
- });
|
|
|
+// ─── token 渲染 ─────────────────────────────────────────────
|
|
|
+
|
|
|
+function renderTokens(tokens: Item[], displayMap: Record<string, string>): string {
|
|
|
+ return tokens
|
|
|
+ .map((t) => {
|
|
|
+ if (t.type === 'number' && t.label) return t.label;
|
|
|
+ if (t.type === 'operator') return displayMap[t.value] || t.value;
|
|
|
+ if (t.type === 'logic') return displayMap[t.value] || t.value;
|
|
|
+ if (t.type === 'operand') return displayMap[t.value] || t.label || t.value;
|
|
|
+ return t.value;
|
|
|
+ })
|
|
|
+ .join(' ');
|
|
|
}
|
|
|
|
|
|
-/**
|
|
|
- * 简易算术求值 — 支持 + - * / 和括号(不含函数调用)
|
|
|
- */
|
|
|
-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('意外的表达式结尾');
|
|
|
+// ─── OR 分支拆分 ────────────────────────────────────────────
|
|
|
|
|
|
- if (token.type === 'number') {
|
|
|
- this.consume();
|
|
|
- return parseFloat(token.value);
|
|
|
- }
|
|
|
+function splitOrBranches(tokens: Item[]): Item[][] {
|
|
|
+ const branches: Item[][] = [];
|
|
|
+ let current: Item[] = [];
|
|
|
+ let depth = 0;
|
|
|
|
|
|
- if (token.type === 'leftParen') {
|
|
|
- this.consume();
|
|
|
- const val = this.parseAddSub();
|
|
|
- if (this.peek()?.type !== 'rightParen') throw new Error('缺少右括号');
|
|
|
- this.consume();
|
|
|
- return val;
|
|
|
- }
|
|
|
+ for (const t of tokens) {
|
|
|
+ if (t.type === 'leftParen') depth++;
|
|
|
+ else if (t.type === 'rightParen') depth--;
|
|
|
|
|
|
- if (token.type === 'operator' && token.value === '-') {
|
|
|
- this.consume();
|
|
|
- return -this.parseAtom();
|
|
|
+ if (depth === 0 && t.type === 'logic' && t.value === '||') {
|
|
|
+ branches.push(current);
|
|
|
+ current = [];
|
|
|
+ } else {
|
|
|
+ current.push(t);
|
|
|
}
|
|
|
-
|
|
|
- throw new Error(`无法识别的操作数: ${token.value}`);
|
|
|
- }
|
|
|
-
|
|
|
- evaluate(): number {
|
|
|
- return this.parseAddSub();
|
|
|
}
|
|
|
+ if (current.length > 0) branches.push(current);
|
|
|
+ return branches;
|
|
|
}
|
|
|
|
|
|
-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}`);
|
|
|
- }
|
|
|
+function branchContains(branch: Item[], vars: Set<string>): boolean {
|
|
|
+ return branch.some((t) => t.type === 'operand' && vars.has(t.value));
|
|
|
}
|
|
|
|
|
|
+// ─── 原子条件提取 ──────────────────────────────────────────
|
|
|
+
|
|
|
/**
|
|
|
- * 比较运算求值器 — 同时记录每个原子条件的求值详情(含可读原因)。
|
|
|
+ * 将 token 列表递归拆解为原子比较条件的 token 组。
|
|
|
+ *
|
|
|
+ * 步骤:
|
|
|
+ * 1. 递归去掉最外层匹配的括号
|
|
|
+ * 2. 在顶层 && / || 处拆分
|
|
|
+ * 3. 对仍含括号的片段递归处理
|
|
|
+ *
|
|
|
+ * 例如 `(coVal>10 && coVal<=50) || co2Val<0.5` →
|
|
|
+ * [[coVal,>,10], [coVal,<=,50], [co2Val,<,0.5]]
|
|
|
*/
|
|
|
-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;
|
|
|
+function extractAtomicSegments(tokens: Item[]): Item[][] {
|
|
|
+ // 递归去掉最外层匹配的括号
|
|
|
+ while (tokens.length >= 2 && tokens[0].type === 'leftParen') {
|
|
|
+ let d = 0;
|
|
|
+ let matched = true;
|
|
|
+ for (let i = 0; i < tokens.length; i++) {
|
|
|
+ if (tokens[i].type === 'leftParen') d++;
|
|
|
+ if (tokens[i].type === 'rightParen') d--;
|
|
|
+ if (d === 0 && i < tokens.length - 1) { matched = false; break; }
|
|
|
}
|
|
|
- return left;
|
|
|
+ if (matched) tokens = tokens.slice(1, -1);
|
|
|
+ else break;
|
|
|
}
|
|
|
|
|
|
- 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 segments: Item[][] = [];
|
|
|
+ let current: Item[] = [];
|
|
|
+ let depth = 0;
|
|
|
|
|
|
- // 读取左侧算术表达式
|
|
|
- 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);
|
|
|
+ for (const t of tokens) {
|
|
|
+ if (t.type === 'leftParen') depth++;
|
|
|
+ else if (t.type === 'rightParen') depth--;
|
|
|
|
|
|
- const condition = `${leftStr} ${this.displayMap[operator] || operator} ${rightStr}`;
|
|
|
-
|
|
|
- if (passes) {
|
|
|
- this.details.push({ condition, passes: true });
|
|
|
+ if (depth === 0 && t.type === 'logic') {
|
|
|
+ if (current.length > 0) { segments.push(current); current = []; }
|
|
|
} else {
|
|
|
- this.details.push({
|
|
|
- condition,
|
|
|
- passes: false,
|
|
|
- reason: `${leftStr}当前为 ${leftVal},不满足${operatorStr}${rightStr}的条件`,
|
|
|
- });
|
|
|
+ current.push(t);
|
|
|
}
|
|
|
-
|
|
|
- return passes;
|
|
|
}
|
|
|
+ if (current.length > 0) segments.push(current);
|
|
|
|
|
|
- getDetails(): EvaluateDetail[] {
|
|
|
- return this.details;
|
|
|
- }
|
|
|
+ // 对仍含括号的片段递归
|
|
|
+ return segments.flatMap((s) =>
|
|
|
+ s.some((t) => t.type === 'leftParen') ? extractAtomicSegments(s) : [s],
|
|
|
+ );
|
|
|
}
|
|
|
|
|
|
+// ─── 对外接口 ───────────────────────────────────────────────
|
|
|
+
|
|
|
/**
|
|
|
- * 根据传入的变量值,判断表达式是否能通过,返回每个条件的详细原因。
|
|
|
- *
|
|
|
- * @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 的条件"
|
|
|
- * // }
|
|
|
+ * @param expr - 表达式,如 "coValOut>=0 && coValOut<5"
|
|
|
+ * @param reasons - 关键字列表,如 ['coValOut', 'o2ValOut']
|
|
|
+ * @param operandSet - 操作数集合
|
|
|
+ * @param displayMap - 变量名/运算符 → 中文标签映射
|
|
|
+ * @returns { passes, details, summary }
|
|
|
*/
|
|
|
export function evaluateExpression(
|
|
|
expr: string,
|
|
|
- variableValues: Record<string, number>,
|
|
|
+ reasons: string[],
|
|
|
operandSet: Set<string>,
|
|
|
displayMap?: Record<string, string>,
|
|
|
): EvaluateResult {
|
|
|
@@ -288,27 +113,65 @@ export function evaluateExpression(
|
|
|
return { passes: false, details: [], summary: '表达式为空或无法解析' };
|
|
|
}
|
|
|
|
|
|
- const substituted = substituteValues(tokens, variableValues, displayMap);
|
|
|
+ const relevantVars = new Set(reasons);
|
|
|
|
|
|
- try {
|
|
|
- const extractor = new ComparisonExtractor(substituted, displayMap || {});
|
|
|
- const passes = extractor.parseOr();
|
|
|
- const details = extractor.getDetails();
|
|
|
+ // 按顶层 || 拆分,只保留涉及传入关键字的 OR 分支
|
|
|
+ const branches = splitOrBranches(tokens).filter((b) => branchContains(b, relevantVars));
|
|
|
|
|
|
- const passedCount = details.filter((d) => d.passes).length;
|
|
|
- const failedDetails = details.filter((d) => !d.passes);
|
|
|
+ if (branches.length === 0) {
|
|
|
+ return {
|
|
|
+ passes: false,
|
|
|
+ details: [],
|
|
|
+ summary: `表达式中未找到涉及 ${reasons.join('、')} 的条件`,
|
|
|
+ };
|
|
|
+ }
|
|
|
|
|
|
- let summary: string;
|
|
|
- if (passes) {
|
|
|
- summary = `全部 ${details.length} 个条件满足`;
|
|
|
- } else {
|
|
|
- const reasons = failedDetails.map((d) => d.reason || d.condition).join(';');
|
|
|
- summary = `条件不满足(${passedCount}/${details.length}): ${reasons}`;
|
|
|
- }
|
|
|
+ // 逐分支拆解为原子条件,只保留涉及 reasons 的
|
|
|
+ const allDetails: EvaluateDetail[] = branches.flatMap((b) => {
|
|
|
+ const atoms = extractAtomicSegments(b);
|
|
|
+ return atoms
|
|
|
+ .filter((atom) => atom.some((t) => t.type === 'operand' && relevantVars.has(t.value)))
|
|
|
+ .map((atom) => ({
|
|
|
+ condition: renderTokens(atom, displayMap || {}),
|
|
|
+ passes: true,
|
|
|
+ }));
|
|
|
+ });
|
|
|
|
|
|
- return { passes, details, summary };
|
|
|
- } catch (e: unknown) {
|
|
|
- const message = e instanceof Error ? e.message : String(e);
|
|
|
- return { passes: false, details: [], summary: `表达式求值出错: ${message}` };
|
|
|
+ if (allDetails.length === 0) {
|
|
|
+ return {
|
|
|
+ passes: false,
|
|
|
+ details: [],
|
|
|
+ summary: `表达式中未找到涉及 ${reasons.join('、')} 的条件`,
|
|
|
+ };
|
|
|
}
|
|
|
+
|
|
|
+ const summary = allDetails.map((d) => d.condition).join(',');
|
|
|
+ return { passes: true, details: allDetails, summary };
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * evaluateExpression 的简化版,直接返回匹配条件的文本,用 ; 连接。
|
|
|
+ *
|
|
|
+ * @param expr - 表达式
|
|
|
+ * @param reasons - 关键字列表
|
|
|
+ * @param operandSet - 操作数集合
|
|
|
+ * @param displayMap - 中文标签映射
|
|
|
+ * @returns 匹配条件的可读文本,如 "闭外氧气 < 20;闭外一氧化碳 ≥ 0 且 闭外一氧化碳 < 5"
|
|
|
+ */
|
|
|
+export function evaluateExpressionLite(
|
|
|
+ expr: string,
|
|
|
+ reasons: string[],
|
|
|
+ operandSet: Set<string>,
|
|
|
+ displayMap?: Record<string, string>,
|
|
|
+): string[] {
|
|
|
+ const tokens = parseExpression(expr, operandSet);
|
|
|
+ if (tokens.length === 0) return [];
|
|
|
+
|
|
|
+ const relevantVars = new Set(reasons);
|
|
|
+ const branches = splitOrBranches(tokens).filter((b) => branchContains(b, relevantVars));
|
|
|
+
|
|
|
+ return branches
|
|
|
+ .flatMap((b) => extractAtomicSegments(b))
|
|
|
+ .filter((atom) => atom.some((t) => t.type === 'operand' && relevantVars.has(t.value)))
|
|
|
+ .map((atom) => renderTokens(atom, displayMap || {}));
|
|
|
}
|