MathExpression 框架
MathExpression 是一个 Swift 库,它提供了一个 API 来解析和计算由 String
实例给出的算术数学表达式。它与 iOS,tvOS 和 macOS 兼容。
此外,初始化器还接受一个额外的可选参数,称为 transformation
,它是一个接受一个 String
实例并返回一个 Double
的闭包。这在创建自定义表达式时提供了极大的灵活性,因为我们可以传递非数字的字符串,并使用转换来评估。
系统要求
版本 | 系统要求 |
---|---|
1.3.0 或更高 | Xcode 13.0+ Swift 5.0+ iOS 12.0+,tvOS 12.0+,macOS 10.14+ |
1.1.0 - 1.2.0 | Xcode 10.0+ Swift 4.2+ iOS 10.0+,tvOS 10.0+,macOS 10.10+ |
1.0.0 和 1.0.1 | Xcode 10.0+ Swift 4.2+ iOS 10.0+ |
从版本 1.3.0 开始,我们将 IDE、Swift 和平台的要求提高到上述要求。请注意,虽然指定了 Xcode 13.0 作为最低要求,但没有任何具体到该版本 Xcode 的要求。因此,该软件包应该与无法使用 Xcode 13 的项目兼容。
安装
您可以通过手动或通过依赖管理器安装框架。
CocoaPods 使用方法
要使用 Cocoapods,首先确保已安装它,并按照其官网 cocoapods.org 上的说明将其更新到最新版本。然后,您应该完成以下步骤:
- 将 MathExpression 添加到您的
Podfile
文件
pod 'MathExpression', '~>1.3.0'
- 在命令行中执行以下命令以更新您的 pod 源并安装新 pod:
$ pod install --repo-update
Carthage 使用方法
要使用 Carthage,首先确保已安装它,并按照其在 GitHub 仓库 上的说明将其更新到最新版本。
- 将 MathExpression 添加到您的
Cartfile
文件
github "peredaniel/MathExpression" ~> 1.3.0
- 运行 Carthage 以安装新框架
$ carthage update
使用 Swift 包管理器
要使用 Swift 包管理器安装,请在您的 Package.swift
文件的 dependencies
部分添加以下内容
.package(url: "https://github.com/peredaniel/MathExpression.git", .upToNextMinor(from: "1.3.0")),
手动安装
我们建议使用依赖管理器来安装依赖项,但如果您不能使用任何依赖管理器,您可以按照以下步骤手动安装框架
- 克隆或下载此仓库。
- 将位于
MathExpression
文件夹中的Source
文件夹拖到您的项目中。
详细说明
安装过程中仍然出现问题?请参阅我们的 分步安装指南!
开始使用
通过上述任何一种方式安装框架后,您可以在Swift文件的“头文件”中添加以下行来导入模块
import MathExpression
然后,您可以在任何位置使用以下代码初始化一个MathExpression
实例(例如,使用表达式(3 + 4) * 9
)
let expression = try MathExpression("(3 + 4) * 9")
然后,您可以使用以下代码获取表达式的计算值
let value = expression.evaluate() // 63.0
内置运算符
框架中已经内置了几个运算符,因此当创建一个新的MathExpression
实例时,它们被认为是保留字符。已实现的运算符列表如下
- 加法,保留字符
+
。 - 减法,保留字符
-
。 - 乘法,保留字符
*
。 - 除法,保留字符
/
。 - 负数,保留字符
_
(仅在解析过程中内部使用)。
我们将这些称为运算符,并区分加法运算符(加法和减法)和乘法运算符(乘法和除法)。
此外,左括号(
和右括号)
也是保留字符。
验证过程
您可能已经注意到,MathExpression
初始化器可能会抛出错误。的确,初始化器会通过给定的String实例运行验证过程以确保其可计算性,如果验证失败则抛出错误。
如果满足以下任何一个条件,则表达式被认为是无效的
- 空字符串:字符串为空,也就是说,您尝试初始化
MathExpression("")
。 - 括号位置错误:括号位置错误,即存在右括号在左括号之前,或者乘法运算符后跟一个左括号,或者任何运算符后跟一个右括号。例如,"
(a + b +)
","(/ x * y)
"或") x + (y * 3
"是非有效表达式。 - 括号数量不匹配:左括号和右括号的数量多于它们的对应数量。例如,"
(a + b))
" - 连续运算符:存在一些组合运算符是没有意义的。更具体地说,如果表达式包含以下任意一种组合,则将认为无效:
,
*/
,/ *
,/*
,+/
,+-
,-*
,-/
。其他任何组合都是有效的:+-
,+-
,-+-
和--
分别转换为+
,-
,-
和+
,而其余的*+
,/+
,*-
,--
则根据解析顺序将被解析为第二个运算符是后面跟随乘法运算符的数字(或求值后的结果)的一部分。 - 以非加法运算符开头:如果表达式以任意非加法运算符开头,将会抛出错误。也就是说,以
*
或/
开头。 - 以运算符结束:如果表达式的最后一个字符是运算符,将会抛出错误。
作为一个一般性的建议,表达式对人类来说应该是合理的。
运算符优先级
MathExpression解析器将按照以下严格顺序执行运算。
- 尝试用当前的表述初始化一个
Double
实例并判断是否为数字,如果是,则返回其数值。 - 判断表述是否以加法运算符(求和或减法)开头。
- 如果可能的话,评估括号内的每一个表达式,从内到外,从左到右。
- 评估乘法运算符(首先乘法,然后除法)。
- 评估加法运算符(首先加法,然后减法)。
- 如果没有满足以上的任何一项,则将剩余的字符串应用转换,并返回得到的
Double
值。
转换
转换为输入的String实例提供了极大的灵活性来计算数学表达式。本质上,一个 转换
仅仅是一个形式,即
(String) -> Double
这意味着任何接受一个String实例并返回一个Double值的函数。这个转换作为一个可选参数传递给MathExpression
初始化器,默认值为nil
。如果没有提供转换或显式传递nil
,则MathExpression
将使用如下代码初始化自己:
{ Double($0) ?? 0.0 }
以下是一些需要注意的要点
- 转换返回的是非可选的
Double
实例:你 必须 提供一种非失败的代码或默认值。这是为了防止解析器在尝试解析不包含实际数学运算符的String实例时无限循环。 - 变换是最后执行的操作,并且仅当字符串实例本身不是数值时才执行。这意味着,根据您的变换,可能会有限制。特别是,如果您的变换是一个数学函数,由于操作的顺序,可能得到错误的结果。例如,下面是关于指数函数的章节。
- 变换仍然是块,因此请注意防止保留周期。
- 虽然在理论上您可以传递一个执行异步代码的变换(例如,从远程数据库中获取模型并执行计算以获取数字),但我们从未以此为目标开发框架,因此我们无法确保表达式将被正确评估。
示例:指数函数
让我们考虑以下代码块
let transformation: (String) -> Double = { string in
let splitString = string.split(separator: "^").map { String($0) }
guard splitString.count == 2,
let base = try? MathExpression(splitString.first ?? ""),
let exponent = try? MathExpression(splitString.last ?? "") else { return 0.0 }
return pow(base.evaluate(), exponent.evaluate())
}
此变换本质上是将^
符号定义为指数运算符。如果您查看文件ExponentialTransformationTests.swift
,您会注意到变换仅在以下条件之一满足时才返回正确的结果
- 基数和指数都是非负数。
- 基数是负数且指数是非负的奇数。
特别是,偶指数不会去掉负号,因为算法在指数之前解析和评估减法运算符,而在纯数学中指数必须优先,因为它是一个乘法运算符。
算法性能
验证和评估算法都遵循分而治之的方法,并采用递归实现。也就是说,这两种算法将给定的字符串实例拆分成更小的实例,直到得到单个数值或非数学表达式(在后一种情况中,变换返回相应的值)。此外,验证算法遍历整个字符串实例一次,以确保不存在无效的连续运算符。
因此,如果包含数学表达式的字符串实例长度为n
,这两种算法的复杂度如下
- 验证:
O(1) + O(n) + O(n * log(n)) = O(n * log(n))
- 评估:
O(n * log(n))
请注意,此分析不包括可能作为参数传递的变换。复杂的变换可能会增加评估算法的复杂度。
项目中包含每个单元测试的performance
版本(除了ExponentialTransformationTests
,其目的是不同的),可通过运行方案PerformanceTests
独立运行。此外,存在一个名为StressPerformanceTests
的类,用于捕获验证和评估算法中的任何性能边缘情况。到目前为止,该类包括以下压力测试
- 由500对连接括号组成的数学表达式,中心包含单一的值。验证和评估的平均时间小于0.4秒。
- 涉及501个表达式的拼接,每个表达式类型为
(a + b * c)
,其中c
是另一个类型的表达式(a + b * c)
,依此类推。取3个表达式,最终结果会是(a + b * (c + d * (e + f * (g + h))))
。验证和评估的平均时间大约是1.1秒。 - 由预先选择的
Formulae
枚举子集中的500个随机表达式组合而成的随机生成的数学表达式。验证和评估的平均时间大约是5秒。
示例应用
此存储库包含一个示例应用,其中包含一些使用此框架的场景。
计算器
此框架的第一个(也是最明显的)用例是计算器应用,类似于iOS或macOS的非科学计算器界面。输入仅限于框架中已内置的操作(即加法、减法、乘法和除法),并提供添加括号和更改输入符号的选项。另外,每次按下新操作或=
按钮时,计算器都会在当前输入上方显示完整的操作。
评估器
此框架的第二个明显用例是评估由String
给出的表达式。根据Evaluator
,我们可以这样做。通过两个UITextField
提供输入。第一个是评估的表达式,而第二个允许选择在评估过程中使用的transformation
。使用UIPickerView
从以下集合中选择transformation
- 默认:尝试将
String
转换为Double
,如果不可能,则返回0.0
。 - 计数:返回
String
实例的count
属性值。 - 阶乘:评估类似
n!
的表达式,作为n
的阶乘(即:n * (n - 1) * (n - 2) * ... * 2 * 1
,其中n
必须是一个非负 整数。如果n < 1
,则n!
返回1
。如果n
不是一个整数,则n!
返回0.0
。在使用此转换时要谨慎:阶乘函数以接近指数的速度增长,如果用于大数则很容易溢出。
未来版本可能会添加更多转换。
贡献
本框架的开发是为了满足其他项目的需求,在该项目中我们需要评估一个涉及非数值标识符的数学表达式,这些标识符从 CoreData 取出一个模型。因此,一旦满足了需求(添加适当的测试和文档),我们就停止了开发。如果您有任何改进的建议,我们将非常乐意听到/看到它们。只需新建一个 issue 进一步讨论,或者创建一个 pull request 与我们分享您的想法。
代码风格指南和格式化工具
除 However,我们遵循 Ray Wenderlich Swift 风格指南,除了 间距 部分:我们使用 4 个空格 而不是 2 个空格缩进。
为了遵循上述代码风格指南,我们使用 SwiftFormat。规则集已检查到本存储库的 .swiftformat
文件中。在推送任何代码之前,请按照上述存储库中的 How do I install it? 指示安装 SwiftFormat
并在项目的根目录中执行以下命令
swiftformat . --config .swiftformat --swiftversion 5.0 --exclude Package.swift
这将重新格式化项目文件夹内所有的 *.swift
文件以遵循指南,除了 Package.swift
清单文件。
路线图
尽管当前实现已经提供了我们所需要的功能,但我们心中还有几个想法,当我们有时间时愿意去实现。特别是:
- 为运算符(公开、不可修改)和转换(可修改)添加一个
priority
属性,以便它们能够按我们真正需要的顺序执行。 - 添加一些额外的数学运算符,例如
^
(代表指数),以及可能的一些函数,如三角函数。
致谢
类似框架
如果这个框架不能满足您的需求,您没有时间为其做出贡献,GitHub上目前维护着几个杰出的替代品。在开发这个框架之前,我们尝试了以下这些