From fe03c7e42bc04980add2aecb9fbe7a1af219f6d8 Mon Sep 17 00:00:00 2001 From: jahav Date: Mon, 20 Oct 2025 02:02:15 +0200 Subject: [PATCH 1/3] Add a basic Pratt parser This initial parser can parse numbers, arithmetic operations and numbers. --- src/ClosedXML.Parser.Ast/AstNode.cs | 2 +- src/ClosedXML.Parser.Ast/BinaryNode.cs | 4 +- src/ClosedXML.Parser.Ast/ValueNode.cs | 4 +- .../Lexers/PrattParserPrecedenceTests.cs | 63 +++++++++++++++ src/ClosedXML.Parser/Pratt/BindingPower.cs | 15 ++++ src/ClosedXML.Parser/Pratt/IParselet.cs | 8 ++ src/ClosedXML.Parser/Pratt/IPrefixParselet.cs | 6 ++ src/ClosedXML.Parser/Pratt/Lexer.cs | 27 +++++-- .../Pratt/Parselets/BinaryOpParselet.cs | 30 ++++++++ .../Pratt/Parselets/GroupParselet.cs | 18 +++++ .../Pratt/Parselets/NumberParselet.cs | 34 ++++++++ src/ClosedXML.Parser/Pratt/Parser.cs | 77 +++++++++++++++++++ src/ClosedXML.Parser/Pratt/ParserFactory.cs | 25 ++++++ src/ClosedXML.Parser/Pratt/Token.cs | 10 +-- 14 files changed, 305 insertions(+), 18 deletions(-) create mode 100644 src/ClosedXML.Parser.Tests/Lexers/PrattParserPrecedenceTests.cs create mode 100644 src/ClosedXML.Parser/Pratt/BindingPower.cs create mode 100644 src/ClosedXML.Parser/Pratt/IParselet.cs create mode 100644 src/ClosedXML.Parser/Pratt/IPrefixParselet.cs create mode 100644 src/ClosedXML.Parser/Pratt/Parselets/BinaryOpParselet.cs create mode 100644 src/ClosedXML.Parser/Pratt/Parselets/GroupParselet.cs create mode 100644 src/ClosedXML.Parser/Pratt/Parselets/NumberParselet.cs create mode 100644 src/ClosedXML.Parser/Pratt/Parser.cs create mode 100644 src/ClosedXML.Parser/Pratt/ParserFactory.cs diff --git a/src/ClosedXML.Parser.Ast/AstNode.cs b/src/ClosedXML.Parser.Ast/AstNode.cs index e7ea1bc..7aad746 100644 --- a/src/ClosedXML.Parser.Ast/AstNode.cs +++ b/src/ClosedXML.Parser.Ast/AstNode.cs @@ -14,4 +14,4 @@ public abstract record AstNode public virtual bool Equals(AstNode? other) => other is not null && Children.SequenceEqual(other.Children); public override int GetHashCode() => Children.Sum(child => child.GetHashCode()); -} \ No newline at end of file +} diff --git a/src/ClosedXML.Parser.Ast/BinaryNode.cs b/src/ClosedXML.Parser.Ast/BinaryNode.cs index a8ad586..93c8834 100644 --- a/src/ClosedXML.Parser.Ast/BinaryNode.cs +++ b/src/ClosedXML.Parser.Ast/BinaryNode.cs @@ -24,8 +24,8 @@ public record BinaryNode(BinaryOperation Operation) : AstNode public BinaryNode(BinaryOperation operation, AstNode left, AstNode right) : this(operation) { - Children = new[] { left, right }; + Children = [left, right]; } public override string GetDisplayString(ReferenceStyle style) => OpNames[Operation]; -}; \ No newline at end of file +} diff --git a/src/ClosedXML.Parser.Ast/ValueNode.cs b/src/ClosedXML.Parser.Ast/ValueNode.cs index 0a52355..026820c 100644 --- a/src/ClosedXML.Parser.Ast/ValueNode.cs +++ b/src/ClosedXML.Parser.Ast/ValueNode.cs @@ -1,4 +1,4 @@ -namespace ClosedXML.Parser; +namespace ClosedXML.Parser; public record ValueNode(string Type, object Value) : AstNode { @@ -11,4 +11,4 @@ public override string GetDisplayString(ReferenceStyle style) { return Value?.ToString() ?? "BLANK"; } -}; \ No newline at end of file +} diff --git a/src/ClosedXML.Parser.Tests/Lexers/PrattParserPrecedenceTests.cs b/src/ClosedXML.Parser.Tests/Lexers/PrattParserPrecedenceTests.cs new file mode 100644 index 0000000..750d1d4 --- /dev/null +++ b/src/ClosedXML.Parser.Tests/Lexers/PrattParserPrecedenceTests.cs @@ -0,0 +1,63 @@ +using System.Diagnostics; +using ClosedXML.Parser.Pratt; + +namespace ClosedXML.Parser.Tests.Lexers; + +public class PrattParserPrecedenceTests +{ + [Theory] + [InlineData("1+2+3+4", "(((1+2)+3)+4)")] + [InlineData("1-2-3-4", "(((1-2)-3)-4)")] + [InlineData("1-2+3-4+5", "((((1-2)+3)-4)+5)")] + [InlineData("1*2*3*4", "(((1*2)*3)*4)")] + [InlineData("1/2/3/4", "(((1/2)/3)/4)")] + [InlineData("1*2/3*4/5", "((((1*2)/3)*4)/5)")] + [InlineData("2^3^4^5", "(((2^3)^4)^5)")] // Even exponential is left-associative in Excel, contrary to standard convention + public void Operations_with_same_precedence_are_left_associative(string formula, string normalizedForm) + { + AssertSameFormulas(formula, normalizedForm); + } + + [Theory] + [InlineData("1+(2+3+4)+((5+6)+7)", "((1+((2+3)+4))+((5+6)+7))")] + [InlineData("1-(2-3-4)-((5-6)-7)", "((1-((2-3)-4))-((5-6)-7))")] + [InlineData("1-(2+3-4)+((5-6)+7)", "((1-((2+3)-4))+((5-6)+7))")] + [InlineData("1*(2*3*4)*((5*6)*7)", "((1*((2*3)*4))*((5*6)*7))")] + [InlineData("1/(2/3/4)/((5/6)/7)", "((1/((2/3)/4))/((5/6)/7))")] + [InlineData("1/(2*3/4)*((5/6)*7)", "((1/((2*3)/4))*((5/6)*7))")] + [InlineData("2^(3^4)^5", "((2^(3^4))^5)")] + public void Groups_override_precedence(string formula, string normalizedForm) + { + AssertSameFormulas(formula, normalizedForm); + } + + [Theory] + [InlineData("1+2*3+4/5*6^7-8", "(((1+(2*3))+((4/5)*(6^7)))-8)")] + [InlineData("1+2-3*4+5/6^7-8*9", "((((1+2)-(3*4))+(5/(6^7)))-(8*9))")] + public void Operations_are_grouped_by_precedence(string formula, string normalizedForm) + { + AssertSameFormulas(formula, normalizedForm); + } + + private static void AssertSameFormulas(string formula, string normalizedForm) + { + var parser = ParserFactory.Create(new F()); + var root = parser.ParseFormula(formula, new Ctx()); + + Assert.Equal(normalizedForm, GetNormalizedForm(root)); + } + + private static string GetNormalizedForm(AstNode node) + { + return node switch + { + ValueNode value => value.GetDisplayString(A1), + BinaryNode binaryOp => "(" + + GetNormalizedForm(binaryOp.Children[0]) + + binaryOp.GetDisplayString(A1) + + GetNormalizedForm(binaryOp.Children[1]) + + ")", + _ => throw new UnreachableException() + }; + } +} diff --git a/src/ClosedXML.Parser/Pratt/BindingPower.cs b/src/ClosedXML.Parser/Pratt/BindingPower.cs new file mode 100644 index 0000000..a4a2765 --- /dev/null +++ b/src/ClosedXML.Parser/Pratt/BindingPower.cs @@ -0,0 +1,15 @@ +namespace ClosedXML.Parser.Pratt; + +/// +/// Values of binding power for operators in an expression. Higher number = higher binding power. +/// Precedence of operators is specified by ISO-29500:18.17.2.2. Operators that have the same +/// precedence associate left-to-right. +/// +internal static class BindingPower +{ + internal const int Addition = 3; + internal const int Subtraction = 3; + internal const int Multiplication = 4; + internal const int Division = 4; + internal const int Exponentiation = 5; +} diff --git a/src/ClosedXML.Parser/Pratt/IParselet.cs b/src/ClosedXML.Parser/Pratt/IParselet.cs new file mode 100644 index 0000000..c810996 --- /dev/null +++ b/src/ClosedXML.Parser/Pratt/IParselet.cs @@ -0,0 +1,8 @@ +namespace ClosedXML.Parser.Pratt; + +internal interface IParselet +{ + TNode Parse(TContext ctx, TNode left, Token op); + + int GetBindingPower(); +} diff --git a/src/ClosedXML.Parser/Pratt/IPrefixParselet.cs b/src/ClosedXML.Parser/Pratt/IPrefixParselet.cs new file mode 100644 index 0000000..fc60f63 --- /dev/null +++ b/src/ClosedXML.Parser/Pratt/IPrefixParselet.cs @@ -0,0 +1,6 @@ +namespace ClosedXML.Parser.Pratt; + +internal interface IPrefixParselet +{ + TNode Parse(TContext ctx, Token token); +} diff --git a/src/ClosedXML.Parser/Pratt/Lexer.cs b/src/ClosedXML.Parser/Pratt/Lexer.cs index 2122088..3ca760a 100644 --- a/src/ClosedXML.Parser/Pratt/Lexer.cs +++ b/src/ClosedXML.Parser/Pratt/Lexer.cs @@ -16,8 +16,7 @@ internal class Lexer private static readonly bool[] IsOp; private readonly Queue _queue = new(4); - private readonly string _input; - + private string _input = string.Empty; // Currently tokenized formula private int _start; // The start index of currently parsed token in Next() private int _i; // Index of current code point _c in _input private int _c; // A current code point (including astral planes) or -1 if at the EOF @@ -30,18 +29,30 @@ static Lexer() IsOp[op] = true; } - /// - /// Create a new instance of a lexer. - /// - /// Formula to tokenize. + public Lexer() + : this(string.Empty) + { + } + public Lexer(string input) { - _input = input ?? throw new ArgumentNullException(); - _i = -1; + Reset(input); } private bool IsEof => _c == EOF; + /// + /// Prepare lexer to start tokenization of the . + /// + /// Formula to tokenize. + public void Reset(string formula) + { + _input = formula ?? throw new ArgumentNullException(); + _start = -1; + _i = -1; + _c = 0; + } + public Token Consume() { if (_queue.Count == 0) diff --git a/src/ClosedXML.Parser/Pratt/Parselets/BinaryOpParselet.cs b/src/ClosedXML.Parser/Pratt/Parselets/BinaryOpParselet.cs new file mode 100644 index 0000000..3887c36 --- /dev/null +++ b/src/ClosedXML.Parser/Pratt/Parselets/BinaryOpParselet.cs @@ -0,0 +1,30 @@ +namespace ClosedXML.Parser.Pratt.Parselets; + +internal class BinaryOpParselet : IParselet +{ + private readonly IAstFactory _factory; + private readonly Parser _parser; + private readonly BinaryOperation _op; + private readonly int _bp; + + public BinaryOpParselet(IAstFactory factory, Parser parser, BinaryOperation op, int bp) + { + _factory = factory; + _parser = parser; + _op = op; + _bp = bp; + } + + public TNode Parse(TContext ctx, TNode left, Token op) + { + var right = _parser.ParseExpression(ctx, _bp); + var node = _factory.BinaryNode(ctx, op.Range, _op, left, right); // TODO: Fix binary node range + return node; + } + + public int GetBindingPower() + { + return _bp; + } +} + diff --git a/src/ClosedXML.Parser/Pratt/Parselets/GroupParselet.cs b/src/ClosedXML.Parser/Pratt/Parselets/GroupParselet.cs new file mode 100644 index 0000000..4598b3b --- /dev/null +++ b/src/ClosedXML.Parser/Pratt/Parselets/GroupParselet.cs @@ -0,0 +1,18 @@ +namespace ClosedXML.Parser.Pratt.Parselets; + +internal class GroupParselet : IPrefixParselet +{ + private readonly Parser _parser; + + public GroupParselet(Parser parser) + { + _parser = parser; + } + + public TNode Parse(TContext ctx, Token token) + { + var node = _parser.ParseExpression(ctx, 0); + _parser.Consume(TokenType.RightParen); + return node; + } +} diff --git a/src/ClosedXML.Parser/Pratt/Parselets/NumberParselet.cs b/src/ClosedXML.Parser/Pratt/Parselets/NumberParselet.cs new file mode 100644 index 0000000..da843bf --- /dev/null +++ b/src/ClosedXML.Parser/Pratt/Parselets/NumberParselet.cs @@ -0,0 +1,34 @@ +using System.Globalization; + +namespace ClosedXML.Parser.Pratt.Parselets; + +/// +/// Get a number node from a token. +/// +/// +/// double.Parse parses even NaN or , but we can never receive such text +/// from the lexer. +/// +internal class NumberParselet : IPrefixParselet +{ + private readonly IAstFactory _factory; + private readonly Parser _parser; + + public NumberParselet(IAstFactory factory, Parser parser) + { + _factory = factory; + _parser = parser; + } + + public TNode Parse(TContext ctx, Token token) + { +#if NETSTANDARD2_1 + var text = token.GetText(_parser.Input); +#else + var text = token.GetText(_parser.Input).ToString(); // NetFx has a double whammy, it's slow and gets extra memory to GC +#endif + var number = double.Parse(text, NumberStyles.AllowDecimalPoint | NumberStyles.AllowExponent, CultureInfo.InvariantCulture); + var node = _factory.NumberNode(ctx, token.Range, number); + return node; + } +} diff --git a/src/ClosedXML.Parser/Pratt/Parser.cs b/src/ClosedXML.Parser/Pratt/Parser.cs new file mode 100644 index 0000000..6a3c34c --- /dev/null +++ b/src/ClosedXML.Parser/Pratt/Parser.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; + +namespace ClosedXML.Parser.Pratt; + +/// +/// Pratt parser. +/// +internal class Parser +{ + private readonly Lexer _lexer = new(); + private readonly Dictionary> _prefixParselets = new(); + private readonly Dictionary> _parselets = new(); + + internal string Input { get; private set; } = string.Empty; + + public TNode ParseFormula(string formula, TContext ctx) + { + Input = formula; + _lexer.Reset(formula); + return ParseExpression(ctx, 0); + } + + internal TNode ParseExpression(TContext ctx, int minBp) + { + var node = Prefix(ctx); + + while (true) + { + var maybeOp = _lexer.Peek(); + if (maybeOp.Type == TokenType.Eof) + break; + + var isOp = _parselets.TryGetValue(maybeOp.Type, out var parselet); + if (!isOp) + break; + + var bp = parselet!.GetBindingPower(); + if (bp <= minBp) + break; + + var op = _lexer.Consume(); + node = parselet.Parse(ctx, node, op); + } + + return node; + } + + private TNode Prefix(TContext ctx) + { + var token = _lexer.Consume(); + + if (!_prefixParselets.TryGetValue(token.Type, out var parselet)) + throw new InvalidOperationException($"No parselet found for {token.Type}."); + + return parselet.Parse(ctx, token); + } + + internal Token Consume(TokenType expectedType) + { + var token = _lexer.Consume(); + if (token.Type != expectedType) + throw new InvalidOperationException($"Expected token of type {expectedType}, but received {token.Type}."); + + return token; + } + + internal void Register(TokenType type, IPrefixParselet parselet) + { + _prefixParselets.Add(type, parselet); + } + + internal void Register(TokenType type, IParselet parselet) + { + _parselets.Add(type, parselet); + } +} diff --git a/src/ClosedXML.Parser/Pratt/ParserFactory.cs b/src/ClosedXML.Parser/Pratt/ParserFactory.cs new file mode 100644 index 0000000..84f22cb --- /dev/null +++ b/src/ClosedXML.Parser/Pratt/ParserFactory.cs @@ -0,0 +1,25 @@ +using ClosedXML.Parser.Pratt.Parselets; + +namespace ClosedXML.Parser.Pratt; + +internal static class ParserFactory +{ + public static Parser Create( + IAstFactory factory) + { + var parser = new Parser(); + + // Register prefix parselets + parser.Register(TokenType.Number, new NumberParselet(factory, parser)); + parser.Register(TokenType.LeftParen, new GroupParselet(parser)); + + // Register operation parselets + parser.Register(TokenType.Plus, new BinaryOpParselet(factory, parser, BinaryOperation.Addition, BindingPower.Addition)); + parser.Register(TokenType.Minus, new BinaryOpParselet(factory, parser, BinaryOperation.Subtraction, BindingPower.Subtraction)); + parser.Register(TokenType.Mul, new BinaryOpParselet(factory, parser, BinaryOperation.Multiplication, BindingPower.Multiplication)); + parser.Register(TokenType.Div, new BinaryOpParselet(factory, parser, BinaryOperation.Division, BindingPower.Division)); + parser.Register(TokenType.Pow, new BinaryOpParselet(factory, parser, BinaryOperation.Power, BindingPower.Exponentiation)); + + return parser; + } +} diff --git a/src/ClosedXML.Parser/Pratt/Token.cs b/src/ClosedXML.Parser/Pratt/Token.cs index 1166cff..69103d9 100644 --- a/src/ClosedXML.Parser/Pratt/Token.cs +++ b/src/ClosedXML.Parser/Pratt/Token.cs @@ -4,18 +4,18 @@ namespace ClosedXML.Parser.Pratt; internal readonly struct Token { - private readonly SymbolRange _text; - public Token(TokenType type, int start, int end) { Type = type; - _text = new SymbolRange(start, end); + Range = new SymbolRange(start, end); } public TokenType Type { get; } + public SymbolRange Range { get; } + public ReadOnlySpan GetText(string input) { - return input.AsSpan(_text.Start, _text.Length); + return input.AsSpan(Range.Start, Range.Length); } -} \ No newline at end of file +} From a3697a4742519e5002acbaa6b9ef4a4b27e69d6b Mon Sep 17 00:00:00 2001 From: jahav Date: Mon, 20 Oct 2025 22:50:21 +0200 Subject: [PATCH 2/3] Add "parselet" to user dictionary --- src/ClosedXML.Parser.sln.DotSettings | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ClosedXML.Parser.sln.DotSettings b/src/ClosedXML.Parser.sln.DotSettings index c24fb3c..c0b03b0 100644 --- a/src/ClosedXML.Parser.sln.DotSettings +++ b/src/ClosedXML.Parser.sln.DotSettings @@ -5,6 +5,7 @@ True True True + True True True True From a56b7f3571c06afe328f123d9e3d6b2b69b7d578 Mon Sep 17 00:00:00 2001 From: jahav Date: Mon, 20 Oct 2025 23:25:38 +0200 Subject: [PATCH 3/3] Add an intermediate node structure to parsing IAstFactory needs to know the whole extend of a node, e.g. to replace it. Example: factory gets BinaryNode callback and it should replace a node with a result. The binary node might hav ebeen deeply nested. The range parameter of callbacks is used to know the whole extent of original formula that was used to create the binary node. Thus callback can combine left side, replacement for the binary node, and left side. --- src/ClosedXML.Parser/Pratt/IParselet.cs | 4 +- src/ClosedXML.Parser/Pratt/IPrefixParselet.cs | 4 +- src/ClosedXML.Parser/Pratt/Node.cs | 44 +++++++++++++++++++ .../Pratt/Parselets/BinaryOpParselet.cs | 18 +++++--- .../Pratt/Parselets/GroupParselet.cs | 12 ++--- .../Pratt/Parselets/NumberParselet.cs | 12 ++--- src/ClosedXML.Parser/Pratt/Parser.cs | 18 ++++---- src/ClosedXML.Parser/SymbolRange.cs | 14 +++++- 8 files changed, 92 insertions(+), 34 deletions(-) create mode 100644 src/ClosedXML.Parser/Pratt/Node.cs diff --git a/src/ClosedXML.Parser/Pratt/IParselet.cs b/src/ClosedXML.Parser/Pratt/IParselet.cs index c810996..74af3c5 100644 --- a/src/ClosedXML.Parser/Pratt/IParselet.cs +++ b/src/ClosedXML.Parser/Pratt/IParselet.cs @@ -1,8 +1,8 @@ namespace ClosedXML.Parser.Pratt; -internal interface IParselet +internal interface IParselet { - TNode Parse(TContext ctx, TNode left, Token op); + Node Parse(TContext ctx, Node left, Token op); int GetBindingPower(); } diff --git a/src/ClosedXML.Parser/Pratt/IPrefixParselet.cs b/src/ClosedXML.Parser/Pratt/IPrefixParselet.cs index fc60f63..95c0f3a 100644 --- a/src/ClosedXML.Parser/Pratt/IPrefixParselet.cs +++ b/src/ClosedXML.Parser/Pratt/IPrefixParselet.cs @@ -1,6 +1,6 @@ namespace ClosedXML.Parser.Pratt; -internal interface IPrefixParselet +internal interface IPrefixParselet { - TNode Parse(TContext ctx, Token token); + Node Parse(TContext ctx, Token token); } diff --git a/src/ClosedXML.Parser/Pratt/Node.cs b/src/ClosedXML.Parser/Pratt/Node.cs new file mode 100644 index 0000000..7ae92be --- /dev/null +++ b/src/ClosedXML.Parser/Pratt/Node.cs @@ -0,0 +1,44 @@ +namespace ClosedXML.Parser.Pratt; + +/// +/// An info about node used during parsing. +/// +/// The TNode type of a node from . +internal readonly struct Node +{ + public Node(T value, int start, int end) + : this(value, new SymbolRange(start, end)) + { + } + + public Node(T value, SymbolRange range) + { + Value = value; + Range = range; + } + + /// + /// Parsed value of a node, created by the . + /// + public T Value { get; } + + /// + /// A range that was used to created the node. + /// + public SymbolRange Range { get; } + + public static implicit operator T(Node node) + { + return node.Value; + } + + internal Node ExtendLeft(Token token) + { + return new Node(Value, token.Range.ExtendRight(Range)); + } + + internal Node ExtendRight(Token token) + { + return new Node(Value, Range.ExtendRight(token.Range)); + } +} diff --git a/src/ClosedXML.Parser/Pratt/Parselets/BinaryOpParselet.cs b/src/ClosedXML.Parser/Pratt/Parselets/BinaryOpParselet.cs index 3887c36..6305502 100644 --- a/src/ClosedXML.Parser/Pratt/Parselets/BinaryOpParselet.cs +++ b/src/ClosedXML.Parser/Pratt/Parselets/BinaryOpParselet.cs @@ -1,13 +1,13 @@ namespace ClosedXML.Parser.Pratt.Parselets; -internal class BinaryOpParselet : IParselet +internal class BinaryOpParselet : IParselet { - private readonly IAstFactory _factory; - private readonly Parser _parser; + private readonly IAstFactory _factory; + private readonly Parser _parser; private readonly BinaryOperation _op; private readonly int _bp; - public BinaryOpParselet(IAstFactory factory, Parser parser, BinaryOperation op, int bp) + public BinaryOpParselet(IAstFactory factory, Parser parser, BinaryOperation op, int bp) { _factory = factory; _parser = parser; @@ -15,11 +15,15 @@ public BinaryOpParselet(IAstFactory factory, Parser Parse(TContext ctx, Node left, Token op) { var right = _parser.ParseExpression(ctx, _bp); - var node = _factory.BinaryNode(ctx, op.Range, _op, left, right); // TODO: Fix binary node range - return node; + var nodeRange = left.Range + .ExtendRight(op.Range) + .ExtendRight(right.Range); + + var node = _factory.BinaryNode(ctx, nodeRange, _op, left, right); + return new Node(node, nodeRange); } public int GetBindingPower() diff --git a/src/ClosedXML.Parser/Pratt/Parselets/GroupParselet.cs b/src/ClosedXML.Parser/Pratt/Parselets/GroupParselet.cs index 4598b3b..f3c8013 100644 --- a/src/ClosedXML.Parser/Pratt/Parselets/GroupParselet.cs +++ b/src/ClosedXML.Parser/Pratt/Parselets/GroupParselet.cs @@ -1,18 +1,18 @@ namespace ClosedXML.Parser.Pratt.Parselets; -internal class GroupParselet : IPrefixParselet +internal class GroupParselet : IPrefixParselet { - private readonly Parser _parser; + private readonly Parser _parser; - public GroupParselet(Parser parser) + public GroupParselet(Parser parser) { _parser = parser; } - public TNode Parse(TContext ctx, Token token) + public Node Parse(TContext ctx, Token leftParen) { var node = _parser.ParseExpression(ctx, 0); - _parser.Consume(TokenType.RightParen); - return node; + var rightParen = _parser.Consume(TokenType.RightParen); + return node.ExtendLeft(leftParen).ExtendRight(rightParen); } } diff --git a/src/ClosedXML.Parser/Pratt/Parselets/NumberParselet.cs b/src/ClosedXML.Parser/Pratt/Parselets/NumberParselet.cs index da843bf..28a95eb 100644 --- a/src/ClosedXML.Parser/Pratt/Parselets/NumberParselet.cs +++ b/src/ClosedXML.Parser/Pratt/Parselets/NumberParselet.cs @@ -9,18 +9,18 @@ namespace ClosedXML.Parser.Pratt.Parselets; /// double.Parse parses even NaN or , but we can never receive such text /// from the lexer. /// -internal class NumberParselet : IPrefixParselet +internal class NumberParselet : IPrefixParselet { - private readonly IAstFactory _factory; - private readonly Parser _parser; + private readonly IAstFactory _factory; + private readonly Parser _parser; - public NumberParselet(IAstFactory factory, Parser parser) + public NumberParselet(IAstFactory factory, Parser parser) { _factory = factory; _parser = parser; } - public TNode Parse(TContext ctx, Token token) + public Node Parse(TContext ctx, Token token) { #if NETSTANDARD2_1 var text = token.GetText(_parser.Input); @@ -29,6 +29,6 @@ public TNode Parse(TContext ctx, Token token) #endif var number = double.Parse(text, NumberStyles.AllowDecimalPoint | NumberStyles.AllowExponent, CultureInfo.InvariantCulture); var node = _factory.NumberNode(ctx, token.Range, number); - return node; + return new Node(node, token.Range); } } diff --git a/src/ClosedXML.Parser/Pratt/Parser.cs b/src/ClosedXML.Parser/Pratt/Parser.cs index 6a3c34c..254c270 100644 --- a/src/ClosedXML.Parser/Pratt/Parser.cs +++ b/src/ClosedXML.Parser/Pratt/Parser.cs @@ -6,22 +6,22 @@ namespace ClosedXML.Parser.Pratt; /// /// Pratt parser. /// -internal class Parser +internal class Parser { private readonly Lexer _lexer = new(); - private readonly Dictionary> _prefixParselets = new(); - private readonly Dictionary> _parselets = new(); + private readonly Dictionary> _prefixParselets = new(); + private readonly Dictionary> _parselets = new(); internal string Input { get; private set; } = string.Empty; - public TNode ParseFormula(string formula, TContext ctx) + public T ParseFormula(string formula, TContext ctx) { Input = formula; _lexer.Reset(formula); - return ParseExpression(ctx, 0); + return ParseExpression(ctx, 0).Value; } - internal TNode ParseExpression(TContext ctx, int minBp) + internal Node ParseExpression(TContext ctx, int minBp) { var node = Prefix(ctx); @@ -46,7 +46,7 @@ internal TNode ParseExpression(TContext ctx, int minBp) return node; } - private TNode Prefix(TContext ctx) + private Node Prefix(TContext ctx) { var token = _lexer.Consume(); @@ -65,12 +65,12 @@ internal Token Consume(TokenType expectedType) return token; } - internal void Register(TokenType type, IPrefixParselet parselet) + internal void Register(TokenType type, IPrefixParselet parselet) { _prefixParselets.Add(type, parselet); } - internal void Register(TokenType type, IParselet parselet) + internal void Register(TokenType type, IParselet parselet) { _parselets.Add(type, parselet); } diff --git a/src/ClosedXML.Parser/SymbolRange.cs b/src/ClosedXML.Parser/SymbolRange.cs index c1b1c4e..bfe42cc 100644 --- a/src/ClosedXML.Parser/SymbolRange.cs +++ b/src/ClosedXML.Parser/SymbolRange.cs @@ -1,4 +1,6 @@ -namespace ClosedXML.Parser; +using System; + +namespace ClosedXML.Parser; /// /// A range of a symbol in formula text. @@ -37,4 +39,12 @@ public override string ToString() { return $"[{Start}:{End}]"; } -} \ No newline at end of file + + internal SymbolRange ExtendRight(SymbolRange rangeToRight) + { + if (End != rangeToRight.Start) + throw new InvalidOperationException($"The range end {End} doesn't match start of the range to the right {rangeToRight.Start}."); + + return new SymbolRange(Start, rangeToRight.End); + } +}