3

我正在使用 Sprache 解析文件的一部分,如下所示:

OneThing=Foo
AnotherThing=Bar
YetAnotherThing=Baz

所有三行都是强制性的,但它们可以以任何顺序出现。我有各个行的解析器,如下所示:

public static readonly Parser<string> OneThing = (
    from open in Parse.String("OneThing=")
    from rest in Parse.AnyChar.Except(Parse.LineTerminator).Many().Text()
    from newLine in Parse.LineEnd
    select rest
);

我将它们结合起来解析整个部分,如下所示:

public static readonly Parser<MyClass> Section = (
    from oneThing in SectionGrammar.OneThing
    from anaotherThing in SectionGrammar.AnotherThing
    from yetAnotherThing in SectionGrammar.YetAnotherThing
    select new MyClass(oneThing, anotherThing, yetAnotherThing)
);

但这仅在线条以 OneThing、AnotherThing、YetAnotherThing 的顺序出现时才有效。如何更改此设置以允许行以任何顺序出现但仍强制每行应出现一次?

非常感谢任何帮助!谢谢

4

3 回答 3

0

我对 Sprache 很天真,但一种可能很冗长的方法是为每一行选择每个选项的元组,然后在最终选择中过滤元组数组。就像是:

public static readonly Parser<MyClass> Section = (
select a from (from oneThing in SectionGrammar.OneThing select Tuple.Create(oneThing, null, null))
    .Or(from anotherThing in SectionGrammar.AnotherThing select Tuple.Create(null, anotherThing, null))
    .Or(from yetAnotherThing in SectionGrammar.YetAnotherThing select Tuple.Create(null, null, yetAnotherThing))

select b from (from oneThing in SectionGrammar.OneThing select Tuple.Create(oneThing, null, null))
    .Or(from anotherThing in SectionGrammar.AnotherThing select Tuple.Create(null, anotherThing, null))
    .Or(from yetAnotherThing in SectionGrammar.YetAnotherThing select Tuple.Create(null, null, yetAnotherThing))

select c from (from oneThing in SectionGrammar.OneThing select Tuple.Create(oneThing, null, null))
    .Or(from anotherThing in SectionGrammar.AnotherThing select Tuple.Create(null, anotherThing, null))
    .Or(from yetAnotherThing in SectionGrammar.YetAnotherThing select Tuple.Create(null, null, yetAnotherThing))

select new MyClass(
    new[] { a, b, c }.Where(i => i.Item1 != null).Select(i => i.Item1).First(),
    new[] { a, b, c }.Where(i => i.Item2 != null).Select(i => i.Item2).First(),
    new[] { a, b, c }.Where(i => i.Item3 != null).Select(i => i.Item3).First()
));

但感觉应该有更好的方法。如果您对我说有 20 行具有独特的解析并且可能以不同的顺序排列,那么上述内容也不是很可扩展。

于 2020-05-28T09:53:34.290 回答
0

我不认为你可以单独使用 Sprache 解析器来做到这一点,但它可以与其他一些集成到其中的自定义逻辑结合使用。

public static List<string> ExpectedThings = new List<string>(new[] { 
    "OneThing", 
    "AnotherThing", 
    "YetAnotherThing" 
});

public static string SelectThingValue(string thingKey, string thingVal)
{
    if (ExpectedThings.IndexOf(thingKey) == -1)
    {                
        throw new ParseException($"Already parsed an instance of '{thingKey}'.");
    }
    
    ExpectedThings.Remove(thingKey);
    
    return thingVal;
}

public static readonly Parser<string> ThingParser = (
    from key in ExpectedThings.Aggregate((Parser<string>)null, (acc, thing) => {
        var nextThingParser = Parse.String(thing).Text();
        return acc == null ? nextThingParser : acc.Or(nextThingParser);
    })
    from eq in Parse.Char('=')
    from val in Parse.AnyChar.Except(Parse.LineTerminator).Many().Text()
    select SelectThingValue(key, val)
);

public static MyClass ParseThings()
{
    const string input = @"OneThing=Foo
AnotherThing=Bar
YetAnotherThing=Baz";

    string[] vals = ThingParser.DelimitedBy(Parse.LineEnd).Parse(input).ToArray();

    if (ExpectedThings.Any())
    {
        throw new ParseException($"Missing things in input string: {string.Join(", ", ExpectedThings.Select(thing => $"'{thing}'"))}");
    }

    return new MyClass(vals[0], vals[1], vals[2]);
}

static void Main(string[] args)
{
    MyClass myClass = ParseThings();
}

这里的想法是您将预期的“事物”输入到ExpectedThings列表中。然后通过使用 LINQ 的函数ThingParser动态链接列表中每个项目的调用来构建它。在解析器的部分,它调用which is there 来删除刚刚从列表中解析的东西,所以我们知道该东西已经被解析了。它还会检查以确保该事物尚未被解析,如果已解析,它将引发异常。它所做的最后一项检查是查看是否还有任何项目,如果有,则意味着它没有解析其中一个。由于所有这些都是必需的,因此我们在这里抛出一个错误。.Or()Aggregate()selectSelectThingValue()ExpectedThings

根据您的实际用例,您绝对可以使这更加结构化和动态化,但这基于您问题中的示例。这里的所有内容也是静态的,但您也可以更改它以允许您在ExpectedThings.

于 2020-08-05T15:01:37.907 回答
0

有一种方法,但它很丑- 不幸的是。总结一下它的关键点:

  • 您将必须创建自己的解析器。
  • 构造函数初始化和不变性消失了——你会明白为什么。
  • 将值传递给您的对象的方式非常hacky - 至少在我看来。
  • 你不能保证每个键出现一次
  • 它是如此庞大和复杂,以至于您不妨说“不,让我们在每一行结尾拆分,然后拆分=,然后从那里取出它。”

这个解决方案不是我的——我从来没有想过这样的事情。取自Mike Hadlow 的博客EasyNetQ ConnectionStringGrammar.cs中缺少的部分——令人惊讶的是,Mike 是该文件的贡献者。

首先,您创建您的属性值类型。几乎你已经在你的第一个片段上完成了这个,但不管它是:

static Parser<string> Text = Parse.AnyChar.Except(Parse.LineTerminator).Many().Text()

对于数字

static Parser<int> Number = Parse.Number.Select(int.Parse);

然后是创建键值解析器的方法

public static Parser<ThingsVisitor> CreateKeyValueParser<T>(
    string keyName,
    Parser<T> valueParser,
    Expression<Func<Things, T>> getter)
{
    return
        from key in Parse.String(keyName).Token()
        from separator in Parse.Char('=')
        from value in valueParser
        select (ThingsVisitor)(t =>
        {
            CreateSetter(getter)(t, value);
            return t;
        });
}

ThingsVisitor只是一个别名:using ThingsVisitor = System.Func<Things, Things>;. Things在 leu 中MyClassCreateSetter()从 getter 的表达式中提取 setter。我把它留给读者——如果你点击链接,这并不难。

然后你使用该方法来创建你的属性解析器

Parser<ThingsVisitor> Props = new List<Parser<ThingsVisitor>>
    {
        CreateKeyValueParser("OneThing", Text, t => t.OneThing),
        CreateKeyValueParser("AnotherThing", Number, t => t.AnotherThing),
        CreateKeyValueParser("YetAnotherThing", Text, t => t.YetAnotherThing)
    }.Aggregate((a, b) => a.Or(b));

然后为整个输入定义解析器

Parser<IEnumerable<ThingsVisitor>> PropsParser =
    from first in Props
    from rest in Parse.LineEnd.Then(_ => Props).Many()
    select new[] { first }.Concat(rest);

最后,您可以解析输入

    Things things = new();
    IEnumerable<ThingsVisitor> parser = PropsParser.Parse(@"OneThing=Foo
AnotherThing=32
YetAnotherThing=Baz");
    parser.Aggregate(things, (thing, visitorFunction) => visitorFunction(thing));
于 2021-12-30T21:57:59.597 回答