如何使用 PHP 从布尔表示法 (DSL) 创建数组树

How to create an array tree from boolean notation (DSL) with PHP

提问人:MarkokraM 提问时间:1/20/2017 最后编辑:MarkokraM 更新时间:1/21/2017 访问量:214

问:

我的输入很简单:

$input = '( ( "M" AND ( "(" OR "AND" ) ) OR "T" )';

其中 ( 在树上开始一个新节点,然后 ) 结束它。AND 和 OR 单词是为布尔运算保留的,因此除非它们不在 “” 标记内,否则它们具有特殊的含义。在我的 DSL 中,AND 和 OR 子句更改了 By 节点级别,因此在级别上只能有 AND 或 OR 子句。如果 AND 在 OR 之后,它将启动一个新的子节点。“”中的所有字符都应按原样看待。最后“可以像往常一样用\”逃脱。

在PHP中翻译句子的好方法是什么:

$output = array(array(array("M" , array("(", "AND")) , "T"), FALSE);

请注意,FALSE 是一个指示符,表示根级别具有 OR 关键字。如果输入为:

( ( "M" AND ( "(" OR "AND" ) ) AND "T" )

则输出为:

$output = array(array(array("M", array("(", "AND")), "T"), TRUE);

使用 replace('(', 'array('); 和 eval 代码很诱人,但转义字符和包装文字会成为一个问题。

目前我没有在 DSL 上实现 NOT 布尔运算符。

感谢您的帮助。JavaSript 代码也可以。

Python 示例:

在使用 PHP 和 Javascript 之前,我用 Python 做了一些测试。我所做的是:

  1. 使用正则表达式查找字符串文本
  2. 将文本替换为生成的键
  3. 将文字存储到 assoc 列表
  4. 通过括号将输入拆分为单级列表
  5. 查找根级布尔运算符
  6. 摆脱布尔运算符和空格
  7. 将文本键替换为存储值

它可能会起作用,但我相信一定有更复杂的方法可以做到这一点。

http://codepad.org/PdgQLviI

PHP 节点 DSL 分层数据 布尔逻辑

评论

0赞 shudder 1/20/2017
什么表示嵌套级别的关键字? 而不是在你的例子中。OR( "(" AND "AND" )( "(" OR "AND" )
0赞 MarkokraM 1/20/2017
它由上层表示,由根层表示。所以我实际上只需要根级别的 AND/OR 子句。其他级别来回交换子句。我希望我理解正确......
0赞 MarkokraM 1/20/2017
这意味着 AND/OR 子句在根级别以外的任何级别上都是可选的。只是为了让一切更具可读性。
1赞 Ira Baxter 1/20/2017
您要构建的是解析器。请参阅我的 SO 答案,了解如何构建递归下降解析器,这些解析器非常适合解析此类简单表达式。stackoverflow.com/questions/2245962/......

答:

1赞 shudder 1/20/2017 #1

这是我的副项目的库修改。它应该处理这些字符串 - 执行一些压力测试,如果它在某个地方损坏,请告诉我。

需要 tokenizer 类型类来提取和标记变量,因此它们不会干扰语法解析和标记化 parethesis,以便它们可以直接匹配(-evaluate 的内容不会捕获嵌套级别,并且会覆盖同一级别的所有上下文)。它还具有一些关键字语法(比需要的多一点,因为它只会针对根级别进行解析)。尝试使用错误的键访问变量注册表以及括号不匹配时引发。lazygreedyInvalidArgumentExceptionRuntimeException

class TokenizedInput
{
    const VAR_REGEXP         = '\"(?P<string>.*?)\"';
    const BLOCK_OPEN_REGEXP  = '\(';
    const BLOCK_CLOSE_REGEXP = '\)';
    const KEYWORD_REGEXP     = '(?<keyword>OR|AND)';

    // Token: <TOKEN_DELIM_LEFT><TYPE_TOKEN><ID_DELIM>$id<TOKEN_DELIM_RIGHT>
    const TOKEN_DELIM_LEFT  = '<';
    const TOKEN_DELIM_RIGHT = '>';

    const VAR_TOKEN         = 'VAR';
    const KEYWORD_TOKEN     = 'KEYWORD';
    const BLOCK_OPEN_TOKEN  = 'BLOCK';
    const BLOCK_CLOSE_TOKEN = 'ENDBLOCK';

    const ID_DELIM  = ':';
    const ID_REGEXP = '[0-9]+';

    private $original;
    private $tokenized;
    private $data = [];

    private $blockLevel = 0;
    private $varTokenId = 0;

    protected $procedure = [
        'varTokens'    => self::VAR_REGEXP,
        'keywordToken' => self::KEYWORD_REGEXP,
        'blockTokens'  => '(?P<open>' . self::BLOCK_OPEN_REGEXP . ')|(?P<close>' . self::BLOCK_CLOSE_REGEXP . ')'
    ];

    private $tokenMatch;

    public function __construct($input) {
        $this->original = (string) $input;
    }

    public function string() {
        isset($this->tokenized) or $this->tokenize();
        return $this->tokenized;
    }

    public function variable($key) {
        isset($this->tokenized) or $this->tokenize();
        if (!isset($this->data[$key])) {
            throw new InvalidArgumentException("Variable id:($key) does not exist.");
        }
        return $this->data[$key];
    }

    public function tokenSearchRegexp() {
        if (!isset($this->tokenMatch)) {
            $strings  = $this->stringSearchRegexp();
            $blocks   = $this->blockSearchRegexp();
            $this->tokenMatch = '#(?:' . $strings . '|' . $blocks . ')#';
        }
        return $this->tokenMatch;
    }

    public function stringSearchRegexp($id = null) {
        $id = $id ?: self::ID_REGEXP;
        return preg_quote(self::TOKEN_DELIM_LEFT . self::VAR_TOKEN . self::ID_DELIM)
            . '(?P<id>' . $id . ')'
            . preg_quote(self::TOKEN_DELIM_RIGHT);
    }

    public function blockSearchRegexp($level = null) {
        $level = $level ?: self::ID_REGEXP;
        $block_open = preg_quote(self::TOKEN_DELIM_LEFT . self::BLOCK_OPEN_TOKEN . self::ID_DELIM)
            . '(?P<level>' . $level . ')'
            . preg_quote(self::TOKEN_DELIM_RIGHT);
        $block_close = preg_quote(self::TOKEN_DELIM_LEFT . self::BLOCK_CLOSE_TOKEN . self::ID_DELIM)
            . '\k<level>'
            . preg_quote(self::TOKEN_DELIM_RIGHT);
        return $block_open . '(?P<contents>.*)' . $block_close;
    }

    public function keywordSearchRegexp($keyword = null) {
        $keyword = $keyword ? '(?P<keyword>' . $keyword . ')' : self::KEYWORD_REGEXP;
        return preg_quote(self::TOKEN_DELIM_LEFT . self::KEYWORD_TOKEN . self::ID_DELIM)
            . $keyword
            . preg_quote(self::TOKEN_DELIM_RIGHT);
    }

    private function tokenize() {
        $current = $this->original;
        foreach ($this->procedure as $method => $pattern) {
            $current = preg_replace_callback('#(?:' . $pattern . ')#', [$this, $method], $current);
        }

        if ($this->blockLevel) {
            throw new RuntimeException("Syntax error. Parenthesis mismatch." . $this->blockLevel);
        }

        $this->tokenized = $current;
    }

    protected function blockTokens($match) {
        if (isset($match['close'])) {
            $token = self::BLOCK_CLOSE_TOKEN . self::ID_DELIM . --$this->blockLevel;
        } else {
            $token = self::BLOCK_OPEN_TOKEN . self::ID_DELIM . $this->blockLevel++;

        }

        return $this->addDelimiters($token);
    }

    protected function varTokens($match) {
        $this->data[$this->varTokenId] = $match[1];
        return $this->addDelimiters(self::VAR_TOKEN . self::ID_DELIM . $this->varTokenId++);
    }

    protected function keywordToken($match) {
        return $this->addDelimiters(self::KEYWORD_TOKEN . self::ID_DELIM . $match[1]);
    }

    private function addDelimiters($token) {
        return self::TOKEN_DELIM_LEFT . $token . self::TOKEN_DELIM_RIGHT;
    }
}

解析器类型类对标记化字符串执行匹配 - 提取已注册的变量,并通过克隆本身递归进入嵌套上下文。 运算符类型处理是不寻常的,这使得它更像是一个派生类,但无论如何,在解析器的世界里很难实现令人满意的抽象。

class ParsedInput
{
    private $input;
    private $result;
    private $context;

    public function __construct(TokenizedInput $input) {
        $this->input = $input;
    }

    public function result() {
        if (isset($this->result)) { return $this->result; }

        $this->parse($this->input->string());
        $this->addOperator();

        return $this->result;
    }

    private function parse($string, $context = 'root') {
        $this->context = $context;
        preg_replace_callback(
            $this->input->tokenSearchRegexp(),
            [$this, 'buildStructure'],
            $string
        );

        return $this->result;
    }

    protected function buildStructure($match) {
        if (isset($match['contents'])) { $this->parseBlock($match['contents'], $match['level']); }
        elseif (isset($match['id'])) { $this->parseVar($match['id']); }
    }

    protected function parseVar($id) {
        $this->result[] = $this->input->variable((int) $id);
    }

    protected function parseBlock($contents, $level) {
        $nested = clone $this;
        $this->result[] = $nested->parse($contents, (int) $level);
    }

    protected function addOperator() {
        $subBlocks = '#' . $this->input->blockSearchRegexp(1) . '#';
        $rootLevel = preg_replace($subBlocks, '', $this->input->string());
        $rootKeyword = '#' . $this->input->keywordSearchRegexp('AND') . '#';
        return $this->result[] = (preg_match($rootKeyword, $rootLevel) === 1);
    }

    public function __clone() {
        $this->result = [];
    }
}

用法示例:

$input = '( ( "M" AND ( "(" OR "AND" ) ) AND "T" )';

$tokenized = new TokenizedInput($input);
$parsed = new ParsedInput($tokenized);

$result = $parsed->result();

我删除了命名空间/导入/intrefaces,因此您可以根据需要进行调整。也不想挖掘(现在可能无效)评论,所以也删除了它们。

评论

0赞 MarkokraM 1/21/2017
parseBlock 上的克隆方法和 ParsedInput 上的结果方法似乎不起作用。我的环境:PHP 5.6.25 (cli) (构建时间:2016 年 8 月 18 日 11:39:15) 版权所有 (c) 1997-2016 PHP Group Zend Engine v2.6.0,版权所有 (c) 1998-2016 Zend Technologies 如果我不更改为克隆 $this->,文件将无法编译......而不是 (clone $this)->
0赞 MarkokraM 1/21/2017
如果在没有级别参数的情况下调用 addOperator 中的 blockSearchRegexp,那么结果会更好,你可能知道原因吗?
1赞 shudder 1/21/2017
@MarkokraM 代码中的一些php7风格(1.operator precedence - error,2.primitive typehints - in 等)。现在应该没问题了。cloneblockSearchRegexp()