diff --git a/docs/parsers.md b/docs/parsers.md index 6a34347..03bc43e 100644 --- a/docs/parsers.md +++ b/docs/parsers.md @@ -387,7 +387,7 @@ Assert.Equal(12, result.Item2); ### ZeroOrOne -Makes an existing parser optional. +Makes an existing parser optional. The method can also be be post-fixed. ```c# Parser ZeroOrOne(Parser parser) @@ -397,6 +397,7 @@ Usage: ```c# var parser = ZeroOrOne(Terms.Text("hello")); +// or Terms.Text("hello").ZeroOrOne() parser.Parse("hello"); parser.Parse(""); // returns null but with a successful state ``` @@ -408,18 +409,48 @@ Result: null ``` +### Optional + +Makes an existing parser optional. Contrary to `ZeroOrOne` the result is always a list, with +either zero or one element. It is then easy to know if the parser was successful or not by +using the Linq operators `Any()` and `FirstOrDefault()`. + +```c# +static Parser> Optional(this Parser parser) +``` + +Usage: + +```c# +var parser = Terms.Text("hello").Optional(); +parser.Parse("hello"); +parser.Parse(""); // returns an empty list +parser.Parse("hello").FirstOrDefault(); +parser.Parse("").FirstOrDefault(); // returns null +``` + +Result: + +``` +["hello"] +[] +"hello" +null +``` + ### ZeroOrMany -Executes a parser as long as it's successful. The result is a list of all individual results. +Executes a parser as long as it's successful. The result is a list of all individual results. The method can also be post-fixed. ```c# -Parser> ZeroOrMany(Parser parser) +Parser ZeroOrMany(Parser parser) ``` Usage: ```c# var parser = ZeroOrMany(Terms.Text("hello")); +// or Terms.Text("hello").ZeroOrMany() parser.Parse("hello hello"); parser.Parse(""); ``` @@ -433,16 +464,17 @@ Result: ### OneOrMany -Executes a parser as long as it's successful, and is successful if at least one occurrence is found. The result is a list of all individual results. +Executes a parser as long as it's successful, and is successful if at least one occurrence is found. The result is a list of all individual results. The method can also be post-fixed. ```c# -Parser> OneOrMany(Parser parser) +Parser OneOrMany(Parser parser) ``` Usage: ```c# var parser = OneOrMany(Terms.Text("hello")); +// or Terms.Text("hello").OneOrMany() parser.Parse("hello hello"); parser.Parse(""); ``` @@ -484,7 +516,7 @@ null // success Matches all occurrences of a parser that are separated by another one. If a separator is not followed by a value, it is not consumed. ``` -Parser> Separated(Parser separator, Parser parser) +Parser Separated(Parser separator, Parser parser) ``` Usage: @@ -641,7 +673,7 @@ Convert the result of a parser. This is usually used to create custom data struc Parser Then(Func conversion) Parser Then(Func conversion) Parser Then(U value) -Parser Then() // returns default(U) +Parser Then() // Converts the result to `U` ``` Usage: diff --git a/src/Parlot/Fluent/Optional.cs b/src/Parlot/Fluent/Optional.cs new file mode 100644 index 0000000..d8e1b0f --- /dev/null +++ b/src/Parlot/Fluent/Optional.cs @@ -0,0 +1,70 @@ +using Parlot.Compilation; +using System; +using System.Collections.Generic; +using System.Linq.Expressions; + +namespace Parlot.Fluent; + +/// +/// Returns a list containing zero or one element. +/// +/// +/// This parser will always succeed. If the previous parser fails, it will return an empty list. +/// +public sealed class Optional : Parser>, ICompilable +{ + private readonly Parser _parser; + public Optional(Parser parser) + { + _parser = parser ?? throw new ArgumentNullException(nameof(parser)); + } + + public override bool Parse(ParseContext context, ref ParseResult> result) + { + context.EnterParser(this); + + var parsed = new ParseResult(); + + var success = _parser.Parse(context, ref parsed); + + result.Set(parsed.Start, parsed.End, success ? [parsed.Value] : []); + + // Optional always succeeds + context.ExitParser(this); + return true; + } + + public CompilationResult Compile(CompilationContext context) + { + var result = context.CreateCompilationResult>(true, ExpressionHelper.ArrayEmpty()); + + // T value = _defaultValue; + // + // parse1 instructions + // + // value = new OptionalResult(parser1.Success, success ? [parsed.Value] : []); + // + + var parserCompileResult = _parser.Build(context); + + var block = Expression.Block( + parserCompileResult.Variables, + Expression.Block( + Expression.Block(parserCompileResult.Body), + context.DiscardResult + ? Expression.Empty() + : Expression.IfThenElse( + parserCompileResult.Success, + Expression.Assign(result.Value, Expression.NewArrayInit(typeof(T), parserCompileResult.Value)), + Expression.Assign(result.Value, Expression.Constant(Array.Empty(), typeof(T[]))) + ) + ) + ); + + result.Body.Add(block); + + return result; + } + + public override string ToString() => $"{_parser}?"; +} diff --git a/src/Parlot/Fluent/Parser.cs b/src/Parlot/Fluent/Parser.cs index 5ea821f..b8facea 100644 --- a/src/Parlot/Fluent/Parser.cs +++ b/src/Parlot/Fluent/Parser.cs @@ -1,5 +1,6 @@ using Parlot.Rewriting; using System; +using System.Globalization; using System.Linq; namespace Parlot.Fluent; @@ -26,7 +27,7 @@ public abstract partial class Parser /// /// Builds a parser that converts the previous result. /// - public Parser Then() => new Then(this, default(U)); + public Parser Then() => new Then(this, x => (U?)Convert.ChangeType(x, typeof(U?), CultureInfo.CurrentCulture)); /// /// Builds a parser that converts the previous result when it succeeds or returns a default value if it fails. @@ -97,7 +98,7 @@ public Parser Named(string name) /// /// Builds a parser that discards the previous result and replaces it by the specified type or value. /// - [Obsolete("Use Then() instead.")] + [Obsolete("Use Then(value) instead.")] public Parser Discard(U value) => new Discard(this, value); /// diff --git a/src/Parlot/Fluent/ParserExtensions.Cardinality.cs b/src/Parlot/Fluent/ParserExtensions.Cardinality.cs new file mode 100644 index 0000000..b1f8d74 --- /dev/null +++ b/src/Parlot/Fluent/ParserExtensions.Cardinality.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; + +namespace Parlot.Fluent; + +public static partial class ParserExtensions +{ + public static Parser> OneOrMany(this Parser parser) + => new OneOrMany(parser); + + public static Parser> ZeroOrMany(this Parser parser) + => new ZeroOrMany(parser); + + public static Parser ZeroOrOne(this Parser parser, T defaultValue) + => new ZeroOrOne(parser, defaultValue); + + public static Parser ZeroOrOne(this Parser parser) + => new ZeroOrOne(parser, default!); + + public static Parser> Optional(this Parser parser) + => new Optional(parser); +} diff --git a/test/Parlot.Tests/CompileTests.cs b/test/Parlot.Tests/CompileTests.cs index 8b94a7d..86a89fc 100644 --- a/test/Parlot.Tests/CompileTests.cs +++ b/test/Parlot.Tests/CompileTests.cs @@ -1,6 +1,7 @@ using Parlot.Fluent; using System; using System.Collections.Generic; +using System.Linq; using System.Numerics; using Xunit; using static Parlot.Fluent.Parsers; @@ -222,6 +223,15 @@ public void ShouldZeroOrOne() Assert.Null(parser.Parse(" foo")); } + [Fact] + public void OptionalShouldSucceed() + { + var parser = Terms.Text("hello").Optional().Compile(); + + Assert.Equal("hello", parser.Parse(" hello world hello").FirstOrDefault()); + Assert.Null(parser.Parse(" foo").FirstOrDefault()); + } + [Fact] public void ShouldZeroOrOneWithDefault() { @@ -287,10 +297,10 @@ public void ShouldCompileCapture() Parser Plus = Literals.Char('+'); Parser Minus = Literals.Char('-'); Parser At = Literals.Char('@'); - Parser WordChar = Literals.Pattern(char.IsLetterOrDigit); - Parser> WordDotPlusMinus = OneOrMany(OneOf(WordChar.Then(x => 'w'), Dot, Plus, Minus)); - Parser> WordDotMinus = OneOrMany(OneOf(WordChar.Then(x => 'w'), Dot, Minus)); - Parser> WordMinus = OneOrMany(OneOf(WordChar.Then(x => 'w'), Minus)); + Parser WordChar = Literals.Pattern(char.IsLetterOrDigit).Then(x => x.Span[0]); + Parser> WordDotPlusMinus = OneOrMany(OneOf(WordChar, Dot, Plus, Minus)); + Parser> WordDotMinus = OneOrMany(OneOf(WordChar, Dot, Minus)); + Parser> WordMinus = OneOrMany(OneOf(WordChar, Minus)); Parser Email = Capture(WordDotPlusMinus.And(At).And(WordMinus).And(Dot).And(WordDotMinus)); string _email = "sebastien.ros@gmail.com"; @@ -409,7 +419,7 @@ public void ShouldCompileDiscard() Assert.False(Terms.Decimal().Discard(true).Compile().TryParse("abc", out _)); #pragma warning restore CS0618 // Type or member is obsolete - Assert.True(Terms.Decimal().Then().Compile().TryParse("123", out var t1) && t1 == false); + Assert.True(Terms.Decimal().Then().Compile().TryParse("123", out var t1) && t1 == 123); Assert.True(Terms.Decimal().Then(true).Compile().TryParse("123", out var t2) && t2 == true); Assert.False(Terms.Decimal().Then(true).Compile().TryParse("abc", out _)); } diff --git a/test/Parlot.Tests/FluentTests.cs b/test/Parlot.Tests/FluentTests.cs index 24bb680..5990d53 100644 --- a/test/Parlot.Tests/FluentTests.cs +++ b/test/Parlot.Tests/FluentTests.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.Linq; using System.Numerics; using Xunit; @@ -48,16 +49,25 @@ public void WhenShouldResetPositionWhenFalse() Assert.Equal(1235, result1.Item2); } + [Fact] + public void ShouldCast() + { + var parser = Literals.Integer().Then(); + + Assert.True(parser.TryParse("123", out var result1)); + Assert.Equal(123, result1); + } + [Fact] public void ShouldReturnElse() { - var parser = Literals.Integer().Then(x => x).Else(null); + var parser = Literals.Integer().Then().Else(0); Assert.True(parser.TryParse("123", out var result1)); Assert.Equal(123, result1); Assert.True(parser.TryParse(" 123", out var result2)); - Assert.Null(result2); + Assert.Equal(0, result2); } [Fact] @@ -515,10 +525,10 @@ public void ShouldParseEmails() Parser Plus = Literals.Char('+'); Parser Minus = Literals.Char('-'); Parser At = Literals.Char('@'); - Parser WordChar = Literals.Pattern(char.IsLetterOrDigit); - Parser> WordDotPlusMinus = OneOrMany(OneOf(WordChar.Then(), Dot, Plus, Minus)); - Parser> WordDotMinus = OneOrMany(OneOf(WordChar.Then(), Dot, Minus)); - Parser> WordMinus = OneOrMany(OneOf(WordChar.Then(), Minus)); + Parser WordChar = Literals.Pattern(char.IsLetterOrDigit).Then(x => x.Span[0]); + Parser> WordDotPlusMinus = OneOrMany(OneOf(WordChar, Dot, Plus, Minus)); + Parser> WordDotMinus = OneOrMany(OneOf(WordChar, Dot, Minus)); + Parser> WordMinus = OneOrMany(OneOf(WordChar, Minus)); Parser Email = Capture(WordDotPlusMinus.And(At).And(WordMinus).And(Dot).And(WordDotMinus)); string _email = "sebastien.ros@gmail.com"; @@ -579,7 +589,7 @@ public void DiscardShouldReplaceValue() Assert.False(Terms.Decimal().Discard(true).TryParse("abc", out _)); #pragma warning restore CS0618 // Type or member is obsolete - Assert.True(Terms.Decimal().Then().TryParse("123", out var t1) && t1 == false); + Assert.True(Terms.Decimal().Then().TryParse("123", out var t1) && t1 == 123); Assert.True(Terms.Decimal().Then(true).TryParse("123", out var t2) && t2 == true); Assert.False(Terms.Decimal().Then(true).TryParse("abc", out _)); } @@ -976,6 +986,15 @@ public void ShouldZeroOrOne() Assert.Null(parser.Parse(" foo")); } + [Fact] + public void OptionalShouldSucceed() + { + var parser = Terms.Text("hello").Optional(); + + Assert.Equal("hello", parser.Parse(" hello world hello").FirstOrDefault()); + Assert.Null(parser.Parse(" foo").FirstOrDefault()); + } + [Fact] public void ZeroOrOneShouldNotBeSeekable() {