Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a max argument to Many methods. #72

Open
ashscodes opened this issue Mar 16, 2022 · 4 comments
Open

Add a max argument to Many methods. #72

ashscodes opened this issue Mar 16, 2022 · 4 comments

Comments

@ashscodes
Copy link

Opening an issue as per the recommendation here

The idea would be to add a max argument to any Many method to provide an upper bound on matches.

Suggested:

OneOrMany<T>(Parser<T> parser, int max = 0) // default unlimited
ZeroOrMany<T>(Parser<T> parser, int max = 0) // default unlimited
@loudenvier
Copy link

It would also benefit Parser<List<T>> Separated<U, T>(Parser<U> separator, Parser<T> parser) if a count or even min and max parameters were allowed.

Parser<List<T>> Separated<U, T>(Parser<U> separator, Parser<T> parser, int count=0)

It would stop when 'count' entries were found.

Parser<List<T>> Separated<U, T>(Parser<U> separator, Parser<T> parser, int min=0, int max=Int32.MaxValue)

It would fail if it didn't find min separated entries, or if it found more than max separated entries...

@loudenvier
Copy link

loudenvier commented Aug 4, 2023

After inspecting the Separated parser source code I found it very simple and intuitive and then I went all the way and implemented SeparatedBy which works exactly like Separated but accepts minOccurs and maxOcurrs parameters to control minimum and maximum cardinality. I just didn't implemented compilation, since I didn't need it, but it shouldn't be too complicated. Here is the full source:

using Parlot.Rewriting;

namespace Parlot.Fluent
{
    public sealed class SeparatedBy<U, T> : Parser<List<T>>, ISeekable
    {
        private readonly Parser<U> _separator;
        private readonly Parser<T> _parser;
        private readonly int _minOccurs;
        private readonly int _maxOccurs;

        public SeparatedBy(Parser<U> separator, Parser<T> parser, int minOccurs = 0, int maxOccurs = 0) {
            _separator = separator ?? throw new ArgumentNullException(nameof(separator));
            _parser = parser ?? throw new ArgumentNullException(nameof(parser));
            if (maxOccurs > 0 && maxOccurs < minOccurs) throw new ArgumentOutOfRangeException(nameof(maxOccurs));
            if (maxOccurs == 0 && minOccurs > 0) maxOccurs = int.MaxValue;
            _minOccurs = minOccurs;
            _maxOccurs = maxOccurs;
        }

        public bool CanSeek => _parser is ISeekable seekable && seekable.CanSeek;

        public char[] ExpectedChars => _parser is ISeekable seekable ? seekable.ExpectedChars : default;

        public bool SkipWhitespace => _parser is ISeekable seekable && seekable.SkipWhitespace;

        public bool HasMax => _maxOccurs > 0;

        public bool HasMin => _minOccurs > 0;

        public override bool Parse(ParseContext context, ref ParseResult<List<T>> result) {
            context.EnterParser(this);

            List<T>? results = null;

            var start = 0;
            var end = context.Scanner.Cursor.Position;
            var resetStart = end;

            var first = true;
            var parsed = new ParseResult<T>();
            var separatorResult = new ParseResult<U>();
            var occur = 0;

            while (!HasMax || occur < _maxOccurs) {
                if (!first) {
                    if (!_separator.Parse(context, ref separatorResult)) {
                        break;
                    }
                }

                if (!_parser.Parse(context, ref parsed)) {
                    if (!first) {
                        // A separator was found, but not followed by another value.
                        // It's still succesful if there was one value parsed, but we reset the cursor to before the separator
                        context.Scanner.Cursor.ResetPosition(end);
                        break;
                    }

                    return false;
                } else {
                    end = context.Scanner.Cursor.Position;
                }

                if (first) {
                    results = new List<T>();
                    start = parsed.Start;
                    first = false;
                }

                results.Add(parsed.Value);
                occur++;
            }

            if (HasMin && results?.Count < _minOccurs) {
                context.Scanner.Cursor.ResetPosition(resetStart);
                result = new ParseResult<List<T>>(start, resetStart.Offset, null);
                return false;
            }

            result = new ParseResult<List<T>>(start, end.Offset, results);
            return true;
        }
    }
}

The same logic could be added to other "Many" parsers!

@sebastienros
Copy link
Owner

@loudenvier While adding some tests on Separated I saw that the doc was stating that no value was valid but it's not. Would you want to provide a PR to add your suggestions? Would require to have the compilation to work too, but if you start the PR without it I can help, or explain how it works, it's quite simple, I am also rediscovering how it works every time I need to touch this part ;)

@loudenvier
Copy link

@sebastienros Yes, sir! I'll submit a PR over the weekend (too much work at work right now :-)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants