65.9K
CodeProject 正在变化。 阅读更多。
Home

Math Parser .NET

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (56投票s)

2011 年 10 月 26 日

CPOL

9分钟阅读

viewsIcon

171976

downloadIcon

6914

一个非常易于使用的.NET数学解析器库。

引言

前段时间,我在 CodeProject 上写了一篇文章,名为 TokenIcer。TokenIcer 是一个程序,可以根据在程序中创建的正则表达式规则,自动在 C# 或 VB.NET 中创建词法分析器。

自从我写了那篇文章以来,我收到了一些非常好的反馈,我想更进一步。我决定使用 TokenIcer 来创建一个数学方程式解析器。这个数学解析器在此文章中呈现,名为 Math Parser .NET。它是一个 .NET 类库项目,可以与您自己的程序一起使用。

背景

已经存在好几个数学解析器。我的数学解析器的目标是使其尽可能简单易用。我最大的抱怨之一是,当一个好的库因为其自身的特性列表而掩盖了其简单性时,它就会“变坏”。我认为我的数学解析器在保持基本功能和用法简单的同时,很好地完成了它的工作。

Using the Code

在深入介绍如何使用该库之前,我想首先介绍一下该库的内部工作原理。该库本身首先由一个词法分析器组成。我使用 TokenIcer 自动创建了词法分析器。词法分析器将采用这样的输入字符串

1 + 2 * 3 + (9 / 3) 

并将其转换为枚举的标记,如下所示

{Integer}{WhiteSpace}{Add}{WhiteSpace}{Integer}{WhiteSpace}{Multiply}
  {WhiteSpace}{Integer}{WhiteSpace}{Add}{WhiteSpace}{LParen}
  {Integer}{WhiteSpace}{Divide}{WhiteSpace}{Integer}{RParen}

一旦没有更多的标记需要处理,词法分析器将返回一个空标记。

接下来的任务是创建一个循环,遍历每个标记,直到收到空标记。解析器接收到的输入将采用所谓的“中缀表示法”。中缀表示法意味着数字由数学运算符分隔。就像上面的例子一样,1 和 2 由一个加号分隔。2 和 3 由一个乘号分隔。此外,数学运算符具有特定的优先级顺序。在上面的例子中,9 / 3 应该首先计算,因为它在括号中。2 * 3 应该其次计算,因为乘法和除法的优先级高于加法。为了更容易地解析数学表达式,我将方程式从中缀表示法转换为逆波兰表示法。在逆波兰表示法(或 RPN)中,数学符号位于数字**之后**。因此,在转换之后,上面的示例输入将看起来类似于这样

1 2 3 * 9 3 / + +

一旦方程式采用逆波兰表示法,只需从左到右读取并进行实际的数学运算即可。

要在您自己的程序中使用 MathNetLib 库,您所要做的就是添加对 MathNetLib DLL 文件的引用。之后,您需要实例化一个 MathNetLib.Parser 类的新实例。有三种主要的方程求解方法。第一种叫做 SimplifyInt()SimplifyInt() 将接受一个数学方程,求解它,并返回一个整数答案。此方法有两个重载。这是第一个也是最简单的示例

static void Main()
{
    MathParserNet.Parser parser = new MathParserNet.Parser();
    int retval;

    try {
        retval = parser.SimplifyInt("3.2 + 7.6");
    } catch (Exception ex) {
        throw ex;
    }
}

此示例将返回一个 11 的值。关于 SimplifyInt() 需要记住的是,它将始终返回一个整数答案,并且默认情况下,它将四舍五入该答案。 SimplifyInt() 有一个重载方法签名,它还允许您指定如何处理答案的小数部分(如果有)。这是一个示例

static void Main()
{
    MathParserNet.Parser parser = new MathParserNet.Parser();
    int retval;

    try {
        retval = parser.SimplifyInt("3.2 + 7.6", 
                 MathParserNet.Parser.RoundingMethods.Truncate);
    } catch (Exception ex) {
        throw ex;
    }
}

RoundingMethods 是一个枚举,具有以下值和描述

  • Round -- 这是默认值。将小数四舍五入到最接近的整数。如果小数点后的数字是 5 或更高,它将向上舍入整数。如果小数点后的数字是 4 或更低,它将向下舍入整数。
  • RoundDown -- 无论小数点后的数字是多少,这都将始终向下舍入整数。
  • RoundUp -- 无论小数点后的数字是多少,这都将始终向上舍入整数。
  • Truncate -- 这将完全不进行任何舍入,而只是“截断”小数点和其后的任何数字。

第二个主要求解方程的方法叫做 SimplifyDouble()。这个方法,像 SimplifyInt() 一样,将求解方程,但将始终返回一个 double 类型的答案。这是一个该方法实际应用的例子

static void Main()
{
    MathParserNet.Parser parser = new MathParserNet.Parser();
    double retval;

    try {
        retval = parser.SimplifyDouble("3.2 + 7.6");
    } catch (Exception ex) {
        throw ex;
    }
}

这将返回 10.8 的答案。

求解数学方程的第三个也是最后一个版本简单地称为 Simplify()Simplify() 将求解一个方程,然后自动确定它是一个整数还是一个浮点数答案。 Simplify() 返回一个 MathParserNet.SimplificationReturnValue 对象。您可以检查此对象的 ReturnType 属性以确定答案是浮点数还是整数。这是一个使用示例

static void Main()
{
    MathParserNet.Parser parser = new MathParserNet.Parser();
    MathParserNet.SimplificationReturnValue retval;

    try {
        retval = parser.Simplify("3.2 + 7.2");
    } catch (Exception ex)
        throw ex;
    }
    if (retval.ReturnType == MathParserNet.SimplificationReturnValue.ReturnTypes.Float)
    {
        Console.WriteLine("The answer is a Floating point number!");
        Console.WriteLine(retval.DoubleValue);
    }
    if (retval.ReturnType == MathParserNet.SimplificationReturnValue.ReturnTypes.Integer)
    {
        Console.WriteLine("The answer is an Integer!");
        Console.WriteLine(retval.IntValue);
    }

无论何时使用任何简化函数,都应将其封装在 try/catch 块中。MathParserNet 实现了五个异常。以下是每个异常的描述

  • MismatchedParenthesisException -- 如果您尝试解决的方程式中开括号多于闭括号,反之亦然,则会抛出此异常。
  • NoSuchFunctionException -- 如果您尝试使用尚未定义的函数,则会抛出此异常。函数将在下面描述。
  • NoSuchVariableException -- 如果您尝试使用尚未定义的变量,则会抛出此异常。变量将在下面描述。
  • VariableAlreadyDefinedException -- 如果您尝试定义一个已在前面定义的变量,则会抛出此异常。变量将在下面描述。
  • CouldNotParseExpressionException -- 如果在尝试解析表达式时出现任何其他问题,则会抛出此异常。将空方程式传递给任何简化函数都会导致此异常。

解析器可以解析以下内容

  • () -- 括号
  • + -- 加号 (3 + 2)
  • - -- 减号 (3 - 2)
  • * -- 乘号 (3 * 2)
  • / -- 除号 (3 / 2)
  • % -- 模数符号 (3 % 2)(除两个数,但返回余数)
  • ^ -- 指数符号 (3 ^ 2)(3 的平方)
  • ABS -- 函数返回一个数的绝对值 (ABS(-3))
  • SIN -- 返回一个数的正弦 (SIN(3.14))
  • COS -- 返回一个数的余弦 (COS(3.14))
  • TAN -- 返回一个数的正切 (TAN(3.14))
  • LOG -- 返回一个数的以 10 为底的对数
  • LOGN -- 返回一个数的自然对数
  • func<name> -- 调用用户定义函数(参见下面的“函数”)

变量

MathParserNet 支持使用变量。您可以使用 AddVariable() 方法在 MathParserNet 中定义变量。这是一个向解析器添加三个变量的示例

static void Main()
{
    MathParserNet.Parser parser = new MathParserNet.Parser();

    try {
        parser.AddVariable("PI", 3.14159265);
        parser.AddVariable("Three", 3);
        parser.AddVariable("HalfPI", "PI / 2");

        parser.SimplifyDouble("HalfPI * Three + 7");
    } catch (Exception ex) {
        throw ex;
    }
}

变量有点像文字处理器中的剪切和粘贴。当解析器看到一个变量时,它会查找该值并将变量名替换为实际值。所以 HalfPI 实际上变成了 "3.14159265 / 2",传递给 SimplifyDouble() 方法的方程实际上变成了 "3.14159265 / 3 + 7"。

已定义的变量也可以通过调用 RemoveVariable() 方法删除。您只需将变量名作为参数传递给 RemoveVariable(),变量就会被删除。请记住,任何使用已删除变量定义的变量都会变得无效。例如,在上面的代码中,如果我删除 PI 变量,那么 HalfPI 变量也将变得无效。此外,您还可以通过调用 RemoveAllVariables() 方法删除**所有**已创建的变量。

另一件需要记住的事情是变量名**是**区分大小写的。PI 与 Pi、pI 或 pi 不同。

函数

除了变量,MathParserNet 还支持函数。您可以定义一个函数来做任何您想做的事情。这是一个示例

static void Main()
{
    MathParserNet.Parser parser = new MathParserNet.Parser();

    try {
        parser.AddFunction("GetSlope", 
          new MathParserNet.FunctionArgumentList {"x1", "x2", 
          "y1", "y2"}, "(y1-y2)/(x1-x2)");

        parser.SimplifyDouble("3 * funcGetSlope(3.2 * (9/3), 4.5, 6, 9.2)");
    } catch (Exception ex) {
        throw ex;
    }
}

此示例创建了一个名为 GetSlope 的函数。该函数接受四个参数 (x1,x2,y1,y2)。函数必须至少接受一个参数,并且可以定义为接受任意数量的参数。但请记住,函数参数名**不能**与已定义的变量名相同。要调用函数,您必须在函数名前加上“func”(当然不带引号)。需要记住的另一件事是,与变量一样,函数也区分大小写(“func”前缀也区分大小写)。

函数,就像变量一样,也可以通过调用 RemoveFunction() 方法来取消定义。与变量一样,您只需将函数名作为参数传递给 RemoveFunction()。此外,您还可以通过调用 RemoveAllFunctions() 方法删除**所有**已创建的函数。

委托函数

也许 MathParserNet 最酷,可以说也是最方便的功能是能够在任何 .NET 语言中创建自己的自定义函数,并将其用作数学解析器中的自定义函数。这是一个示例,展示了我们的 GetSlope 函数作为委托函数

static void Main()
{
    MathParserNet.Parser parser = new MathParserNet.Parser();
    int myDouble;
    double area;
    double slope;

    try {
        parser.RegisterCustomFunction("GetSlope", GetSlope);
        parser.RegisterCustomFunction("Doubler", Doubler);
        parser.RegisterCustomFunction("AreaCircle", AreaCircle);

        area = parser.SimplifyInt("AreaCircle(3.7)");
        myDouble = parser.SimplifyDouble("Doubler(7)");
        slope = parser.SimplifyDouble("GetSlope(-3.2, 12, 8, 13)");
    } catch (Exception ex) {
        throw ex;
    }
}

static int Doubler(int num)
{
    return num * 2;
}

static double AreaCircle(double radius)
{
    return Math.PI * (radius * radius);
}

static double GetSlope(double x1,double x2,double y1, double y2)
{
    return (y1-y2)/(x1-x2);
}

委托函数最多可以有 4 个参数。返回类型**必须**与参数相同,并且参数必须都是相同类型。参数可以是 intdoubleobject。整数和双精度类型不言自明。如果声明 object 类型的参数,则对象将是 MathParserNet.SimplificationReturnValue 类型。这样您的函数就可以同时接受整数和双精度参数。此外,您的返回类型必须是 object,但该对象可以是整数或双精度。数学解析器将处理其他一切!

与变量和函数一样,委托函数可以通过使用 UnregisterCustomFunction() 方法来删除。您只需将函数名作为参数传递。此外,您还可以通过调用 UnregisterAllCustomFunctions() 方法删除**所有**已定义的委托函数。

杂项方法

还有一个 Reset() 方法。通过调用此方法,解析器将删除**所有**变量、**所有**函数和**所有**已创建的委托函数。这就像从一个全新的、刚刚实例化的 Parser 类开始。

最后一个要提及的小功能是 ToFraction() 函数。每当函数返回 SimplificationReturnValue 时,该对象都带有一个名为 ToFraction() 的方法,它会将返回的值转换为分数。例如,如果您的 SimplificationReturnValue 对象的 DoubleValue 为 0.25,那么调用 ToFraction() 将返回 "1/4"。我将把这个留给您去尝试。

附加功能

我包含了一个单元测试项目,用于测试数学解析库。此外,还有一个演示 Windows Forms 项目,展示了 MathParserNET 的许多(如果不是全部)功能。如果任何人有任何问题或反馈,我总是很乐意听取!

历史

  • 2011年10月30日 -- 发布版本 1.1。此版本在出现错误时抛出异常而不是返回值。此外,此版本引入了委托方法。并且,已创建 SimplifyIntSimplifyDouble 方法。此外,我为数学库创建了一个真正的单元测试项目和一个演示应用程序。感谢所有反馈,请大家继续提供!
  • 2011年10月26日 -- 初始版本发布。
© . All rights reserved.