FormulaRuntimeEvaluator.cs 2.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172
  1. using System.Data;
  2. using System.Globalization;
  3. using System.Text.RegularExpressions;
  4. namespace Admin.NET.Plugin.AiDOP.Infrastructure.FormulaExpr;
  5. /// <summary>
  6. /// 结构化公式运行时求值器 v1。
  7. /// 当前用于打通“可执行计算”的工程落点:调用方负责提供 @指标 与 $事实 的数值上下文。
  8. /// </summary>
  9. public static class FormulaRuntimeEvaluator
  10. {
  11. private static readonly Regex MetricRefRe = new(@"@([A-Za-z][A-Za-z0-9_]*)", RegexOptions.Compiled);
  12. private static readonly Regex FactRefRe = new(@"\$([A-Za-z][A-Za-z0-9_]*)", RegexOptions.Compiled);
  13. public sealed class RuntimeResult
  14. {
  15. public decimal? Value { get; init; }
  16. public List<string> Errors { get; } = new();
  17. public bool Success => Errors.Count == 0 && Value.HasValue;
  18. }
  19. public static RuntimeResult TryEvaluate(
  20. string? formulaExpr,
  21. IReadOnlyDictionary<string, decimal?> metricValues,
  22. IReadOnlyDictionary<string, decimal?> factValues)
  23. {
  24. var parsed = FormulaParser.Parse(formulaExpr);
  25. var result = new RuntimeResult();
  26. if (parsed.IsEmpty)
  27. {
  28. result.Errors.Add("公式为空");
  29. return result;
  30. }
  31. foreach (var error in parsed.Errors) result.Errors.Add(error);
  32. if (result.Errors.Count > 0) return result;
  33. var expr = formulaExpr!.Trim().Replace("%", "/100", StringComparison.Ordinal);
  34. expr = MetricRefRe.Replace(expr, m =>
  35. {
  36. var code = m.Groups[1].Value;
  37. if (!metricValues.TryGetValue(code, out var value) || value == null)
  38. {
  39. result.Errors.Add($"缺少指标值 @{code}");
  40. return "0";
  41. }
  42. return value.Value.ToString(CultureInfo.InvariantCulture);
  43. });
  44. expr = FactRefRe.Replace(expr, m =>
  45. {
  46. var code = m.Groups[1].Value;
  47. if (!factValues.TryGetValue(code, out var value) || value == null)
  48. {
  49. result.Errors.Add($"缺少事实值 ${code}");
  50. return "0";
  51. }
  52. return value.Value.ToString(CultureInfo.InvariantCulture);
  53. });
  54. if (result.Errors.Count > 0) return result;
  55. try
  56. {
  57. var computed = new DataTable().Compute(expr, "");
  58. return new RuntimeResult { Value = Convert.ToDecimal(computed, CultureInfo.InvariantCulture) };
  59. }
  60. catch (Exception ex)
  61. {
  62. result.Errors.Add("公式计算失败:" + ex.Message);
  63. return result;
  64. }
  65. }
  66. }