简体   繁体   中英

How can I parse lines that can appear in any order with Sprache?

I'm using Sprache to parse a section of a file that looks like this:

OneThing=Foo
AnotherThing=Bar
YetAnotherThing=Baz

All three lines are mandatory but they can appear in any order. I have parsers for the individual lines, that look like this:

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
);

And I combine them to parse the whole section, like this:

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)
);

But this only works if the lines appear in the order OneThing, AnotherThing, YetAnotherThing. How can I change this to allow the lines to appear in any order but still enforce that each line should appear once?

Any help much appreciated! Thanks

I'm quite naive with Sprache but one perhaps verbose way is to select a tuple of each option for each line then filter the tuple array in your final select. Something like:

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()
));

But it feels like there should be a better way. The above also isn't very scalable if you'd said to me that there were 20 lines that had unique parsing and could be in different orders.

I don't think you can do this with a Sprache parser alone, but it's possible in combination with some other custom logic being integrated into it.

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();
}

The idea here is that you input your expected "things" into the ExpectedThings list. Then the ThingParser is built by dynamically chaining .Or() calls on each of those items in the list using LINQ's Aggregate() function. In the select portion of the parser, it calls SelectThingValue() which is there to remove the thing that was just parsed from the list, so we know that thing has already been parsed. It also checks to make sure the thing hasn't already been parsed, and if it has, it will throw an exception. The last check it does is to see if there are any items still in ExpectedThings and if so, that means it didn't parse one of them. And since all of them are required, we throw an error here.

You can absolutely make this much more structured and dynamic depending on your actual use case, but this works based on the example in your question. Everything here is static also, but you could change that as well to allow you to have dynamic values in ExpectedThings .

There is a way but it's ugly -unfortunately. To summarize its key points:

  • You will have to create your own Parser.
  • Constructor initialization and immutability go out the window -you will see why.
  • The way to pass the values to your object is very hacky -at least to my taste.
  • You can't guarantee that each key appears once
  • It's so big and complex that you might as well say "nah let's split on each line ending, then split on = and then take it from there."

This solution is not mine -I would have never thought of something like that. It was taken from Mike Hadlow's blog . The missing parts from EasyNetQ ConnectionStringGrammar.cs -where surprisingly Mike is a contributor to this file.

First you create your property value types. Pretty much you've already done this on your first snippet but regardless here it is:

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

and for numbers

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

Then the method that will create a key-value parser

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 is just an alias: using ThingsVisitor = System.Func<Things, Things>; . Things is in leu of MyClass . CreateSetter() extracts the setter from the expression of the getter. I leave that to the reader -it's not hard if you follow the links.

Then you use the method to create your properties parser

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));

Then you define the parser for the whole input

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

And finally, you you can parse the input

    Things things = new();
    IEnumerable<ThingsVisitor> parser = PropsParser.Parse(@"OneThing=Foo
AnotherThing=32
YetAnotherThing=Baz");
    parser.Aggregate(things, (thing, visitorFunction) => visitorFunction(thing));

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM