如何在C#中解析Excel公式

前言

在日常工作中,我们经常需要在Excel中使用公式对表中数据进行计算(求和、求差和求均值等)和分析,从而实现对数据的分类,通常情况下,当数据量较少或场景变化单一的情况下,使用公式可以满足用户的要求,但当数据量较大或者场景变化复杂的情况下,使用公式也无法满足用户的需求的情况。这个时候就可以用编码的方式来解决,以下面的背景需求为例,小编将为大家介绍如何使用葡萄城公司基于 .NET 和 .NET Core 平台的服务端高性能表格组件组件GrapeCity Documents for Excel (以下简称GcExcel)解析Excel中的现有公式并根据需求对其进行修改。

背景需求

下图是一张销售数据表,左侧显示原始销售数据,包括销售代表的姓名、地区、产品和销售数量,右侧显示了从原始数据中提取的特定的销售代表对应的销售分析结果,以及每个产品区域组合的月度销售目标进度。目标进度的标准如下:

  • 低于 2500:低于目标
  • 超过 3000:达到目标
  • 超过 5000:高于目标

一般情况下,我们使用Excel中的 IF、ISNUMBER 和 FILTER 函数就可以实现将左侧的销售原始数据转化为右侧的销售分析结果,如下所示:

=IF(ISNUMBER(FILTER(A2:D19,A2:A19="Fritz")),IFS(FILTER(A2:D19,A2:A19="Fritz")>5000,"Above Target",FILTER(A2:D19,A2:A19="Fritz")>3000,"On Target",FILTER(A2:D19,A2:A19="Fritz")<2500,"Below Target"),FILTER(A2:D19,A2:A19="Fritz"))

但是这样的话就会出现一个问题,对于不同的人名,小编需要将上面公式中销售代表的姓名进行替换,也就是需要不断地手动改变姓名执行操作,这一举动不仅枯燥,而且很容易出错。因此这个时候就可以使用GcExcel通过解析公式并使用解析的语法树轻松替换销售代表姓名,可以简化此任务。

使用 C# 解析和修改 Excel 公式

首先,创建一个新的 C#(.NET Core) 项目,并使用NuGet 包管理器安装 GcExcel 包,然后按照前面的步骤操作。

1、使用示例数据初始化工作簿

实例化 Workbook 类的实例并从 Excel 文件导入示例数据,如下所示。

C# 复制代码
//Create a new workbook
var workbook = new GrapeCity.Documents.Excel.Workbook();           
//Load sample data from excel file
workbook.Open("SampleData.xlsx");
//Enable dynamic array formula
workbook.AllowDynamicArray = true;

2、提取公式

在工作簿加载示例数据和预期公式后,我们从工作表中提取所需的公式,以便使用 Formula 属性进行解析和修改。

GcExcel API 提供的公式解析器希望传递的公式不带"="(等于)运算符,以便成功进行公式解析。因此,请注意如何在不使用"="运算符的情况下提取公式。

C# 复制代码
//Fetch worksheet
var worksheet = workbook.Worksheets[0];
//Fetch the original formula which needs to be parsed.
var originalFormula = worksheet.Range["H3"].Formula.Substring(1);

3、解析公式

调用 FormulaSynatxTree 类的 Parse 方法来解析公式并生成语法树,帮助您理解公式包含的所有不同类型的值、运算符和函数。

公式语法树的每个标记都由 GcExcel API 中的其他类表示,例如函数的 FunctionNode、运算符的 OperatorNode 等。

下面的代码解析了上一步中提取的销售分析公式。然后,它将生成的 FormulaSyntaxTree 中的值附加到工作簿,该工作簿随后保存为 Excel 文件,以帮助您了解公式的语法树。

C# 复制代码
//Method to parse a formula and print the syntax tree
public static void ParseAndPrint(IWorksheet worksheet, string formula)
{
   // Get syntax tree
   var syntaxTree = FormulaSyntaxTree.Parse(formula);

   // Flatten nodes
   var displayItems = new List<(string TypeName, int IndentLevel, string Content)>();

   void flatten(SyntaxNode node, int level)
   {
      displayItems.Add((node.GetType().Name, level, node.ToString()));
      foreach (var child in node.Children)
      {
         flatten(child, level + 1);
      }
   }

   flatten(syntaxTree.Root, 0);

   // Output          
   worksheet.ShowRowOutline = false;
   worksheet.OutlineColumn.ColumnIndex = 1;

   // Header
   worksheet.Range["A1"].Value = "Formula";
   worksheet.Range["A3"].Value = "Syntax node";
   worksheet.Range["B3"].Value = "Part";

   // Values
   worksheet.Range["B1"].Value = "'=" + formula;
   for (var i = 0; i < displayItems.Count; i++)
   {
      var item = displayItems[i];
      var text = "'" + item.TypeName;

      worksheet.Range[i + 4, 0].Value = text;
      worksheet.Range[i + 4, 0].IndentLevel = item.IndentLevel;
      worksheet.Range[i + 4, 1].Value = "'" + item.Content;
   }

   //Apply styling
   worksheet.Range["A1:B3"].Interior.Color = System.Drawing.Color.FromArgb(68, 114, 196);
   worksheet.Range["A1:B3"].Font.Color = System.Drawing.Color.White;
   worksheet.Range["A1:B3"].Borders.Color = System.Drawing.Color.FromArgb(91, 155, 213);
   worksheet.Range["A1:B3"].Borders.LineStyle = BorderLineStyle.Thin;
   worksheet.Range["A1,A3,B3"].Font.Size = 14;
   worksheet.Range["A1,A3,B3"].Font.Bold = true;
   worksheet.Range["A:C"].EntireColumn.AutoFit();           
}

下图是生成的 FormulaSyntaxTree 的效果图图。请注意,这只是完整语法树的一部分:

4、修改公式

从上一步生成的语法树中,您可以看到销售代表姓名以 TextNode 形式表示,并且在公式中多次出现。我们可以通过简单的查找和替换操作来替换所有这些出现的情况,如下面的代码所示:

  1. 了替换公式中的销售代表姓名,我们从他们的姓名列表开始。我们使用 UNIQUE 函数从原始数据中过滤掉唯一名称列表。然后使用这个 UNIQUE 函数的结果来解析和修改所有销售代表的销售分析公式。
  2. 我们使用 TextNode 类修改销售代表姓名。下面的代码初始化 TextNode 类的实例,并将要在公式中搜索的销售代表姓名作为参数传递。该实例可以称为查找节点。
  3. 接下来,我们初始化 TextNode 类的另一个实例,并将公式中要替换的销售代表姓名作为参数传递。该实例可以称为替换节点。
  4. 下面的代码中定义了一个递归函数 replaceNode,用于遍历语法树的所有子节点,并将每个出现的 Find 节点替换为 Replace 节点。每个销售代表都会重复此操作。
  5. 修改公式后,新公式将分配给工作表中的单元格以生成预期的销售报告。

下面的代码包含一些格式化代码来格式化销售报告内容。

C# 复制代码
//Method to parse and modify the formula
public static void ModifyFormula(IWorksheet worksheet, string originalFormula)
{
    //Apply UNIQUE formula to get unique sales representatives list
    worksheet.Range["F1"].Value = "Unique Rep";
    worksheet.Range["F2"].Formula = "=UNIQUE(A2:A19)";
    var uniqueRep = worksheet.Range["F2#"];
    // Apply Styling
    worksheet.Range["F:F"].EntireColumn.AutoFit();
    worksheet.Range["F1"].Interior.Color = System.Drawing.Color.FromArgb(68, 114, 196);
    worksheet.Range["F1"].Font.Color = System.Drawing.Color.White;
    worksheet.Range["F2#"].Borders.Color = System.Drawing.Color.FromArgb(91, 155, 213);
    worksheet.Range["F2#"].Borders.LineStyle = BorderLineStyle.Thin;

    //Get syntax tree
    var syntaxTree = FormulaSyntaxTree.Parse(originalFormula);

    //Find
    var findText = new TextNode("Fritz");

    //Replacement
    var replaceText = new TextNode("");

    //Loop through names list to modify the formula for each sales representative
    for (int r = 0, resultRow = 3; r < uniqueRep.Cells.Count; r++, resultRow = resultRow + 4)
    {
       //Get name to be replaced in the formula
       var cval = uniqueRep.Cells[r].Value.ToString();

       if (findText.Value != cval)
       {
          //Assign name to be replaced to Replace TextNode
          replaceText.Value = cval;

          //Invoke the recursive method to perform find and replace operation
          replaceNode(syntaxTree.Root, findText, replaceText);

          //Assign the modified formula to a cell in the worksheet
          var resultRange = "H" + resultRow.ToString();
          worksheet.Range[resultRange].Formula = "=" + syntaxTree.ToString();
          worksheet.Range[resultRange + "#"].Borders.Color = System.Drawing.Color.FromArgb(91, 155, 213);
          worksheet.Range[resultRange + "#"].Borders.LineStyle = BorderLineStyle.Thin;

          //Update the value of Find node to perform find and replace operation for next sales representative name
          findText = replaceText;
       }
    }

    //Find and replace
    void replaceNode(SyntaxNode lookIn, SyntaxNode find, SyntaxNode replacement)
    {
       var children = lookIn.Children;

       for (var i = 0; i < children.Count; i++)
       {
          var child = children[i];
          if (child.Equals(find))
          {
             children[i] = replacement;
          }
          else
          {
             replaceNode(child, find, replacement);
          }
       }
    }
 }

这是修改后的公式之一:

=IF(ISNUMBER(FILTER(A2:D19,A2:A19="Xi")),IFS(FILTER(A2:D19,A2:A19="Xi")>5000,"Above Target",FILTER(A2:D19,A2:A19="Xi")>3000,"On Target",FILTER(A2:D19,A2:A19="Xi")<2500,"Below Target"),FILTER(A2:D19,A2:A19="Xi"))

5、保存 Excel 文件

将所有修改的公式添加到工作表后,将调用 Workbook 类的 Save 方法来保存 Excel 文件,如下面的代码所示:

C# 复制代码
//Save modified Excel file
workbook.Save("ModifiedFormula.xlsx", SaveFileFormat.Xlsx);

打开保存的 Excel 文件可以看到下图:

总结

以上就是使用C#实现解析Excel的全过程,如果您想了解更多信息,欢迎点击这里查看更多资料。

扩展链接:

轻松构建低代码工作流程:简化繁琐任务的利器

优化预算管理流程:Web端实现预算编制的利器

如何在.NET电子表格应用程序中创建流程图