Search code examples
superpower

Superpower Parser: Deal with partial match of a sub parser in a combinator?


So I have written a parser for a proprietary file type. I am 95% there, but my parser is failing on the last line of the file which is a #. This is a partial match for several other parsers. It looks like it tries to parse the line as a PropertyList and fails which causes the entire parser to fail.

What can I do to solve this?

mcve is as follow, Fiddle is here https://dotnetfiddle.net/f30sN9

using System;
using Superpower;
using Superpower.Model;
using Superpower.Parsers;
using System.Collections.Generic;
                    
public class Program
{
    public static void Main()
    {
        var result1 = TagTXTParser.firstLine.TryParse(sampleFirstLine);
        Console.WriteLine(result1);
        var result2 = TagTXTParser.propertyList.TryParse(samplePropertyList);
        Console.WriteLine(result2);
        var result8 = TagTXTParser.record.TryParse(sampleRecord);
        Console.WriteLine(result8);
        
        var result9 = TagTXTParser.TagRecordsFileParser.TryParse(sampleFile);
        Console.WriteLine(result9);
    }
    
    public static string sampleFirstLine = "// 895.34 - Tags\n";
    public static string sampleSectionHeading = "#Parameters\n";
    public static string sampleItemList = "<[0]>|1|0|0|0|0|0.000000|0.000000|0.000000| | | |\n";
    public static string samplePropertyList = "#4|NavDisabled|0|1| | |0|0| |\n";
    public static string sampleSection1 = "#Parameters\n<[0] >| 1 | 0 | 0 | 0 | 0 | 0.000000 | 0.000000 | 0.000000 | | | | \n";
    public static string sampleSection2 = "#Parameters\n<[0] >| 1 | 0 | 0 | 0 | 0 | 0.000000 | 0.000000 | 0.000000 | | | | \n<[1] >| 1 | 0 | 0 | 0 | 0 | 0.000000 | 0.000000 | 0.000000 | | | | \n";
    public static string sampleSection3 = "#Parameters\n";
    public static string sampleRecord = "#3|ScreenNumber|0|1| | |1|0| |\n#Parameters\n<[0] >| 1 | 0 | 0 | 0 | 0 | 0.000000 | 0.000000 | 0.000000 | | | |\n#Alarm\n#History\n#ItemParameters\n<[0] >| 1 | 1 | \n";
    public static string sampleFile = 
@"// 8.10 - Application Tags
#1|SelectedWindowOnNavBar|0|1| | |1|0| |
#Parameters
<[0]>|1|0|0|0|0|0.000000|0.000000|0.000000| | | |
#Alarm
#History
#ItemParameters
<[0]>|1|1|
#2|ScreenName|0|3| | |1|0| |
#Parameters
<[0]>|1|0|0|0|0|0.000000|0.000000|0.000000| | | |
#Alarm
#History
#ItemParameters
<[0]>|1|1|
#3|ScreenNumber|0|1| | |1|0| |
#Parameters
<[0]>|1|0|0|0|0|0.000000|0.000000|0.000000| | | |
#Alarm
#History
#ItemParameters
<[0]>|1|1|
#4|NavDisabled|0|1| | |0|0| |
#Parameters
<[0]>|1|0|0|0|0|0.000000|0.000000|0.000000| | | |
#Alarm
#History
#ItemParameters
<[0]>|1|1|
#5|PLC_1_IPAddress|0|3| | |1|0| |
#Parameters
<[0]>|1|1|0|0|0|0.000000|0.000000|0.000000| | | |
#Alarm
#History
#ItemParameters
<[0]>|1|1|
#6|NumOfMeters|0|3| | |1|0| |
#Parameters
<[0]>|1|1|0|0|0|0.000000|0.000000|0.000000| | | |
#Alarm
#History
#ItemParameters
<[0]>|1|1|
#7|ComputerName|0|3| | |1|0| |
#Parameters
<[0]>|1|0|0|0|0|0.000000|0.000000|0.000000| | | |
#Alarm
#History
#ItemParameters
<[0]>|1|1|
#8|TZPosition|0|1| | |0|0| |
#Parameters
<[0]>|1|0|0|0|0|0.000000|0.000000|0.000000| | | |
#Alarm
#History
#ItemParameters
<[0]>|1|1|
#9|NewTime|0|3| | |1|0| |
#Parameters
<[0]>|1|0|0|0|0|0.000000|0.000000|0.000000| | | |
#Alarm
#History
#ItemParameters
<[0]>|1|1|
#";
        
}

public class TagTXTParser
{
    public record TagProperies(int Id, string Name, List<string> others);
    public record TagRecordSection(List<string>[] items);

    public class TagRecord
    {
        public int Id { get; set; } //** first element of First line
        public string Name { get; set; }



        public List<string> Properties { get; set; }
        public TagRecordSection Parameters { get; set; }
        public TagRecordSection Alarm { get; set; }
        public TagRecordSection History { get; set; }
        public TagRecordSection ItemParameters { get; set; }
    }

    public static TextParser<char> hash = Character.EqualTo('#');
    public static TextParser<TextSpan> eol = Span.EqualTo('\n').Or(Span.EqualTo("\r\n"));
    public static TextParser<char> pipe = Character.EqualTo('|');

    public static TextParser<string> text = Character.ExceptIn('#', '|', '\n', '\r').Many().Select(x => new string(x));

    public static TextParser<string> firstLine = from _ in Span.EqualTo("//")
        from rest in text
        from end in eol
        select new string(rest.ToCharArray());

    public static TextParser<char[]> heading =
        from h in hash
        from name in Character.Letter.AtLeastOnce()
        from end in eol
        select name;

    public static TextParser<List<string>> itemList = //from _ in eol
        from items in text.ManyDelimitedBy<string, char>(pipe)
        from end in eol
        select new List<string>(items);



    public static TextParser<TagProperies> propertyList =
        from _ in hash
        from id in Character.Digit.AtLeastOnce()
        from endOfId in pipe
        from name in text
        from endOfName in pipe
        from otherItems in itemList
        select new TagProperies(Convert.ToInt32(new string(id)), name, otherItems);


    public static TextParser<TagRecordSection> section =
        from _ in heading
        from items in itemList.Many()
        select new TagRecordSection(items);


    public static TextParser<TagRecord> record =
        from props in propertyList
        from sections in section.Repeat(4)
        select new TagRecord()
    {
        Id = props.Id,
        Name = props.Name,
        Properties = props.others,
        Parameters = sections[0],
        Alarm = sections[1],
        History = sections[2],
        ItemParameters = sections[3]
    };

    public static TextParser<TagRecord[]> TagRecordsFileParser = 
        from _ in firstLine
        from records in record.Many()
        from end in hash
        select records;
}

Solution

  • You'll need add a call to .Try() in your TagRecordsFileParser:

    public static TextParser<TagRecord[]> TagRecordsFileParser =
        from _ in firstLine
        from records in record.Try().Many() // <--- HERE
        from end in hash
        select records;
    

    The reason is because when you just call record.Many(), it goes through your 9 records and then sees another hash so it thinks it's another record. The parser "consumes" the hash but it turns out it's not a record at all, which causes it to fail. Calling record.Try().Many() tells Superpower to try and parse as many record's as possible, but if one then fails, don't fail the entire parser, just backtrack whatever was already consumed and then proceed with the rest of the parser definition.

    The other thing you have to do is add a call to .AtEnd() when parsing your input:

    var result9 = TagTXTParser.TagRecordsFileParser.AtEnd().TryParse(sampleFile);
    

    This way the parser will only succeed if it successfully parses your entire input. This will make it so that your input always has to end with a single # character. Otherwise without this, the parser would succeed if the file ended with something like #x which I don't think you'd want.