Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/ClosedXML.Parser.Ast/SheetNameNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ public record SheetNameNode(string Sheet, string Name) : AstNode
{
public override string GetDisplayString(ReferenceStyle style)
{
return $"[{Sheet}]!{Name}";
var sheet = NameUtils.ShouldQuote(Sheet) ? '\'' + Sheet.Replace("'", "''") + '\'' : Sheet;
return $"{sheet}!{Name}";
}
}
}
108 changes: 108 additions & 0 deletions src/ClosedXML.Parser.Tests/Lexers/ParseletIdentTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
using ClosedXML.Parser.Pratt;

namespace ClosedXML.Parser.Tests.Lexers;

public class ParseletIdentTests
{
[Theory]

// Local area
[InlineData("A1:B1", typeof(ReferenceNode))]
[InlineData("$A$1:$B$1", typeof(ReferenceNode))]

// Local cell
[InlineData("A1", typeof(ReferenceNode))]
[InlineData("A$1", typeof(ReferenceNode))]
[InlineData("$A1", typeof(ReferenceNode))]
[InlineData("$A$1", typeof(ReferenceNode))]
[InlineData("XFD1048576", typeof(ReferenceNode))]
[InlineData("XFD$1048576", typeof(ReferenceNode))]
[InlineData("$XFD1048576", typeof(ReferenceNode))]
[InlineData("$XFD$1048576", typeof(ReferenceNode))]

// Local colspan
[InlineData("A:B", typeof(ReferenceNode))]
[InlineData("$GE:$XFD", typeof(ReferenceNode))]

// Local rowspan starting with absolute
[InlineData("$1:8", typeof(ReferenceNode))]
[InlineData("$72:$85", typeof(ReferenceNode))]

// sheet!A1:A2
[InlineData("Sheet!A1:B2", typeof(SheetReferenceNode))]
[InlineData("Sheet!$Z$84:$BG$99", typeof(SheetReferenceNode))]

// sheet!A1
[InlineData("Sheet!A1", typeof(SheetReferenceNode))]
[InlineData("Sheet!$Z$84", typeof(SheetReferenceNode))]

// sheet!$1:2
[InlineData("Sheet!$4:81", typeof(SheetReferenceNode))]
[InlineData("Sheet!$1:$5", typeof(SheetReferenceNode))]

// sheet!name
[InlineData("Sheet!name", typeof(SheetNameNode))]
[InlineData("Sheet!_name", typeof(SheetNameNode))]

// sheet!1:2
[InlineData("Sheet!1:2", typeof(SheetReferenceNode))]
[InlineData("Sheet!1:$2", typeof(SheetReferenceNode))]

// name
[InlineData("_name", typeof(NameNode))]
[InlineData("name", typeof(NameNode))]

// sheet1:sheet2!A1:B2
[InlineData("sheet1:sheet2!A1:B2", typeof(Reference3DNode))]
[InlineData("sheet1:sheet2!$A$1:$B$2", typeof(Reference3DNode))]

// sheet1:sheet2!A1
[InlineData("sheet1:sheet2!A1", typeof(Reference3DNode))]
[InlineData("sheet1:sheet2!$A$1", typeof(Reference3DNode))]

// sheet1:sheet2!A:B
[InlineData("sheet1:sheet2!A:C", typeof(Reference3DNode))]
[InlineData("sheet1:sheet2!$A:$C", typeof(Reference3DNode))]

// sheet1:sheet2!1:2
[InlineData("sheet1:sheet2!1:2", typeof(Reference3DNode))]
[InlineData("sheet1:sheet2!$1:$2", typeof(Reference3DNode))]
public void Can_parse_references_starting_at_ident(string formula, Type expectedNodeType)
{
var parser = ParserFactory.Create(new F());
var root = parser.ParseFormula(formula, new Ctx());

Assert.Equal(expectedNodeType, root.GetType());
Assert.Equal(formula, root.GetDisplayString(A1));
}

[Theory]
[InlineData("TRUE", true)]
[InlineData("true", true)]
[InlineData("FALSE", false)]
[InlineData("false", false)]
public void Can_parse_logical(string formula, bool expectedValue)
{
var parser = ParserFactory.Create(new F());
var root = parser.ParseFormula(formula, new Ctx());

Assert.Equal(new ValueNode(expectedValue), root);
}

[Theory]
[InlineData("sheet!$")]
[InlineData("sheet!")]
[InlineData("$")]
[InlineData("A01")]
[InlineData("A0")]
[InlineData("A1048577")]
[InlineData("XFE1")]
[InlineData("sheet1:sheet2!")]
[InlineData("sheet1:sheet2!A")]
[InlineData("sheet1:sheet2!name")] // There is no such thing as 3D name
public void Invalid_references_starting_with_ident_throw_parsing_exception(string formula)
{
var parser = ParserFactory.Create(new F());
Assert.Throws<ParsingException>(() => parser.ParseFormula(formula, new Ctx()));
}
}
21 changes: 21 additions & 0 deletions src/ClosedXML.Parser/NameUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,27 @@ public static bool IsSheetNameValid(ReadOnlySpan<char> sheetName)
return sheetName.IndexOfAny(InvalidSheetChars) == -1;
}

internal static bool IsNameValid(ReadOnlySpan<char> name)
{
if (name.Length is < 1 or > 255)
return false;

// TODO: Determine what is a valid name and make the method public.
// Alert box says:
// * Starts with a letter or underscore
// * no space or char that is not allowed
if (name[0] != '_' && !char.IsLetter(name[0]))
return false;

foreach (var nextNameChar in name.Slice(1))
{
if (!char.IsLetter(nextNameChar))
return false;
}

return true;
}

internal static StringBuilder EscapeName(StringBuilder sb, string sheet)
{
return ShouldQuote(sheet.AsSpan())
Expand Down
24 changes: 24 additions & 0 deletions src/ClosedXML.Parser/Pratt/CompatUtils.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
namespace ClosedXML.Parser.Pratt;

/// <summary>
/// Various methods that are not present in .net standard 2.0.
/// </summary>
internal static class CompatUtils
{
/// <summary>
/// Replacement for <c>char.IsAsciiLetter</c> that isn't in the netstandard 2.0
/// </summary>
public static bool IsAsciiLetter(char c)
{
return c is >= 'A' and <= 'Z' ||
c is >= 'a' and <= 'z';
}

/// <summary>
/// Replacement for <c>char.IsAsciiDigit</c> that isn't in the netstandard 2.0
/// </summary>
public static bool IsAsciiDigit(char c)
{
return c is >= '0' and <= '9';
}
}
11 changes: 8 additions & 3 deletions src/ClosedXML.Parser/Pratt/Lexer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,17 @@ public Token Consume()
return _queue.Dequeue();
}

public Token Peek()
public Token Peek(int distance = 1)
{
if (_queue.Count == 0)
// TODO: Replace BCL queue with a structure that allows index access
while (_queue.Count < distance)
_queue.Enqueue(Next());

return _queue.Peek();
var enumerator = _queue.GetEnumerator();
for (var i = 0; i < distance; ++i)
enumerator.MoveNext();

return enumerator.Current;
}

private Token Next()
Expand Down
141 changes: 141 additions & 0 deletions src/ClosedXML.Parser/Pratt/Parselets/IdentParselet.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
using System;

namespace ClosedXML.Parser.Pratt.Parselets;

internal class IdentParselet<TScalar, T, TContext> : IPrefixParselet<T, TContext>
{
private readonly IAstFactory<TScalar, T, TContext> _factory;
private readonly Parser<T, TContext> _parser;

public IdentParselet(IAstFactory<TScalar, T, TContext> factory, Parser<T, TContext> parser)
{
_factory = factory;
_parser = parser;
}

public Node<T> Parse(TContext ctx, Token token)
{
// When we receive an ident, there are following possibilities what it could be (checked
// in this order):
// * A1:B2
// * A1
// * A:B
// * $4:6 - rowspan starting with an absolute row
// * sheet!A1:A2
// * sheet!A1
// * sheet!A:B
// * sheet!$1:2
// * sheet!name
// * sheet!1:2
// * TRUE/FALSE
// * sheet1:sheet2!A1:A2
// * sheet1:sheet2!A1
// * sheet1:sheet2!A:B
// * sheet1:sheet2!$1:2
// * name

// Check for area `A1:B2` or just cell `A1`
// Check for colspan `A:B`
// Check for colspan `$1:2` with absolute row start, because this is an "ident" prefix parselet
if (_parser.TryReferenceA1(token, out var localArea, out var localAreaRange))
{
var value = _factory.Reference(ctx, localAreaRange, localArea);
return new Node<T>(value, localAreaRange);
}


if (_parser.TryGetUnquotedSheet(token, out var sheetNameSpan) && _parser.LookAhead(1).Type == TokenType.Bang)
{
// We are now in `sheet!` Parse local reference.
var sheetName = sheetNameSpan.ToString(); // String allocation, needed for the IAstFactory
var bangToken = _parser.Consume(TokenType.Bang);
var sheetWithBangRange = token.Range.ExtendRight(bangToken.Range);

// No need to check for token type, if EoF, nothing will be matched to such token
var sheetRefToken = _parser.Consume();

// Check for area `sheet!A1:B2` or just cell `sheet!A1`
// Check for colspan `sheet!A:B`
// Check for rowspan `sheet!1:2` with absolute or relative start row
if (_parser.TryReferenceA1(sheetRefToken, out var sheetArea, out var sheetAreaRange))
{
var range = sheetWithBangRange.ExtendRight(sheetAreaRange);
var value = _factory.SheetReference(ctx, range, sheetName, sheetArea);
return new Node<T>(value, range);
}

// Check for `sheet!name`
if (_parser.TryGetName(sheetRefToken, out var name))
{
var range = sheetWithBangRange.ExtendRight(sheetRefToken.Range);
var value = _factory.SheetName(ctx, range, sheetName, name.ToString()); // String allocation, needed for the IAstFactory
return new Node<T>(value, range);
}

throw new ParsingException($"Unable to parse value starting from position {token.Range.Start}.");
}

var tokenText = token.GetText(_parser.Input);
if (EqualCaseInsensitive(tokenText, "TRUE"))
{
var value = _factory.LogicalNode(ctx, token.Range, true);
return new Node<T>(value, token.Range);
}

if (EqualCaseInsensitive(tokenText, "FALSE"))
{
var value = _factory.LogicalNode(ctx, token.Range, false);
return new Node<T>(value, token.Range);
}

// Check for 3D reference for unquoted sheets:
// * Sheet1:Sheet2!A1:B2
// * Sheet1:Sheet2!A1
// * Sheet1:Sheet2!A:B
// * Sheet1:Sheet2!1:2
if (_parser.TryGetUnquotedSheet(token, out var startSheet) &&
_parser.LookAhead(1).Type == TokenType.Range &&
_parser.LookAhead(2) is { Type: TokenType.Ident } maybeEndSheetToken &&
_parser.TryGetUnquotedSheet(maybeEndSheetToken, out var endSheet) &&
_parser.LookAhead(3).Type == TokenType.Bang)
{
var sheetStartToken = token;
var rangeToken = _parser.Consume(TokenType.Range);
var sheetEndToken = _parser.Consume(TokenType.Ident);
var bangToken = _parser.Consume(TokenType.Bang);
var refToken = _parser.Consume();

if (_parser.TryReferenceA1(refToken, out var sheetRangeReference, out var sheetRangeReferenceRange))
{
var range = sheetStartToken.Range
.ExtendRight(rangeToken.Range)
.ExtendRight(sheetEndToken.Range)
.ExtendRight(bangToken.Range)
.ExtendRight(sheetRangeReferenceRange);
var startSheetString = startSheet.ToString(); // String allocation for the IAstFactory
var endSheetString = endSheet.ToString();
var value = _factory.Reference3D(ctx, range, startSheetString, endSheetString, sheetRangeReference);
return new Node<T>(value, range);
}

throw new ParsingException($"Unable to parse value starting from position {token.Range.Start}.");
}

// Check for rowspan `name`
if (_parser.TryGetName(token, out var workbookName))
{
var value = _factory.Name(ctx, token.Range, workbookName.ToString()); // String allocation, needed for the IAstFactory
return new Node<T>(value, token.Range);
}

throw new ParsingException($"Unable to parse value starting from position {token.Range.Start}.");
}

private static bool EqualCaseInsensitive(ReadOnlySpan<char> text, string other)
{
if (text.Length != other.Length)
return false;

return text.CompareTo(other.AsSpan(), StringComparison.OrdinalIgnoreCase) == 0;
}
}
Loading
Loading