chriskonnertz / string-calc
StringCalc 是一个用于字符串传递的数学术语(表达式)的 PHP 计算库。
Requires
- php: >=8.0.0
- ext-mbstring: *
Requires (Dev)
- phpunit/phpunit: 9.*
README
StringCalc 是一个零依赖的 PHP 计算库,用于处理以字符串形式传递的数学术语(表达式)。
安装
通过 Composer
composer require chriskonnertz/string-calc
此库需要 PHP 8.0 或更高版本。对于 PHP 5.6 支持,请使用 StringCalc 1。
使用示例
以下是一个计算术语的最小化 PHP 代码示例。它假设存在一个自动加载器。
$stringCalc = new ChrisKonnertz\StringCalc\StringCalc(); $term = '1+2'; $result = $stringCalc->calculate($term); // $result will contain 3
包含一个交互式 PHP 示例脚本。它位于
dev/demo.php
。
动机
想象一下,你正在构建一个允许用户在文本框中输入数学术语的 Web 应用程序。你将如何计算这些术语的结果?PHP 的 eval 函数不是答案。官方文档建议不要使用此函数:“eval() 语言构造非常危险,因为它允许执行任意 PHP 代码。因此,不建议使用。 StringCalc 有自己的解析器实现,因此是一个更好、更安全的替代方案。StringCalc 遵循现代编码原则,可扩展且文档齐全。如果您有任何改进此库的建议,请随时在 问题 部分讨论。
术语
示例,我们需要一个示例!这里有一个:2*(pi-abs(-0.4))
这是一个遵循 StringCalc 理解的语法和语法规则的数学术语。这些术语的语法和语法与您在 PHP 代码中编写的非常相似。更准确地说,存在一个交集的语法和语法规则集。有一些例外,但通常您可以通过假装自己在编写 PHP 代码来为 StringCalc 编写术语。
术语示例
这里有一些不起眼的示例
1+2*3-4
1 + 2 * 3 - 4
pi * 2
PI * 2
abs(1) + min(1,2) * max(1,2,3)
min(1+2, abs(-1))
1 + ((2 - 3) * (5 - 7))
2 * (-3)
以下是显示更奇异语法的示例列表
1 // A term can consist of just a number
(1+((2))) // Usage of obsolete brackets is allowed
00001 // Prefixing a number with obsolete zero digits is possible
.1 // Ommiting a zero digit before a period charcter is okay
要查看所有可用数学符号类型(术语的部分)的列表,请点击此链接: Symbols/Concrete 类
符号列表
运算符
+
-
*
/
函数
abs
aCos
aCosH
aSin
aSinH
aTan
aTanH
aTanTwo
ceil
cos
cosH
degToRad
en
exp
expMOne
floor
fMod
intVal
hypot
log
logOneP
logTen
max
min
pow
radToDeg
round
sin
sinH
sqrt
tan
tanH
常量
e
euler
lnPi
lnTen
lnTwo
logTenE
logTwoE
onePi
pi
piFour
piTwo
sqrtOneTwo
sqrtPi
sqrtThree
sqrtTwo
twoPi
twoSqrtPi
其他
(
)
,
.
StringCalc 类
StringCalc
是 StringCalc 库的 API 前端。本节描述了该类的公共方法。
构造函数
构造函数有一个名为 $container
的可选参数,该参数实现了 Container\ContainerInterface
。这是 StringCalc 使用的服务容器。如果没有传递参数,构造函数将创建一个具有类型 Container\Container
的新容器对象。容器接口确保容器实现了 PSR-11 标准。因此,您可以替换默认容器,使用任何其他实现 PSR-11 标准的容器,但您必须将其包装在一个使它与 Container\Container
类兼容的包装器类中。我们建议避免额外的开销,并扩展 Container\Container
类。
calculate
calculate()
方法是这个类最重要的方法。它期望一个类型为字符串的参数,名为 $term
。它返回一个类型为 float 或 int 的数字。我们强烈建议在调用此方法时使用 try-catch
块,并编写一个 catch
语句来捕获所有类型为 Exceptions\StringCalcException
的异常。
try { $result = $stringCalc->calculate('min()'); } catch (Exceptions\StringCalcException $exception) { ... // Handle exception } catch (\Exception $exception) { ... // Handle exception. }
在示例中,将抛出一个具有 StringCalcException
类型的异常,因为必须至少调用一次 min
方法。当给定术语的语法或语法无效时,通常抛出类型为 StringCalcException
的异常。它们有两个附加属性:position
是一个整数,告诉你在术语中问题发生的位置,而 subject
是一个可能包含额外数据的字符串,特别是您不应该在不通过 htmlentities()
编码的情况下打印到网站上的原始用户输入!
tokenize
tokenize($term)
方法对传递的术语进行分词。它返回一个包含标记的数组。标记是术语的组成部分,或者更准确地说,是术语的数学符号。标记是一个具有 Tokenizer\Token
类作为其父类的对象。它实现了 __toString()
方法,因此您可以这样做
$term = '1+(2+max(-3,3))'; $tokens = $stringCalc->tokenize($term); foreach ($tokens as $token) { echo ' '.$token.' '; }
这将会打印术语的标记,即整个术语的字符串表示。标记由三个属性组成:值、类型和位置。值由 __toString()
方法返回。类型是一个表示以下类型的常量:字符、单词或数字。位置是值字符串在术语字符串中的索引。标记没有语义意义。
parse
parse(array $tokens)
方法解析一个标记数组。它返回一个节点数组。内部它使用解析器,即 Parser\Parser
来解析标记。它将标记转换为语法树的节点。这些节点具有语义意义,例如它们是数字或运算符(请参阅 符号类型 部分以获取完整的符号类型列表)。它们还有一个层次结构,也称为“语法树”中的“树”。术语中的括号在语法树中创建一个节点。
使用示例
$term = '1+(2+max(-3,3))'; $tokens = $stringCalc->tokenize($term); $rootNode = $stringCalc->parse($tokens); $rootNode->traverse(function($node, $level) { echo str_repeat('__', $level).' ['.get_class($node).']<br>'; });
此示例代码将可视化语法树。它使用 traverse(Closure $callback)
方法遍历树的全部节点。节点的级别通过缩进进行可视化,并打印节点对象的类名以显示节点的类型。一个节点实现了抽象的 Parser\Nodes\AbstractNode
类。有三种类型的节点:容器节点(表示括号内的内容)、函数节点(表示数学函数及其参数)和符号节点,它们代表某些类型的数学符号(数字、运算符等)。这些类位于 Parser\Nodes
命名空间。
addSymbol
如果您想将自定义符号添加到符号列表中,请调用 addSymbol()
方法。它有两个参数。第一个参数名为 $symbol
是符号对象。因此,对象必须扩展抽象类 Symbol\AbstractSymbol
。第二个参数名为 $replaceSymbol
是可选的,允许您替换符号列表中的符号。如果您想使用此参数,您必须传递您想替换的类的完整名称。
示例
class ExampleClassOne extends AbstractConstant { protected $identifiers = ['exampleConst']; protected $value = 123; } // The AbstractSymbol class has this dependency: $stringHelper = $container->get('stringcalc_stringhelper'); $symbol = new ExampleClassOne($stringHelper); $replaceSymbol = ExampleClassTwo::class; $stringCalc->addSymbol($symbol, $replaceSymbol);
addSymbol()
方法只是一个快捷方式,您也可以在符号容器对象上调用此方法。此对象还具有一个 remove
方法,用于从容器中删除符号。
如果您想添加一个新的符号,它不能直接从Symbol\AbstractSymbol
类扩展,而必须从扩展Symbol\AbstractSymbol
类的抽象符号类型类之一扩展。这种约束的原因是,这些类具有语义意义,这种意义并非体现在这些类本身,而是在其他类(如分词器和解析器)中实现的。请参阅符号类型部分,以熟悉符号类型类。
getSymbolContainer
getSymbolContainer()
方法是一个获取器方法,用于返回符号容器。符号容器实现了Symbols\SymbolContainerInterface
,并包含所有已注册符号的实例。它具有多个方法,如add()
、remove()
、size()
和getAll()
。
getContainer
getContainer()
方法是一个获取器方法,用于返回服务容器。有关更多详细信息,请参阅构造函数的相关说明。容器没有设置器方法,您只能通过构造函数来设置它。
符号类型
一个术语由具有特定类型的符号组成。本节列出了所有可用的符号类型。
数字
术语中的数字始终由数字组成,可能包含一个正好一个的点。良好用法示例
0
00
123
4.56
.7
不良用法示例
0.1.2 // Two periods
2.2e3 // "e" will work in PHP code but not in a term
7E-10 // "E" will work in PHP code but not in a term
0xf4c3b00c // Hexadecimal notation is not allowed
0b10100111001 // Binary notation is not allowed
-1 // This is not a number but the "-" operator plus the number "1"
仅供参考:从分词器的角度来看,术语中的数字始终是正数。这意味着分词器将术语-1
分为两部分:-
和1
。
💡 注意:PHP浮点数的分数部分长度有限。如果术语中的数字分数部分较长,则分数部分将在某处被截断。
数字实现
只有一个具体的数字类:Symbols\Concrete\Number
。它扩展了抽象类Symbols\AbstractNumber
。它没有实现任何行为。它基本上是术语中具体数字的占位符。
括号
术语中有两种类型的括号:开括号和闭括号。没有其他类型化。例如,可以有实现括号()
和方括号[]
支持的类,但它们将被同等对待。因此,即使从数学角度来看这可能不是有效的术语,这也是一个有效的术语:[1+)
对于每个开括号,必须有一个闭括号,反之亦然。良好用法示例
(1+1)
(1)
((1+2)*(3+4))
不良用法示例
() // Empty brackets
(1+1 // Missing closing bracket
1+1) // Missing opening bracket
)1+1( // Missing opening bracket for the closing bracket, missing closing bracket for the open bracket
括号实现
Symbols\AbstractBracket
类是所有括号的基类。它由抽象类Symbols\AbstractOpeningBracket
和Symbols\AbstractClosingBracket
扩展。这些类由具体类扩展:Symbols\Concrete\OpeningBracket
和Symbols\Concrete\ClosingBracket
。这些类没有实现行为。
常量
术语中的常量通常表示数学常数,例如π。
用法示例
pi
PI
1+pi*2
常量实现
Symbols\AbstractConstant
类是所有常量的基类。有几个具体的常量扩展了此类。
常量类有一个名为value
的属性,用于存储常量的值。在具体常量类中可以覆盖此值,也可以覆盖getValue()
获取器方法。
运算符
术语中的运算符可以是单一运算符、二元运算符,甚至两者都是。然而,如果它们是单一运算符,则它们必须遵循前缀表示法(例如:-1
)。
单一运算符示例:-1
二元运算符示例:2-1
运算符实现
Symbols\AbstractOperator
类是所有运算符的基类。有几个具体的运算符扩展了此类。
请注意,运算符与函数密切相关。函数至少与运算符一样强大。如果运算符似乎不适合某个用途,则函数可能是一个合适的替代方案。
操作符类实现了 operate($leftNumber, $rightNumber)
方法。其参数表示操作数。可能令人困惑的是,即使操作符是一元操作符,其 operate
方法也需要提供两个参数。$rightNumber
参数将是一元操作的运算数,而左侧将是 0。
函数
术语中的函数表示数学函数。通常,函数的文本表示由两个或更多字母组成,例如:min
使用函数的示例
abs(-1)
ABS(-1)
abs(1+abs(2))
min(1,2)
min(1,2,3)
不良用法示例
abs-1 // Missing brackets
min(1,) // Missing argument
函数实现
Symbols\AbstractFunction
类是所有函数的基类。有几个具体函数扩展了此类。
请注意,操作符与函数密切相关。函数至少与操作符一样强大。如果一个操作符似乎不适合某个用途,那么函数应该是一个合适的替代方案。
函数类实现了 execute(array $arguments)
方法。参数以数组的形式传递给此方法。参数数组的大小可以是 0 到 n。此方法的实现负责验证参数数量。如果参数数量不正确,则抛出 Exceptions\NumberOfArgumentsException
。示例
if (sizeof($arguments) < 1) { throw new NumberOfArgumentsException('Error: Expected at least one argument, none given.'); }
$arguments
数组的项始终为 int 或 float 类型。它们永远不会为 null。
分隔符
分隔符用于分隔(数学)函数的参数。默认情况下,有一个标识符为 ,
的分隔符符号。
示例
max(1,2)
max(1,2,3)
不良用法示例
3+1,2 // Usage out of scope / missusage as decimal mark
max(1,,3) // Missing calculable expression between separators
分隔符实现
Symbols\AbstractSeparator
类是所有分隔符的基类。只有一个具体类扩展了此类:Symbols\Concrete\Separator
语法
本节讨论 StringCalc 可以处理的术语的语法。
语法与实现
请注意,解析器(Parser\Parser
类)和计算器(Calculator\Calculator
类)的实现并不完全模仿下面定义的生产规则。因此,如果将实际实现与语法规则进行比较,请不要感到困扰。
语法定义
S := expression
expression := number | constant | function
expression := openingBracket expression closingBracket
expression := [unaryOperator] simpleExpression (operator [unaryOperator] simpleExpression)*
simpleExpression := number | constant | function
simpleExpression := openingBracket expression closingBracket
simpleExpression := simpleExpression (operator [unaryOperator] simpleExpression)*
function := functionBody openingBracket closingBracket
function := functionBody openingBracket expression (argumentSeparator expression)* closingBracket
请记住,数字始终是正数!术语“-1”由一元操作符后跟一个数字组成。
测试
在 StringCalc 目录中运行 composer install
,然后运行测试
./vendor/phpunit/phpunit/phpunit
一般备注
-
内部,此库使用 PHP 的数学常量、操作符和函数来计算术语。因此,作为一个经验法则,请将您对 PHP 中数学的了解转移到 StringCalc 中的数学。这也适用于 PHP 的浮点精度问题。例如,在 PHP 中(以及 StringCalc 中)
(0.1 + 0.7) * 10
不是 8 也不是 8.0,而是 7.9999999999999991118…。 -
此库不提供对十进制以外的任何数制的支持。它无意提供此类支持。因此,如果您需要其他数制(如二进制数制)的支持,这可能不是您选择的库。
-
本文档中的命名空间是相对的。例如,命名空间
Exceptions\StringCalcException
指的是\ChrisKonnertz\StringCalc\Exceptions\StringCalcException
。 -
此库的版本 100% 无 已知 错误。代码中有些 TODO 备注,但这些指的是可能的改进,而不是错误。
-
一般建议:此库的代码有良好的文档。因此,请毫不犹豫地仔细查看 实现。
-
此库的代码格式符合 PSR-2 标准。
-
该仓库状态:维护中。提交一个问题,您将得到回复。