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

另一个数学解析器 (YAMP)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.98/5 (54投票s)

2012年9月19日

CPOL

21分钟阅读

viewsIcon

137919

downloadIcon

2645

使用反射构建一个像 Matlab 一样快速进行数值计算的数学解析器。

YAMP Logo

引言

在某些项目中,我们可能需要一个合适的数学解析器来执行简单的(小计算器)或更复杂的数学运算。CodeProject 提供了各种各样的数学解析器,从 C/C++ 移植的到纯 C# 编写的都有。本文试图利用 C# 和 .NET Framework 的特性,创建一个纯 C# 的强大解析器。该解析器本身也完全能够在其他平台上运行,因为它是在 Linux 上编写的。

该解析器本身是开源的。YAMP(这是该解析器的名称)的完整源代码可在 Github 上获取。NuGet 包也即将推出。

个人背景

几年前,我曾使用 C# 编译器和一些继承来编写一个数学解析器。方法是提供一个基类,它提供所有可能的方法(例如 sin()exp() 等),这些方法可以通过继承过程使用。然后构建一个表示有效 C# 代码的字符串,其中包含自定义表达式,然后必须对其进行编译。当然,预解析器是必要的,以提供一些数据类型。另一方面,操作符重载和其他 C# 特性也可以使用。

这种解决方案的问题是 C# 编译器对于单个代码文档来说有点重。而且,生成的程序集必须首先加载。因此,在预解析过程之后,我们必须处理 C# 编译器和程序集。这意味着反射也是必要的。因此,任何表达式的最小时间都是 O(102) 毫秒。这太长了,无法提供流畅的输入和输出处理。

CodeProject 上有几个项目旨在提供数学解析器。所有这些项目都比我以前/旧的解决方案更快——它们也更健壮。然而,它们不如我的项目完整——因此,我现在尝试以一种新方式在这个非常老的项目中取得成功。这次我从头开始构建了一个解析器,它使用(并滥用)反射,从而提供了一个易于扩展的代码库,其中包含从运算符到表达式,再到常量和函数的一切。甚至赋值和索引运算符也是可能的。

YAMP 的功能

在我们深入了解 YAMP 之前,我们不妨看看 YAMP 为我们提供了什么。首先简单了解一下一些功能

  • 赋值运算符 (=, +=, -=, ...)
  • 使用 i 作为虚数常数的复数算术。
  • 矩阵代数,包括乘积、除法。
  • 三角函数 (sin, cos, sinh, cosh) 及其反函数 (arcsin, arccos, arcosh, arsinh)。
  • 更复杂的函数,如伽马函数等。
  • 阶乘、转置和左除运算符。
  • 使用索引获取和设置字段。
  • 复数根和幂。

YAMP 的核心元素是 ScalarValue 类,它是抽象 Value 类的实现。该类提供了使用复杂类型进行数值计算所需的一切。还包含了带复数参数的基本三角函数的实现。

MatrixValue 类基本上是 ScalarValue 实例的(二维)列表。我们可能会认为这个列表应该能够是 n 维的,但是,这会带来一些复杂性,例如需要指定一种在 n 维中设置值的方式。目前我们将避免这个问题。

现在我们对 YAMP 的主要功能有了简要的了解,让我们看看 YAMP 可以解析的一些表达式

-2+3+5/7-3*2

这是一个简单的表达式,但它已经包含了不简单的表达式。为了成功解析这个表达式,我们需要注意运算符级别(乘法运算符 * 的级别高于减法运算符 -,除法运算符 / 也是如此)。我们还将负号用作一元运算符,即这里没有前导零。

2^2^2^2

这通常被错误解析。向 MATLAB 询问这个表达式,我们得到 256。这似乎有点低和错误。第一次向 YAMP 询问时,结果也是如此——直到我们包含了一条规则,即使是同级别的运算符也将单独处理——从右到左。这条规则在任何表达式中都不会产生影响——除了幂运算符。现在我们得到了和 Google 显示的相同结果——65536。

x=2,3,4;5,6,7

我们可以使用括号,但在这种情况下我们不需要它们。这是一个简单的矩阵赋值给符号 x。我们自己分配的符号是区分大小写的,而内置函数和常量则不区分大小写。因此,我们可以分配自己的 pi,并通过使用不同的大小写符号(例如 Pi)来访问原始常量。

e^pi

同样,一些常量是内置的,例如 phiepi。因此,这个表达式被成功解析。将 YAMP 作为外部库使用,可以轻松添加我们自己的常量。

2^x-5

这看起来很奇怪,但它是有意义的。首先,我们有一个标量 (2) 乘以一个矩阵(我们之前赋值的那个)。这是未定义的,因此 YAMP 将对矩阵中的每个值执行乘幂运算。结果矩阵将具有与指数相同的维度——它将是一个 2 乘以 3 的矩阵。然后这个矩阵与右侧的 5(一个标量)执行减法。这再次是未定义的,因此 YAMP 再次对每个单元格执行运算。最后我们得到一个 2 乘以 3 的矩阵。

y=x[1,:]

我们使用范围运算符 : 来指定所有可用的索引。行索引设置为 1(我们像 MATLAB 和 Fortran 一样,先使用行再使用列——现在是基于 1 的)——第一行。因此,结果将是一个维度为 1 乘以 3 的矩阵(一个转置的三维向量)。条目将是 2、3、4。

x[:]=8:13

在这里,我们将矩阵的每个值重新分配给一个新值——在向量 8, 9, 10, 11, 12, 13 中给出。如果我们只使用一个索引,它将是一个组合索引。因此,一个 2 乘以 3 的矩阵可能将对 2,3 作为最后一个条目(并将对 1,1 作为第一个条目),但也可能将索引 6 作为最后一个条目(并将索引 1 作为第一个条目)。

xt=x'

伴随运算符要么共轭标量(在实轴上的投影,即改变虚数值的符号),要么转置矩阵并共轭其条目。因此,xt 现在存储一个 3 乘 2 的矩阵。如果我只想转置(不共轭),我可以使用 .' 运算符。

z=sin(pi * (a = eye(4)))

在这里,我们将 eye(4) 的值赋给符号 a。然后该值乘以 pi,再由 sin() 函数求值。结果保存在符号 z 中。eye() 函数生成一个 n 维单位矩阵。如果我们将三角函数应用于矩阵,我们将得到一个矩阵(与幂函数和其他函数一样),其中每个值都是作为特定函数参数的先前值的结果。

whatever[10,10]=8+3i

这里发生了两件事。首先,我们创建一个新符号(名为 whatever),然后我们将第 10 行第 10 列的单元格赋值为 8+3i。因此,我们甚至可以在不存在的符号上使用索引。我们还可以通过分配更高的索引来扩展矩阵。这只能通过设置值来实现——我们无法获取更高的索引。这里将抛出异常。

whatever[2:2:end,1:5]

在这里,我们将获取所有偶数行(2:2:end 表示:从索引 2 开始,以步长 2 到达末尾(一个短宏))及其前半列的信息。显示的矩阵将是一个 5 乘 5 的矩阵,行号为 2,4,6,8,10,列号为 1,2,3,4,5。

YAMP 非常重视矩阵。矩阵乘法是根据适当的维度进行的。让我们看一个示例控制台应用程序

Output in the sample console application

索引运算符允许我们从矩阵中选择(多个)列和/或行。下面的输出显示了内部 3x3 矩阵(从 5x5 矩阵中)的选择。

Output in the sample console application

至此,我们对 YAMP 的简短介绍结束了。当然,YAMP 也能正确执行诸如 i^i(2+3i)^(7-i)17! 等查询。

YAMP 的工作原理

YAMP 包含几个重要的类

  • 解析树
  • 抽象表达式
  • 运算符
  • IFunction

在库外部访问 YAMP 最重要的数据类型是 Parser 类。我们稍后将介绍这个概念。

现在我们想简要看一下整个库的类图。首先是原则上的类图

Short class diagram of YAMP

Parser 生成一个 ParseTree,该 ParseTree 对单例类 Tokens 拥有完全访问权限。该单例用于查找运算符、函数、表达式和常量,以及解析符号。

YAMP 严重依赖反射。第一次使用 YAMP 可能会有点慢(与后续使用相比)。因此,在进行任何测量或用户输入之前,应调用 Parser 的静态 Load() 方法。这可以保证给定表达式的最短执行时间。

任何表达式都将始终由多个子表达式和运算符组成。运算符可以是二元的(如 +、- 等)或一元的(如 !、[]、' 等)。二元运算符需要两个操作数(左和右),而一元运算符只需要一个。表达式可能是以下之一

  • 一个括号表达式,其中再次包含一个完整的 ParseTree
  • 一个数字——它可以是正数、负数、实数或复数。
  • 一个绝对值,有时类似于一个括号表达式,在求值后调用 abs() 函数。
  • 一个符号值——要么待解析(可能是常量或以前存储的东西),要么待设置。
  • 函数表达式可以看作是一个直接附加了括号表达式(表达式之间没有运算符)的符号值。

这导致了以下类图

Expression class diagram

接下来我们来看看运算符的类图

Operator class diagram

这里我们看到 AssignmentOperator 也不过是另一个 BinaryOperator。为了简化和代码重用,另一个抽象类 AssignmentPrefixOperator 被添加为任何组合赋值运算符(如 += 或 -=)和原始赋值运算符之间的附加层。总的来说,添加了以下赋值运算符:+=、-=、*=、/=、\=、^=。

左除 (/) 和右除 (\) 的区别在于,左除意味着 A = B / C = B-1 * C,而右除意味着 A = B \ C = B * C-1。两者之间的区别对于矩阵可能至关重要,因为它关系到我们是从左乘还是从右乘。两个操作数始终是 Value 类型。该类型是一个抽象类,构成了以下派生数据类型的基础

  • 用于数字的 ScalarValue(可以是虚数)
  • 用于矩阵的 MatrixValue(只能是向量),由 ScalarValue 条目组成
  • 用于字符串的 StringValue
  • 用于参数列表的 ArgumentsValue

函数也是 YAMP 中非常重要的一部分。函数的类图如下所示

Function class diagram

这里我们创建了一个标准类型 StandardFunction。该类型必须按照要求实现接口 IFunction。唯一将被调用的是 Perform() 方法。StandardFunction 已经包含了一个只适用于 MatrixValueScalarValue 等数字类型的框架。如果 Perform() 方法不改变,给定矩阵的每个数字都将被更改为正在调用的函数的结果。

这里另一个有用的类型是 ArgumentFunction 类型。这个类型可以用来创建一个完全可操作的重载机器。一旦我们从这个类派生,我们只需要创建一个或多个名为 Function() 且返回类型为 Value 的函数。反射将找出每个函数需要多少参数,并根据给定参数的数量调用正确的函数(或者向用户返回错误)。让我们考虑 RandFunction

class RandFunction : ArgumentFunction
{
	static readonly Random ran = new Random();
	
	public Value Function()
	{
		return new ScalarValue(ran.NextDouble());
	}
	
	public Value Function(ScalarValue dim)
	{
		var k = (int)dim.Value;
		
		if(k <= 1)
			return new ScalarValue(ran.NextDouble());
		
		var m = new MatrixValue(k, k);
		
		for(var i = 1; i <= k; i++)
			for(var j = 1; j <= k; j++)
				m[j, i] = new ScalarValue(ran.NextDouble());
		
		return m;
	}
	
	public Value Function(ScalarValue rows, ScalarValue cols)
	{
		var k = (int)rows.Value;
		var l = (int)cols.Value;
		var m = new MatrixValue(k, l);
		
		for(var i = 1; i <= l; i++)
			for(var j = 1; j <= k; j++)
				m[j, i] = new ScalarValue(ran.NextDouble());
		
		return m;
	}
}

使用此代码,YAMP 将返回零个、一个或两个参数的结果。如果用户提供了两个以上的参数,YAMP 将抛出异常,并显示消息“未找到与用户提供的参数数量匹配的重载”。

实现新的标准函数,即那些只有一个参数的函数,也很容易。这里展示了 ceil() 函数的实现

class CeilFunction : StandardFunction
{
    protected override ScalarValue GetValue(ScalarValue value)
    {
        var re = Math.Ceiling(value.Value);
        var im = Math.Ceiling(value.ImaginaryValue);
        return new ScalarValue(re, im);
    }	
}

BinaryOperator 类的实现非常直接。由于任何二元运算符都需要两个表达式,如果给定表达式的数量不等于二,则 Evaluate() 方法会抛出异常。引入了两个新方法:一个可以被重写名为 Handle(),另一个必须被实现名为 Perform()。前者将被第一个的标准实现使用。

abstract class BinaryOperator : Operator
{
	public BinaryOperator (string op, int level) : base(op, level)
	{
	}
	
	public abstract Value Perform(Value left, Value right);

    public virtual Value Handle(Expression left, Expression right, Hashtable symbols)
    {
        var l = left.Interpret(symbols);
        var r = right.Interpret(symbols);
        return Perform(l, r);
    }
	
	public override Value Evaluate (Expression[] expressions, Hashtable symbols)
	{
		if(expressions.Length != 2)
			throw new ArgumentsException(Op, expressions.Length);
		
		return Handle(expressions[0], expressions[1], symbols);
	}
}

现在我们来讨论 YAMP 是如何实际生成可用对象的。大部分魔法都在 Tokens 类的 RegisterTokens() 方法中完成。

void RegisterTokens()
{
	var assembly = Assembly.GetExecutingAssembly();
	var types = assembly.GetTypes();
	var ir = typeof(IRegisterToken).Name;
	var fu = typeof(IFunction).Name;
	
	foreach(var type in types)
	{
        if (type.IsAbstract)
            continue;

		if(type.GetInterface(ir) != null)
			(type.GetConstructor(Type.EmptyTypes).Invoke(null) as IRegisterToken).RegisterToken();
		
		if(type.GetInterface(fu) != null)
			AddFunction(type.Name.Replace("Function", string.Empty), (type.GetConstructor(Type.EmptyTypes).Invoke(null) as IFunction).Perform, false);
	}
}

代码遍历执行程序集 (YAMP) 中所有可用的类型。如果类型是抽象的,我们就会避开它。否则,我们查看该类型是否实现了 IRegisterToken 接口。如果是这种情况,我们将调用构造函数,将其视为接口,并调用 RegisterToken() 方法。该方法通常看起来像下面这样(针对运算符)

public void RegisterToken ()
{
	Tokens.Instance.AddOperator(_op, this);
}

现在我们遇到了一种情况,即我们的 ParseTree 正在搜索一个运算符。在这种情况下,将调用 Tokens 类的 FindOperator() 方法。

public Operator FindOperator(string input)
{
	var maxop = string.Empty;
	var notfound = true;

	foreach(var op in operators.Keys)
	{
		if(op.Length <= maxop.Length)
			continue;

		notfound = false;

		for(var i = 0; i < op.Length; i++)
			if(notfound = (input[i] != op[i]))
				break;

		if(notfound == false)
			maxop = op;
	}

	if(maxop.Length == 0)
		throw new ParseException(input);

	return operators[maxop].Create();
}

此方法将找到当前输入的最大运算符。如果未找到运算符(最大长度的运算符的长度仍为零),则会抛出异常。否则,找到的运算符将调用 Create() 方法。此方法只是返回运算符的另一个实例。

一个简单的反射魔法示例

我们已经讨论了通过从 StandardFunctionArgumentFunction 派生来添加自定义函数的能力。如果我们要完全从头开始,我们也可以通过只实现 IFunction 接口来编写自己的函数类。现在让我们看一下 ArgumentFunction 的实现。在这里我们将看到反射是一个非常重要的部分。首先让我们看一下这个类的代码

abstract class ArgumentFunction : StandardFunction
{
	protected Value[] arguments;
	IDictionary<int, MethodInfo> functions;
	
	public ArgumentFunction ()
	{
		functions = new Dictionary<int, MethodInfo>();
		var methods = this.GetType().GetMethods();
		
		foreach(var method in methods)
		{
			if(method.Name.Equals("Function"))
			{
				var args = method.GetParameters().Length;
				functions.Add(args, method);
			}
		}
	}
	
	public override Value Perform (Value argument)
	{
		if(argument is ArgumentsValue)
			arguments = (argument as ArgumentsValue).Values;
		else
			arguments = new Value[] { argument };
		
		return Execute();
	}
	
	Value Execute()
	{
        if(functions.ContainsKey(arguments.Length))
		{
			var method = functions[arguments.Length];

            try
            {
                return method.Invoke(this, arguments) as Value;
            }
            catch (Exception ex)
            {
                throw ex.InnerException;
            }
        }
		
		throw new ArgumentsException(name, arguments.Length);
	}
}

关键部分发生在构造函数中。在这里,(特定的实现)被检查。所有名为 Function() 的方法都将以参数数量作为键添加到字典中。字典中的信息然后在 Execute() 方法中使用。在这里,将调用具有指定参数数量的方法。如果参数数量与任何方法不匹配,则将抛出异常。

为了调用 Execute() 方法,Perform() 方法已被重写。这里,任何参数都被转换为 Value 实例的数组。通常,数组中参数的数量为一,除非传递的参数类型为 ArgumentsValue。在这种情况下,Value 类型已经包含零个到多个 Value 实例。

解析一个(简单?)表达式

在接下来的段落中,我们将探讨 YAMP 如何解析一个简单的表达式。查询形式如下

3 - (2-5 * 3++(2 +- 9)/7)^2.

实际上,我们在表达式中包含了一些空白字符以及一些(显而易见的)错误。首先,“++”应该只表示“+”。其次,操作“+-”应该只替换为“-”。首先,问题分为两个具体的任务

  1. 生成表达式树,即从表达式中分离运算符并注意运算符级别。
  2. 解释表达式树,即使用运算符评估表达式。

解析器最终会得到一个表达式树,如下图所示。

The expression tree after the invocation of the parser

在第二步中,我们调用方法来启动元素的解释。因此,解释器工作并对每个括号调用 Interpret() 方法。每个括号都会查看运算符的数量。如果没有可用的运算符,那么就只有一个表达式。然后返回表达式的值。否则,将调用运算符的 Handle() 方法,并传入 Expression 实例的数组。由于运算符是专门的 BinaryOperator 或专门的 UnaryOperator,因此该方法将始终使用正确的参数数量调用正确的函数。

The value tree after the invocation of the interpreter

在这个例子中,我们的表达式树由五层组成。尽管解释从最高层(1)开始,但它需要最低层(5)的信息才能执行。一旦解释了 5,就可以解释第 4 层,然后是第 3 层,依此类推。给定表达式的最终结果是 -193。

YAMP 与其他数学解析器

我们可以猜测 YAMP 可能比大多数用 C/C++ 编写的解析器要慢。这只是我们必须应对的固有优势(或托管劣势)。因此,针对 C++ 编写的解析器进行基准测试可能不公平。由于封送/互操作也需要一些时间,因此解析小查询并比较结果也可能不公平(这次可能对 C/C++ 解析器不公平)。因此,公平的测试可能只能由纯 C# 解析器组成。查看 CodeProject,我们可以找到以下解析器

我们设置了以下四个测试

  • 解析和解释 100000 个随机生成的查询*
  • 解析和解释 10000 次一个预定义查询(带括号的长查询)
  • 解析和解释 10000 次一个预定义查询(中等大小)
  • 解析和解释 10000 次一个预定义查询(短查询)

* 表达式随机生成器的工作原理是:首先我们生成一些(二元)运算符,然后我们在表达式和二元运算符之间交替,每次随机选择一个。表达式只是一个随机选择的数字(整数),运算符是从运算符列表(+、-、*、/、^)中随机选择的一个。最后我们得到一个完整的表达式,其中有 n 个运算符和 n+1 个数字。

这个基准测试的问题是只有 3 个解析器(YAMP、MathParser.NET 和 LL Mathematical Parser)能够解析所有查询(甚至排除了使用幂运算符的那些)。因此,这个基准测试只能提供这三者之间的速度比较。然而,我们可以使用这个测试来识别 YAMP 确实能够解析许多不同的查询。

如果我们查看每条方程所需的运行时间与运算符数量的关系,我们发现 YAMP 的扩展性实际上相当好。MFP 和 LLMP 在这里表现显著更好,然而,MFP 根本无法解析很多内容,而 LLMP 不支持复数参数,也不支持矩阵。

Time per equation in dependency of the number of operators

最快实现的最大问题是它们给出的限制。它们难以扩展,且灵活性不如 YAMP。虽然 YAMP 包含许多有用的功能(索引、赋值、重载函数等),但其他任何解析器都无法达到这种细节级别。

Total time for each benchmark

基准测试越长(越复杂),YAMP 的表现越好。这意味着 YAMP 在短表达式区域表现稳定(没人会注意到太多高性能),而在长表达式区域表现出色(否则大多数人会注意到延迟和其他烦人的问题)。

YAMPMPLLMPMPTKMP.NETMFP
100000 (R)57163728471611xxx
10000 (L)543145081778505444x
10000 (M)39847951675635122147
10000 (S)131338581136519454

在上表中,我们看到 YAMP 可以解析所有方程,并且表现相当不错。考虑到开发过程的早期阶段和功能集,YAMP 无疑是任何涉及数值数学问题的良好选择。

如何在您的代码中使用 YAMP

如果我们将 YAMP 作为外部库使用,我们需要在需要时进行连接。幸运的是,这并不是一项艰巨的任务。考虑添加新常量的情况。

YAMP.Parser.AddCustomConstant("R", 2.53);

通过这种方式,我们可以覆盖现有常量。如果我们稍后移除自定义常量,任何之前被覆盖的常量将再次可用。通过这种方式,我们还可以添加或移除自定义函数。在这里,我们自己的函数需要具有特殊的签名,提供一个参数(类型为 Value)并返回一个类型为 Value 的实例。以下示例使用 lambda 表达式在一行中生成一个非常简单的函数

YAMP.Parser.AddCustomFunction("G", v => new YAMP.ScalarValue((v as YAMP.ScalarValue).Value * Math.PI) );

实际使用 YAMP 需要什么?我们先看一些例子

try
{
    var parser = YAMP.Parser.Parse("2+(2,2;1,2)");
    var result = parser.Execute();
}
catch (Exception ex) // this is quite important
{
    //ex.Message
}

所以我们所需要做的就是调用 Parser 类(包含在 YAMP 命名空间中)的静态 Parse() 方法。该方法会生成一个新的 Parser 实例,其中包含完整的 ParseTree。如果我们要获取给定表达式的结果,我们只需要调用 Execute() 方法。

关于提供的解决方案,这里需要注意:编译控制台、调试或发布项目可能会由于 MonoDevelop 或 Visual Studio 中的一个错误而导致错误。如果引用项目被排除在构建映射之外(就像 YAMP.Compare 项目在这三个构建项目中一样),那么在第一次构建时会抛出错误。因此,我们必须更改项目映射以再次包含 YAMP.Compare。

(当前)限制

YAMP 存在一些限制。其中一些缺点是暂时的,另一些是永久的。让我们来看看未来的计划

  • 一种包含所有必要运算符、ifforwhileswitch 等的脚本语言。
  • 多行语句
  • 自定义输出格式化器(目前输出格式、内部精度等都是固定的)
  • 设置精度的能力
  • 异步调用(您异步调用解析器/解释器,即在新线程中——然后您必须设置事件处理程序才能获得结果)
  • 更多数值算法
  • 更多逻辑函数(目前只实现了 isint()isprime()
  • 更多清除、加载和保存变量的选项

这只是一份简短的计划功能列表。其中一些更容易实现,一些更难。其中一些在固定的时间窗口内,而另一些则提供了永恒的编码任务。

除了(希望)将被打破的限制之外,一些将继续存在的限制也存在

  • 没有多维矩阵。矩阵最多只能是 2D。
  • 矩阵只能包含复数 (ScalarValue),不能包含其他矩阵、字符串等。
  • 变量是区分大小写的(与不区分大小写的常量和函数一起,这被故意设计为一个特性)。
  • 只有前一个输出保存在 $ 变量中
  • 变量只能包含字母和数字,并且必须以字母开头。
  • 可以用变量覆盖常量 i(复数单位)。

YAMP 区分变量和函数,也就是说,不能用名为 sin 的变量覆盖函数 sin()

从 NuGet 获取 YAMP

YAMP Logo

现在 YAMP 已经达到了 0.9 版本,我终于设法将其作为一个 NuGet 包发布。您可以使用 powershell 中的以下命令下载它

Install-Package YAMP

官方 NuGet 页面可在 https://nuget.net.cn/packages/YAMP 访问。当前版本包含一些错误修复、许多新功能、一个基于属性的完整功能文档系统以及一些新运算符(包括各种点运算符,如 .*, ./, .\, .^, ...)以及新的赋值可能性 (+=, -=, *=, ^=, ...)。

关注点

YAMP 利用反射,使代码扩展尽可能简单。这样,只需添加一个继承自抽象 OperatorBinaryOperatorUnaryOperator 类或任何其他可用运算符的类即可实现添加运算符。

YAMP 不依赖于设定的文化(美国数字风格已明确设置为数字格式)。字符串和其他内容以 UTF-8 编码存储。然而,符号只能以字母 (A-Za-Z) 开头,然后只能包含字母和数字 (A-Za-z0-9)。

该项目目前正在大力开发中,因此更新周期相当快(每周至少一到两个新版本)。这意味着如果您对该项目感兴趣,您应该绝对考虑查看 GitHub 页面(以及可能这篇博文)。

一般来说,YAMP 将需要更多感兴趣的人参与其中(主要是在一些数值函数上)。如果您认为自己可以贡献,请随时这样做!欢迎任何人对这个项目做出贡献。

强烈请求 如果您有任何批评意见,请在评论中简要描述。目前,有些人没有留下评论就投票了 4 分。通常,低于 5 分的投票表示我方存在一些错误行为或失误。如果情况属实,请告诉我原因。否则,当文章没有问题时,您应该考虑为什么投票低于 5 分。

历史

  • v1.0.0 | 首次发布 | 2012 年 9 月 19 日。
  • v1.0.1 | 添加了一个包含基准测试数据的表格 | 2012 年 9 月 19 日
  • v1.1.0 | 添加了一个包含示例过程的部分 | 2012 年 9 月 20 日
  • v1.2.0 | 添加了一个包含限制的部分 | 2012 年 9 月 21 日
  • v1.3.0 | 添加了一个关于 ArgumentFunction 的部分 | 2012 年 9 月 22 日
  • v1.3.1 | 修复了一些拼写错误 | 2012 年 9 月 24 日
  • v1.3.2 | 修复了一些拼写错误 | 2012 年 9 月 25 日
  • v1.4.0 | 包含了 NuGet 包部分 | 2012 年 9 月 30 日
© . All rights reserved.