| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889 |
- using System.Text.RegularExpressions;
- namespace Admin.NET.Plugin.AiDOP.Infrastructure.FormulaExpr;
- /// <summary>
- /// 第三批公式结构化 — 最小 DSL v1 解析器。
- ///
- /// 语法:
- /// FormulaExpr ::= Token (Operator Token)*
- /// Token ::= MetricRef | FactRef | Number | '(' FormulaExpr ')'
- /// MetricRef ::= '@' MetricCode
- /// FactRef ::= '$' FactCode
- /// Operator ::= '+' | '-' | '*' | '/'
- /// Number ::= 数字(支持小数、百分号)
- ///
- /// 仅做语法 / 引用抽取,不做执行求值。
- /// </summary>
- 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<string> MetricRefs { get; } = new();
- public List<string> FactRefs { get; } = new();
- public List<string> 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;
- }
- }
|