调车场功能
Shunting-yard functions
我在 Java 程序中使用调车场算法 (https://en.wikipedia.org/wiki/Shunting-yard_algorithm) 来创建计算器。我快完成了,但我仍然需要实现功能。我有 运行 个问题:我希望计算器在将 x 和 y 放在一起时自动将变量相乘 - 示例:计算器将 xy 转换为 x*y。另外,我希望计算器将 (x)(y) 转换为 (x)*(y) 并将 x(y) 转换为 x*(y)。我使用以下代码完成了所有这些工作:
infix = infix.replaceAll("([a-zA-Z])([a-zA-Z])", "*");
infix = infix.replaceAll("([a-zA-Z])\(", "*(");
infix = infix.replaceAll("\)\(", ")*(");
infix = infix.replaceAll("\)([a-zA-Z])", ")*");
(在我的计算器中,变量名总是单个字符。)
这现在很好用,但是当我实现功能时,这当然不起作用。它会将 "sin(1)" 变成 "s*i*n*(1)"。如何让这段代码只对运算符而不是函数进行乘法转换?
预处理要解析的输入并不是实现您想要的内容的好方法。文本替换不知道解析算法知道什么,您也会丢失原始输入,这对于打印有用的错误消息很有用。
相反,您应该根据上下文来决定要做什么。将先前解析的标记的类型保留为输入开头的特殊类型。
如果前一个标记是一个值标记——一个数字、一个变量名或一个子表达式的右括号——而当前一个也是一个值标记,则发出一个额外的乘法运算符。
可以使用相同的逻辑来确定减号是一元否定还是二元减法:如果在值标记之后找到减号,则为减法,否则为否定。
您将 x(y)
转换为 x * (y)
的想法当然会与函数调用语法冲突。
我们可以将其分为两部分。括号表达式有一个规则,乘法有另一个规则。
而不是维基百科文章,这是为了解释目的而故意简化的,我会遵循一个更详细的例子,比如 Parsing Expressions by Recursive Descent 处理括号表达式。
这是我用于解析器的代码,它可以使用隐式乘法。我有多个字母的变量名,并使用 space 来分隔不同的变量,这样你就可以有 "2 pi r".
protected void expression() throws ParseException {
prefixSuffix();
Token t = it.peekNext();
while(t!=null) {
if(t.isBinary()) {
pushOp(t);
it.consume();
prefixSuffix();
}
else if(t.isImplicitMulRhs()) {
pushOp(implicitMul);
prefixSuffix();
}
else
break;
t=it.peekNext();
}
while(!sentinel.equals(ops.peek())) {
popOp();
}
}
这需要一些其他功能。
我使用了一个单独的分词步骤,将输入分解为离散的分词。 Tokens
class 有很多方法,特别是 Token.isBinary()
测试运算符是否是像 +,=,*,/ 这样的二元运算符。另一种方法 Token.isImplicitMulRhs()
测试标记是否可以出现在隐式乘法的右侧,这适用于数字、变量名和左括号。
一个Iterator<Token>
用于输入流。 it.peekNext()
查看下一个标记,it.consume()
移动到输入中的下一个标记。
pushOp(Token)
将一个令牌压入运算符堆栈,而 popOp
移除一个令牌并 . pushOp 具有处理不同运算符优先级的逻辑。如果优先级较低,则弹出运算符
protected void pushOp(Token op)
{
while(compareOps(ops.peek(),op))
popOp();
ops.push(op);
}
需要特别注意的是 implicitMul
一个与乘法具有相同优先级的人工标记,它被压入运算符堆栈。
prefixSuffix()
处理可以是数字和变量的表达式,带有可选的后缀运算符前缀。这将识别“2”、"x"、“-2”、"x++" 从输入中删除标记并将它们添加到适当的 output/operator 堆栈中。
我们可以把BNF中的这个套路想成
<expression> ::=
<prefixSuffix> ( <binaryOp> <prefixSuffix> )* // normal binary ops x+y
| <prefixSuffix> ( <prefixSuffix> )* // implicit multiplication x y
括号处理在 prefixSuffix()
中完成。如果检测到左括号,它将递归调用 expression()
。为了检测匹配的右括号,一个特殊的哨兵标记被压入运算符堆栈。当在输入中遇到右括号时,主循环中断,运算符堆栈中的所有运算符弹出,直到遇到哨兵,控制权返回 prefixSuffix()
。代码可能像
void prefixSuffix() {
Token t = it.peekNext();
if(t.equals('(')) {
it.consume(); // advance the input
operatorStack.push(sentinel);
expression(); // parse until ')' encountered
t = it.peekNext();
if(t.equals(')')) {
it.consume(); // advance the input
return;
} else throw Exception("Unmatched (");
}
// handle variable names, numbers etc
}
另一种方法可能是使用标记,类似于解析器的工作方式。
第一阶段是将输入文本转换为标记列表,标记是表示找到的实体类型及其值的对象。
例如,您可以有一个变量标记,它的值是变量的名称('x'、'y' 等)、左括号或右括号的标记等。
因为,我假设,你事先知道计算器可以使用的函数的名称,你也会有一个函数标记,它的值是函数名称。
因此标记化阶段的输出区分变量和函数。
实现这个并不难,总是先尝试匹配函数名,
所以 "sin" 将被识别为一个函数而不是三个变量。
现在第二阶段可以插入缺失的乘法运算符。现在这并不难,因为您知道只需将它们插入到:
{变量,RIGHT_PAREN}和{变量,LEFT_PAREN,函数}
但绝不会介于 FUNCTION 和 LEFT_PAREN 之间。
我在 Java 程序中使用调车场算法 (https://en.wikipedia.org/wiki/Shunting-yard_algorithm) 来创建计算器。我快完成了,但我仍然需要实现功能。我有 运行 个问题:我希望计算器在将 x 和 y 放在一起时自动将变量相乘 - 示例:计算器将 xy 转换为 x*y。另外,我希望计算器将 (x)(y) 转换为 (x)*(y) 并将 x(y) 转换为 x*(y)。我使用以下代码完成了所有这些工作:
infix = infix.replaceAll("([a-zA-Z])([a-zA-Z])", "*");
infix = infix.replaceAll("([a-zA-Z])\(", "*(");
infix = infix.replaceAll("\)\(", ")*(");
infix = infix.replaceAll("\)([a-zA-Z])", ")*");
(在我的计算器中,变量名总是单个字符。)
这现在很好用,但是当我实现功能时,这当然不起作用。它会将 "sin(1)" 变成 "s*i*n*(1)"。如何让这段代码只对运算符而不是函数进行乘法转换?
预处理要解析的输入并不是实现您想要的内容的好方法。文本替换不知道解析算法知道什么,您也会丢失原始输入,这对于打印有用的错误消息很有用。
相反,您应该根据上下文来决定要做什么。将先前解析的标记的类型保留为输入开头的特殊类型。
如果前一个标记是一个值标记——一个数字、一个变量名或一个子表达式的右括号——而当前一个也是一个值标记,则发出一个额外的乘法运算符。
可以使用相同的逻辑来确定减号是一元否定还是二元减法:如果在值标记之后找到减号,则为减法,否则为否定。
您将 x(y)
转换为 x * (y)
的想法当然会与函数调用语法冲突。
我们可以将其分为两部分。括号表达式有一个规则,乘法有另一个规则。
而不是维基百科文章,这是为了解释目的而故意简化的,我会遵循一个更详细的例子,比如 Parsing Expressions by Recursive Descent 处理括号表达式。
这是我用于解析器的代码,它可以使用隐式乘法。我有多个字母的变量名,并使用 space 来分隔不同的变量,这样你就可以有 "2 pi r".
protected void expression() throws ParseException {
prefixSuffix();
Token t = it.peekNext();
while(t!=null) {
if(t.isBinary()) {
pushOp(t);
it.consume();
prefixSuffix();
}
else if(t.isImplicitMulRhs()) {
pushOp(implicitMul);
prefixSuffix();
}
else
break;
t=it.peekNext();
}
while(!sentinel.equals(ops.peek())) {
popOp();
}
}
这需要一些其他功能。
我使用了一个单独的分词步骤,将输入分解为离散的分词。 Tokens
class 有很多方法,特别是 Token.isBinary()
测试运算符是否是像 +,=,*,/ 这样的二元运算符。另一种方法 Token.isImplicitMulRhs()
测试标记是否可以出现在隐式乘法的右侧,这适用于数字、变量名和左括号。
一个Iterator<Token>
用于输入流。 it.peekNext()
查看下一个标记,it.consume()
移动到输入中的下一个标记。
pushOp(Token)
将一个令牌压入运算符堆栈,而 popOp
移除一个令牌并 . pushOp 具有处理不同运算符优先级的逻辑。如果优先级较低,则弹出运算符
protected void pushOp(Token op)
{
while(compareOps(ops.peek(),op))
popOp();
ops.push(op);
}
需要特别注意的是 implicitMul
一个与乘法具有相同优先级的人工标记,它被压入运算符堆栈。
prefixSuffix()
处理可以是数字和变量的表达式,带有可选的后缀运算符前缀。这将识别“2”、"x"、“-2”、"x++" 从输入中删除标记并将它们添加到适当的 output/operator 堆栈中。
我们可以把BNF中的这个套路想成
<expression> ::=
<prefixSuffix> ( <binaryOp> <prefixSuffix> )* // normal binary ops x+y
| <prefixSuffix> ( <prefixSuffix> )* // implicit multiplication x y
括号处理在 prefixSuffix()
中完成。如果检测到左括号,它将递归调用 expression()
。为了检测匹配的右括号,一个特殊的哨兵标记被压入运算符堆栈。当在输入中遇到右括号时,主循环中断,运算符堆栈中的所有运算符弹出,直到遇到哨兵,控制权返回 prefixSuffix()
。代码可能像
void prefixSuffix() {
Token t = it.peekNext();
if(t.equals('(')) {
it.consume(); // advance the input
operatorStack.push(sentinel);
expression(); // parse until ')' encountered
t = it.peekNext();
if(t.equals(')')) {
it.consume(); // advance the input
return;
} else throw Exception("Unmatched (");
}
// handle variable names, numbers etc
}
另一种方法可能是使用标记,类似于解析器的工作方式。
第一阶段是将输入文本转换为标记列表,标记是表示找到的实体类型及其值的对象。 例如,您可以有一个变量标记,它的值是变量的名称('x'、'y' 等)、左括号或右括号的标记等。 因为,我假设,你事先知道计算器可以使用的函数的名称,你也会有一个函数标记,它的值是函数名称。 因此标记化阶段的输出区分变量和函数。
实现这个并不难,总是先尝试匹配函数名, 所以 "sin" 将被识别为一个函数而不是三个变量。
现在第二阶段可以插入缺失的乘法运算符。现在这并不难,因为您知道只需将它们插入到:
{变量,RIGHT_PAREN}和{变量,LEFT_PAREN,函数}
但绝不会介于 FUNCTION 和 LEFT_PAREN 之间。