将数学方程转换为 C#






4.93/5 (164投票s)
一个神奇的工具,
引言
很久以前,我曾在一个产品上工作,其中一部分工作涉及将数学方程转化为代码。那时,我不是被分配这个角色的人,所以我的猜测是代码是通过简单地从 Word 中提取方程并手动将其翻译成 C# 来编写的。这听起来不错,但它让我想到了:有没有办法自动化这个过程,从而消除这个不可否认的枯燥任务中的人为错误?嗯,事实证明这是可能的,而这正是本文要讨论的内容。
方程,嗯?
我猜,除了任何特殊的数学软件包(如 Matlab)之外,我们大多数开发人员都会以 Word 格式获取数学需求。例如,你可能会得到像这样的简单内容
这个方程很容易编程。看,我来做:y = a*x*x + b*x + c;
。然而,有时你会得到非常棘手的方程,就像下面这样
上面的内容来自维基百科。总之,你应该明白了:上面的方程有点太难编程了。我的意思是,我确信如果你有无限的预算或廉价劳动力,你可以做到,但我保证你会出错,因为每次都弄对(如果你有一百个)是困难的。
所以,我的想法是:嘿,应该有一种方法可以将方程数据以某种方式结构化,然后你就可以为 C# 重构它。这就是 MathML 进入的场景。
MathML
好吧,你可能在想这个MathML怪物是什么。基本上,它是一种类似 XML 的数学标记语言。如果所有浏览器都支持它,你就会看到上面的方程使用浏览器的字符而不是位图进行渲染。但无论如何,有一个工具支持它:Word。具体来说是 Microsoft Word 2007。有一个鲜为人知的技巧可以让 Word 将方程转换为 MathML。你基本上必须找到方程选项...
并选择 MathML 选项
好吧,现在将我们的第一个方程复制到剪贴板将得到类似以下内容
![]() |
<mml:math>
<mml:mi>y</mml:mi>
<mml:mo>=</mml:mo>
<mml:mi>a</mml:mi>
<mml:msup>
<mml:mrow>
<mml:mi>x</mml:mi>
</mml:mrow>
<mml:mrow>
<mml:mn>2</mml:mn>
</mml:mrow>
</mml:msup>
<mml:mo>+</mml:mo>
<mml:mi mathvariant="italic">bx</mml:mi>
<mml:mo>+</mml:mo>
<mml:mi>c</mml:mi>
</mml:math>
|
通过查看原始方程,你大概可以猜出这一切的含义。嘿,我们只是提取了方程的结构!这很酷,除了一个问题:将其转换为 C#!(否则,它就没有意义了。)
语法树
保持数据原样是没用的。有很多额外的信息(比如 'bx' 附近的斜体说明),也有信息缺失(比如 'b' 和 'x' 之间应该存在的乘号)。所以,我们对这个问题的看法是将其 XML 结构转换为更面向对象的、类似 XML 的结构。事实上,这正是程序所做的——它将 XML 元素转换为相应的 C# 类。在大多数情况下,XML 和 C# 是一对一对应的,因此一个<mi/>
元素变成一个Mi
类。所以太好了,我们不费吹灰之力就将 XML 变成了语法树。现在,这棵树并不完美,但它确实存在。让我们来讨论一下我们必须克服的一些棘手问题。
单/多字母变量
“sin”是表示s乘以i乘以n,还是一个名为“sin”的变量,还是Math.Sin
函数?当我查看我拥有的方程时,有些使用了多个字母,有些是单个字母。如何处理这些并没有“放之四海而皆准”的解决方案。基本上,我将其设置为一个选项。
乘号(×)
如果你写ab,它可能表示a乘以b。如果是这样,你需要找到所有省略乘号的位置。有趣的是,不同的数学编辑软件包(我用 MathML 和 Word 测试过)在乘号方面使用了不同的 Unicode 符号。结果是,找出乘号丢失的位置非常困难。
希腊字母转罗马字母
有些人反对在 C# 代码中使用希腊常数。嘿,我用 UTF-8 编码,所以我可以包含任何内容,包括日文字符和其他有趣的 Unicode 符号。这会搞乱 IntelliSense,因为你的键盘可能没有希腊键——除非你住在希腊。此外,这是快速破坏可维护性的一种方式。因此,我必须添加的一个功能是将希腊字母转换为罗马字母描述,这样 Δ 就会变成 Delta,依此类推。实际上,Delta 是一个特例,因为我们非常习惯于将其附加到变量上(例如,写 ΔV)。因此,我为 Δ 添加了一个特殊规则,使其在所有其他变量都是单字母的情况下也能保持附加。
正确处理 e、π 和 exp
基本上,字母 pi(π)可以只是一个变量,也可以表示Math.PI
。字母 e 也是如此——它可以是Math.E
,在大多数情况下也是如此。另一个更棘手的替换是 exp 到Math.Exp
。所有这三者的支持都必须添加。
幂函数内联
大多数人都知道x*x
比Math.Pow(x, 2.0)
快,尤其是在处理整数时。内联 X 的幂及以上是程序中的一个选项。我见过一些文章(找不到链接),其中有人声称如果你避免使用Math.Pow
方式,就会丢失精度。我不太确定。
运算归约
我被告知,有些表达式的输出在其构成运算方面效率不高。例如,a*x*x+b*x+c
不如x*(a*x+b)+c
高效,因为它有更多的乘法。因此,我解决方案的未来目标之一是尝试优化这些场景。不过,这会降低它们的可读性!
在从 XML 转换为 C# 的过程中还有许多其他问题,但主要思想保持不变:在每种可能的 MathML 元素上正确实现 Visitor 模式,删除不必要的信息并提供缺失的信息。让我们看一些例子。
示例
好吧,我打赌你迫不及待想看到一个实际的例子。让我们从之前的那个开始
这是我们得到的输出
y=a*x*x+b*x+c;
我省略了程序也创建的变量的初始化步骤。
让我们看看更复杂的方程。这是它,以防你忘记了
敢猜猜我们工具的输出是什么吗?
p = rho*R*T + (B_0*R*T-A_0-((C_0) / (T*T))+((E_0) / (Math.Pow(T, 4))))*rho*rho +
(b*R*T-a-((d) / (T)))*Math.Pow(rho, 3) +
alpha*(a+((d) / (t)))*Math.Pow(rho, 6) +
((c*Math.Pow(rho, 3)) / (T*T))*(1+gamma*rho*rho)*Math.Exp(-gamma*rho*rho);
我最初的输出使用了希腊字母(提醒:C# 支持它们)。然而,由于编码的原因,我让我的工具将它们更改为罗马字母版本,从而展示了另一个功能。
好的,让我们再做一个例子来确保——这次带一个平方根。这是方程
我关闭了此方程的幂函数内联——我们不希望根号表达式被评估两次。这是输出
a = 0.42748 * ((Math.Pow((R*T_c), 2)) / (P_c)) *
Math.Pow((1 + m * (1 - Math.Sqrt(T_r))), 2);
这很棒,不是吗?如果你有一天拿到一份包含数百页公式的文件,那么你可以通过快速地将它们编码来让你的客户惊喜。
结论
我希望你喜欢这个工具。也许你甚至会觉得它很有用。我最近使用 F# 从头开始重新设计了该工具,你可以在这里找到最新版本。