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