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;
}
}