jzaaa / string-calc
StringCalc是一个用于处理字符串形式的数学表达式(术语)的PHP计算库,基于jzaaa和chriskonnertz的string-calc进行扩展
Requires
- php: >=5.6.0
This package is auto-updated.
Last update: 2024-09-23 09:17:34 UTC
README
StringCalc是一个用于处理字符串形式的数学表达式(术语)的PHP计算库。
安装
通过Composer
composer require jzaaa/string-calc:dev-variables-excel
之后,您可以通过运行composer update来获取此库的最新版本。
您可以在不使用Composer的情况下使用此库,但那时需要注册一个自动加载函数。
此库需要PHP 5.6或更高版本。
使用示例
以下是一个计算术语的最小化PHP代码示例。它假设存在一个自动加载器。
$stringCalc = new ChrisKonnertz\StringCalc\StringCalc(); $term = '1+2'; $result = $stringCalc->calculate($term); // $result will contain 3
为了解释所有这些更改的动机,这里有一个非常简化但可工作的示例代码。在实际代码中,会有很多验证和术语排序,借助tokenize()方法。
$r = [ '$input' => 1000, '$cond1' => true, '$cond2' => false, ]; $math = [ '$bonus1' => 'if($cond1, 30, 0)', '$bonus2' => 'if($cond1 && !$cond2, 99, 0)', '$deduction' => '$input * 0.15', '$result' => '$input - $deduction + $bonus1 + $bonus2', ]; $stringCalc = new ChrisKonnertz\StringCalc\StringCalc(); foreach ($math as $variable => $term) { $r[$variable] = $stringCalc->calculate($term, $r); } print_r($r);
包含一个交互式的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)
roundUp(1.23, 1) // 1.3
roundUp(1.2) // 1.3
sum(1, 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
比较和逻辑运算符以及if函数
1||0 // 1 - bool result is converted to int
123&&23 // 1 - int/float operators are converted to bool before operator execution
!1||0 // 0 - 'Not' operates as unary
13<15 // 1
if(1!=2, 3, 4) // 3 - if function in excel style
if(0, 1) // false
and(1, 2) // true
and(1, 0, 1) // false
1=1 // 1,same as 1==1
1=0 // 0
变量
$var1 + $var2
$result = $stringCalc->calculate($term, ['$var1'=>1, '$var2'=>2]); // $result will contain 3
要查看所有可用数学符号(术语的一部分)的类型列表,请点击以下链接:Symbols/Concrete classes
符号列表
运算符
+
-
*
/
<
<=
>
>=
==
= // same as '=='
!=
||
&&
函数
abs
aCos
aCosH
and
aSin
aSinH
aTan
aTanH
aTanTwo
ceil
cos
cosH
degToRad
en
exp
expMOne
floor
fMod
hypot
if
log
logOneP
logTen
max
min
pow
radToDeg
round
roundUp
sin
sinH
sqrt
sum
tan
tanH
常量
e
euler
lnPi
lnTen
lnTwo
logTenE
logTwoE
onePi
pi
piFour
piTwo
sqrtOneTwo
sqrtPi
sqrtThree
sqrtTwo
twoPi
twoSqrtPi
其他
(
)
,
.
StringCalc类
StringCalc是这个库的API前端。本节描述了此类的公共方法。
构造函数
构造函数有一个名为 $container 的可选参数,该参数实现了 Container\ContainerInterface。这是 StringCalc 使用的服务容器。如果没有传递参数,构造函数将自行创建一个类型为 Container\Container 的新容器对象。容器接口确保容器实现了 PSR-11 标准。因此,您可以用任何实现 PSR-11 标准的其他容器替换默认容器,但您必须将其包装在一个使它兼容于 Container\Container 类的包装类中。我们建议避免额外的开销,并直接扩展 Container\Container 类。
calculate
calculate() 方法是这个类最重要的方法。它期望一个类型为字符串的参数,名为 $term。它返回一个类型为 float 或 int 的数字。我们强烈建议在这个方法的所有调用周围包裹一个 try-catch 块,并编写一个捕获所有类型为 Exceptions\StringCalcException 的异常的 catch 语句。
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
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
注意:逗号字符仅用作函数参数的分隔符。它永远不会被解释为小数点!前者示例:max(1,2)
函数实现
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" 由一个单目运算符后跟一个数字组成。
一般说明
-
内部,此库使用 PHP 的数学常量、运算符和函数来计算项。因此,作为一般规则,请将您关于 PHP 中数学的知识转移到 StringCalc 中的数学。这也适用于 PHP 中与浮点精度相关的问题。例如,在 PHP 中,
(0.1 + 0.7) * 10不是 8 也不是 8.0,而是 7.9999999999999991118…。 -
PHP 的
intdiv函数不存在,因为它不受 PHP 5.6 支持。如果您需要它,您可以查看 此实现。 -
此类不支持除十进制数制以外的任何数制。它没有打算提供此类支持,因此如果您需要其他数制(例如二进制数制)的支持,这可能不是您选择的库。
-
本文档中的命名空间是相对的。例如,命名空间
Exceptions\StringCalcException指的是\ChrisKonnertz\StringCalc\Exceptions\StringCalcException。 -
本库的版本发布100%没有已知的错误。代码中有些TODO注释,但这些指的是可能的改进,而不是错误。
-
一般建议:本库的代码有很好的文档。因此,不要犹豫,可以更仔细地查看实现。
-
本库的代码格式遵循由PSR-2标准定义的代码风格。
-
本仓库的状态:维护中。创建一个问题,您将得到响应,通常在48小时内。