using System.Text.RegularExpressions; namespace Admin.NET.Plugin.AiDOP.Infrastructure.FormulaExpr; /// /// 第三批公式结构化 — 最小 DSL v1 解析器。 /// /// 语法: /// FormulaExpr ::= Token (Operator Token)* /// Token ::= MetricRef | FactRef | Number | '(' FormulaExpr ')' /// MetricRef ::= '@' MetricCode /// FactRef ::= '$' FactCode /// Operator ::= '+' | '-' | '*' | '/' /// Number ::= 数字(支持小数、百分号) /// /// 仅做语法 / 引用抽取,不做执行求值。 /// public static class FormulaParser { // MetricCode 形如 S1_L2_003 / S4_L1_001,允许字母数字下划线 private static readonly Regex MetricRefRe = new(@"@([A-Za-z][A-Za-z0-9_]*)", RegexOptions.Compiled); // FactCode 形如 F_S1_001 / F_COMMON_001 private static readonly Regex FactRefRe = new(@"\$([A-Za-z][A-Za-z0-9_]*)", RegexOptions.Compiled); public sealed class ParseResult { public List MetricRefs { get; } = new(); public List FactRefs { get; } = new(); public List Errors { get; } = new(); public bool IsEmpty { get; set; } } public static ParseResult Parse(string? expr) { var result = new ParseResult(); if (string.IsNullOrWhiteSpace(expr)) { result.IsEmpty = true; return result; } var text = expr.Trim(); // 1) 抽取 @ / $ 引用 foreach (Match m in MetricRefRe.Matches(text)) if (!result.MetricRefs.Contains(m.Groups[1].Value)) result.MetricRefs.Add(m.Groups[1].Value); foreach (Match m in FactRefRe.Matches(text)) if (!result.FactRefs.Contains(m.Groups[1].Value)) result.FactRefs.Add(m.Groups[1].Value); // 2) 括号配对 var stack = 0; foreach (var ch in text) { if (ch == '(') stack++; else if (ch == ')') { stack--; if (stack < 0) { result.Errors.Add("括号不匹配:右括号多余"); break; } } } if (stack > 0) result.Errors.Add("括号不匹配:左括号多余"); // 3) 语法白名单:去掉 @xxx / $xxx 及合法符号后,不应剩下其它字符 var residue = MetricRefRe.Replace(text, ""); residue = FactRefRe.Replace(residue, ""); // 允许:数字、小数点、百分号、算符 + - * / ( ) 、空白、中文"天"(兼容 (D2/D1)*30天) // 纯 ASCII 校验:保留 + 剔除后若出现其它字符标记为警告(不作为硬错) var allowedRe = new Regex(@"^[\s\d\.\+\-\*/\(\)%]*$", RegexOptions.Compiled); if (!allowedRe.IsMatch(residue)) { // 找出不合法字符样本(最多 20 字) var bad = new string(residue.Where(c => !char.IsWhiteSpace(c) && !char.IsDigit(c) && "+-*/().%".IndexOf(c) < 0).Distinct().Take(20).ToArray()); if (bad.Length > 0) result.Errors.Add($"公式包含未识别字符:'{bad}'。仅支持 @指标 $事实 数字及 + - * / ( ) % 算符"); } // 4) 至少含一个 token if (result.MetricRefs.Count == 0 && result.FactRefs.Count == 0 && !Regex.IsMatch(text, @"\d")) { result.Errors.Add("公式至少应包含一个引用(@指标 / $事实)或数字"); } return result; } }