using System.Data;
using System.Globalization;
using System.Text.RegularExpressions;
namespace Admin.NET.Plugin.AiDOP.Infrastructure.FormulaExpr;
///
/// 结构化公式运行时求值器 v1。
/// 当前用于打通“可执行计算”的工程落点:调用方负责提供 @指标 与 $事实 的数值上下文。
///
public static class FormulaRuntimeEvaluator
{
private static readonly Regex MetricRefRe = new(@"@([A-Za-z][A-Za-z0-9_]*)", RegexOptions.Compiled);
private static readonly Regex FactRefRe = new(@"\$([A-Za-z][A-Za-z0-9_]*)", RegexOptions.Compiled);
public sealed class RuntimeResult
{
public decimal? Value { get; init; }
public List Errors { get; } = new();
public bool Success => Errors.Count == 0 && Value.HasValue;
}
public static RuntimeResult TryEvaluate(
string? formulaExpr,
IReadOnlyDictionary metricValues,
IReadOnlyDictionary factValues)
{
var parsed = FormulaParser.Parse(formulaExpr);
var result = new RuntimeResult();
if (parsed.IsEmpty)
{
result.Errors.Add("公式为空");
return result;
}
foreach (var error in parsed.Errors) result.Errors.Add(error);
if (result.Errors.Count > 0) return result;
var expr = formulaExpr!.Trim().Replace("%", "/100", StringComparison.Ordinal);
expr = MetricRefRe.Replace(expr, m =>
{
var code = m.Groups[1].Value;
if (!metricValues.TryGetValue(code, out var value) || value == null)
{
result.Errors.Add($"缺少指标值 @{code}");
return "0";
}
return value.Value.ToString(CultureInfo.InvariantCulture);
});
expr = FactRefRe.Replace(expr, m =>
{
var code = m.Groups[1].Value;
if (!factValues.TryGetValue(code, out var value) || value == null)
{
result.Errors.Add($"缺少事实值 ${code}");
return "0";
}
return value.Value.ToString(CultureInfo.InvariantCulture);
});
if (result.Errors.Count > 0) return result;
try
{
var computed = new DataTable().Compute(expr, "");
return new RuntimeResult { Value = Convert.ToDecimal(computed, CultureInfo.InvariantCulture) };
}
catch (Exception ex)
{
result.Errors.Add("公式计算失败:" + ex.Message);
return result;
}
}
}