Сверхсильный парсер для сбалансированных вложенных скобок - PullRequest
0 голосов
/ 01 октября 2018

Я изо всех сил пытаюсь придумать парсер superpower для набора частичных входов ниже (вложенные сбалансированные скобки с разделителем '|').

Внутри может идти произвольный текстПарены, включая пробелы, другие токены и "()".Только '|', '(', ')', должны иметь здесь особое значение (перевод строки также заканчивает последовательность).Чтобы быть действительным, каждая сбалансированная, заключенная в скобки группа должна иметь '|'и, по крайней мере, один символ, который не является '(' или ')'.

В идеале парсер должен разбивать каждый ввод в список, с элементами (терминальной) строкой или массивом строк, следующим образом:

Действительный:

(a|)                    ->      { "a", "" }
(a | b)                 ->      { "a", "b" }
(a | b.c())             ->      { "a", "b.c()" }
(aa | bb cc )           ->      { "aa" "bb cc" }
(a | b | c #dd)         ->      { "a", "b", "c #dd"}
((a | b) | $c)          ->      { { "a", "b" }, "$c" }
((a | b) | (c | d))     ->      { { "a", "b" }, { "c", "d" } }
(((a | b) | c) | d)     ->      { { { "a", "b" }, "c" }, "d" }
...

Недопустимый / игнорируемый:

()
())
(()
(|)
(|())
(.)
(())
(()|())
(abc)
(a bc)
(a.bc())
...

Мои токены (для целей здесь) следующие:

public enum Tokens
{        
    [Token(Example = "(")]
    LParen,

    [Token(Example = ")")]
    RParen,

    [Token(Example = "|")]
    Pipe,

    [Token(Description = "everything-else")]
    String
} 

1 Ответ

0 голосов
/ 17 октября 2018

Это было сложно, в основном из-за пробелов, которые вам нужно сохранить, но я смог придумать парсер, который отвечает вашим потребностям.Во-первых, мне пришлось немного изменить перечисление Tokens:

public enum Tokens
{
    None,
    String,
    Number,

    [Token(Example = "()")]
    OpenCloseParen,

    [Token(Example = "(")]
    LParen,

    [Token(Example = ")")]
    RParen,

    [Token(Example = "#")]
    Hash,

    [Token(Example = "$")]
    Dollar,

    [Token(Example = "|")]
    Pipe,

    [Token(Example = ".")]
    Dot,

    [Token(Example = " ")]
    Whitespace,
}

Далее мы можем построить следующие Tokenizer:

var tokenizer = new TokenizerBuilder<Tokens>()
    .Match(Span.EqualTo("()"), Tokens.OpenCloseParen)
    .Match(Character.EqualTo('('), Tokens.LParen)
    .Match(Character.EqualTo(')'), Tokens.RParen)
    .Match(Character.EqualTo('#'), Tokens.Hash)
    .Match(Character.EqualTo('$'), Tokens.Dollar)
    .Match(Character.EqualTo('.'), Tokens.Dot)
    .Match(Character.EqualTo('|'), Tokens.Pipe)
    .Match(Character.EqualTo(' '), Tokens.Whitespace)
    .Match(Span.MatchedBy(Character.AnyChar), Tokens.String)
    .Match(Numerics.Natural, Tokens.Number)
    .Build();

Затем создайте классы моделей для хранениявыходные данные (вы, вероятно, можете придумать более подходящие имена для них, поскольку я не совсем уверен, что это за данные, которые вы анализируете):

public abstract class Node
{
}

public class TextNode : Node
{
    public string Value { get; set; }
}

public class Expression : Node
{
    public Node[] Nodes { get; set; }
}

Затем мы создадим парсеры:

public static class MyParsers
{
    /// <summary>
    /// Parses any whitespace (if any) and returns a resulting string
    /// </summary>
    public readonly static TokenListParser<Tokens, string> OptionalWhitespace =
        from chars in Token.EqualTo(Tokens.Whitespace).Many().OptionalOrDefault()
        select chars == null ? "" : new string(' ', chars.Length);

    /// <summary>
    /// Parses a valid text expression
    /// e.g. "abc", "a.c()", "$c", etc.
    /// </summary>
    public readonly static TokenListParser<Tokens, Node> TextExpression =
        from tokens in
            Token.EqualTo(Tokens.OpenCloseParen)
            .Or(Token.EqualTo(Tokens.Hash))
            .Or(Token.EqualTo(Tokens.Dollar))
            .Or(Token.EqualTo(Tokens.Dot))
            .Or(Token.EqualTo(Tokens.Number))
            .Or(Token.EqualTo(Tokens.String))
            .Or(Token.EqualTo(Tokens.Whitespace))
            .Many()
        // if this side of the pipe is all whitespace, return null
        select (Node) (
            tokens.All(x => x.ToStringValue() == " ") 
            ? null
            : new TextNode {
                Value = string.Join("", tokens.Select(t => t.ToStringValue())).Trim()
            }
        );

    /// <summary>
    /// Parses a full expression that may contain text expressions or nested sub-expressions
    /// e.g. "(a | b)", "( (a.c() | b) | (123 | c) )", etc.
    /// </summary>
    public readonly static TokenListParser<Tokens, Node> Expression =
        from leadWs in OptionalWhitespace
        from lp in Token.EqualTo(Tokens.LParen)
        from nodes in TextExpression
            .Or(Parse.Ref(() => Expression))
            .ManyDelimitedBy(Token.EqualTo(Tokens.Pipe))
            .OptionalOrDefault()
        from rp in Token.EqualTo(Tokens.RParen)
        from trailWs in OptionalWhitespace
        where nodes.Length > 1 && nodes.Any(node => node != null) // has to have at least two sides and one has to be non-null
        select (Node)new Expression {
            Nodes = nodes.Select(node => node ?? new TextNode { Value = "" }).ToArray()
        };
}

И, наконец, мы можем использовать токенизатор вместе с синтаксическими анализаторами для анализа вашего ввода:

string input = "(a b | c.())";
var tokens = tokenizer.Tokenize(input);

var result = MyParsers.Expression.TryParse(tokens);
if (result.HasValue)
{
    // input is valid
    var expression = (Expression)result.Value;

    // do what you need with it here, i.e. loop through the nodes, output the text, etc.
}
else
{
    // not valid
}

Это работает практически для всех ваших тестовых случаев, кроме таких, как этот (()|()), где open /close paren - это значение по обе стороны трубы.Существует также, вероятно, лучший способ выполнить некоторые операции синтаксического анализа, поскольку я сам привыкаю к ​​Superpower, но я думаю, что это хорошая основа для начала, так что вы можете оптимизировать ее и / или интегрировать все ваши крайние случаи вэто.

РЕДАКТИРОВАТЬ

Это был пробел, который все испортил.Мне пришлось добавить больше проверок пробелов в парсере Expression, а также добавить условие для проверки непустого TextExpression, а затем также проверить наличие пустого элемента.Это было для обработки случаев, когда одна сторона трубы пуста.Вот рабочий парсер:

public readonly static TokenListParser<Tokens, Node> Expression =
    from _1 in OptionalWhitespace
    from lp in Token.EqualTo(Tokens.LParen)
    from _2 in OptionalWhitespace
    from nodes in 
        TextExpression.Where(node => node != null) // check for actual text node first
        .Or(Expression)
        .Or(TextExpression) // then check to see if it's empty
        .ManyDelimitedBy(Token.EqualTo(Tokens.Pipe))
    from _3 in OptionalWhitespace
    from rp in Token.EqualTo(Tokens.RParen)
    from _4 in OptionalWhitespace
    where nodes.Length > 1 && nodes.Any(node => node != null) // has to have at least two sides and one has to be non-null
    select (Node)new Expression {
        Nodes = nodes.Select(node => node ?? new TextNode { Value = "" }).ToArray()
    };
...