diff --git a/src/ClosedXML.Parser.Tests/ReferenceAreaTests.cs b/src/ClosedXML.Parser.Tests/ReferenceAreaTests.cs index 9e53463..34069fe 100644 --- a/src/ClosedXML.Parser.Tests/ReferenceAreaTests.cs +++ b/src/ClosedXML.Parser.Tests/ReferenceAreaTests.cs @@ -20,147 +20,6 @@ public void DisplayStringR1C1_displays_reference_in_R1C1_style(ReferenceArea ref Assert.Equal(reference, TokenParser.ParseReference(expectedString, false)); } - [Theory] - [MemberData(nameof(ParseA1TestCases))] - public void ParseA1_parses_cell_area_or_rowspan_or_colspan(string text, ReferenceArea expectedReference) - { - Assert.Equal(expectedReference, ReferenceArea.ParseA1(text)); - } - - [Fact] - public void ParseA1_requires_argument() - { - Assert.Throws(() => ReferenceArea.ParseA1(null!)); - } - - [Fact] - public void ParseA1_throws_on_non_references() - { - Assert.Throws(() => ReferenceArea.ParseA1("HELLO")); - } - - [Theory] - [MemberData(nameof(ParseA1TestCases))] - public void TryParseA1_parses_cell_area_or_rowspan_or_colspan(string text, ReferenceArea expectedReference) - { - var success = ReferenceArea.TryParseA1(text, out var area); - Assert.True(success); - Assert.Equal(expectedReference, area); - } - - [Fact] - public void TryParseA1_requires_argument() - { - Assert.Throws(() => ReferenceArea.TryParseA1(null!, out _)); - } - - [Fact] - public void ParseA1_returns_false_on_non_references() - { - var success = ReferenceArea.TryParseA1("HELLO", out var area); - Assert.False(success); - Assert.Equal(default, area); - } - - [Theory] - [MemberData(nameof(ParseSheetA1TestCases))] - public void TryParseSheetA1_accepts_area_or_rowspan_or_colspan_with_sheet(string text, string expectedSheet, ReferenceArea expectedArea) - { - var success = ReferenceArea.TryParseSheetA1(text, out var sheet, out var area); - Assert.True(success); - Assert.Equal(expectedSheet, sheet); - Assert.Equal(expectedArea, area); - } - - [Fact] - public void TryParseSheetA1_cant_parse_workbook_index() - { - var success = ReferenceArea.TryParseSheetA1("[1]Sheet!A1", out _, out _); - Assert.False(success); - } - - [Fact] - public void TryParseSheetA1_cant_parse_reference_without_sheet() - { - var success = ReferenceArea.TryParseSheetA1("A1", out _, out _); - Assert.False(success); - } - - [Fact] - public void TryParseSheetA1_requires_argument() - { - Assert.Throws(() => ReferenceArea.TryParseSheetA1(null!, out _, out _)); - } - - public static IEnumerable ParseSheetA1TestCases - { - get - { - yield return new object[] - { - "Sheet!$C$2", - "Sheet", - new ReferenceArea(new RowCol(Absolute, 2, Absolute, 3, A1)), - }; - yield return new object[] - { - "' ''John''s'' Shop! '!C2", - " 'John's' Shop! ", - new ReferenceArea(new RowCol(Relative, 2, Relative, 3, A1)), - }; - yield return new object[] - { - "Sheet!A1:B2", - "Sheet", - new ReferenceArea(new RowCol(Relative, 1, Relative, 1, A1), new RowCol(Relative, 2, Relative, 2, A1)), - }; - yield return new object[] - { - "'Some Sheet'!C:D", - "Some Sheet", - new ReferenceArea(new RowCol(None, 0, Relative, 3, A1), new RowCol(None, 0, Relative, 4, A1)), - }; - yield return new object[] - { - "'!!WARN'!10:$15", - "!!WARN", - new ReferenceArea(new RowCol(Relative, 10, None, 0, A1), new RowCol(Absolute, 15, None, 0, A1)), - }; - } - } - - public static IEnumerable ParseA1TestCases - { - get - { - yield return new object[] - { - "$C$2", - new ReferenceArea(new RowCol(Absolute, 2, Absolute, 3, A1)), - }; - yield return new object[] - { - "AB123", - new ReferenceArea(new RowCol(Relative, 123, Relative, 28, A1)), - }; - yield return new object[] - { - "$C$2:E7", - new ReferenceArea(new RowCol(Absolute, 2, Absolute, 3, A1), new RowCol(Relative, 7, Relative, 5, A1)), - }; - yield return new object[] - { - "$C:F", - new ReferenceArea(new RowCol(None, 0, Absolute, 3, A1), new RowCol(None, 0, Relative, 6, A1)), - }; - yield return new object[] - { - "10:$15", - new ReferenceArea(new RowCol(Relative, 10, None, 0, A1), new RowCol(Absolute, 15, None, 0, A1)), - }; - } - } - public static IEnumerable DisplayStringA1 { get diff --git a/src/ClosedXML.Parser.Tests/ReferenceParserTests.cs b/src/ClosedXML.Parser.Tests/ReferenceParserTests.cs new file mode 100644 index 0000000..740209e --- /dev/null +++ b/src/ClosedXML.Parser.Tests/ReferenceParserTests.cs @@ -0,0 +1,147 @@ +using static ClosedXML.Parser.ReferenceAxisType; + +namespace ClosedXML.Parser.Tests; + +public class ReferenceParserTests +{ + [Theory] + [MemberData(nameof(ParseA1TestCases))] + public void ParseA1_parses_cell_area_or_rowspan_or_colspan(string text, ReferenceArea expectedReference) + { + Assert.Equal(expectedReference, ReferenceParser.ParseA1(text)); + } + + [Fact] + public void ParseA1_requires_argument() + { + Assert.Throws(() => ReferenceParser.ParseA1(null!)); + } + + [Fact] + public void ParseA1_throws_on_non_references() + { + Assert.Throws(() => ReferenceParser.ParseA1("HELLO")); + } + + [Theory] + [MemberData(nameof(ParseA1TestCases))] + public void TryParseA1_parses_cell_area_or_rowspan_or_colspan(string text, ReferenceArea expectedReference) + { + var success = ReferenceParser.TryParseA1(text, out var area); + Assert.True(success); + Assert.Equal(expectedReference, area); + } + + [Fact] + public void TryParseA1_requires_argument() + { + Assert.Throws(() => ReferenceParser.TryParseA1(null!, out _)); + } + + [Fact] + public void ParseA1_returns_false_on_non_references() + { + var success = ReferenceParser.TryParseA1("HELLO", out var area); + Assert.False(success); + Assert.Equal(default, area); + } + + [Theory] + [MemberData(nameof(ParseSheetA1TestCases))] + public void TryParseSheetA1_accepts_area_or_rowspan_or_colspan_with_sheet(string text, string expectedSheet, ReferenceArea expectedArea) + { + var success = ReferenceParser.TryParseSheetA1(text, out var sheet, out var area); + Assert.True(success); + Assert.Equal(expectedSheet, sheet); + Assert.Equal(expectedArea, area); + } + + [Fact] + public void TryParseSheetA1_cant_parse_workbook_index() + { + var success = ReferenceParser.TryParseSheetA1("[1]Sheet!A1", out _, out _); + Assert.False(success); + } + + [Fact] + public void TryParseSheetA1_cant_parse_reference_without_sheet() + { + var success = ReferenceParser.TryParseSheetA1("A1", out _, out _); + Assert.False(success); + } + + [Fact] + public void TryParseSheetA1_requires_argument() + { + Assert.Throws(() => ReferenceParser.TryParseSheetA1(null!, out _, out _)); + } + + public static IEnumerable ParseSheetA1TestCases + { + get + { + yield return new object[] + { + "Sheet!$C$2", + "Sheet", + new ReferenceArea(new RowCol(Absolute, 2, Absolute, 3, A1)), + }; + yield return new object[] + { + "' ''John''s'' Shop! '!C2", + " 'John's' Shop! ", + new ReferenceArea(new RowCol(Relative, 2, Relative, 3, A1)), + }; + yield return new object[] + { + "Sheet!A1:B2", + "Sheet", + new ReferenceArea(new RowCol(Relative, 1, Relative, 1, A1), new RowCol(Relative, 2, Relative, 2, A1)), + }; + yield return new object[] + { + "'Some Sheet'!C:D", + "Some Sheet", + new ReferenceArea(new RowCol(None, 0, Relative, 3, A1), new RowCol(None, 0, Relative, 4, A1)), + }; + yield return new object[] + { + "'!!WARN'!10:$15", + "!!WARN", + new ReferenceArea(new RowCol(Relative, 10, None, 0, A1), new RowCol(Absolute, 15, None, 0, A1)), + }; + } + } + + public static IEnumerable ParseA1TestCases + { + get + { + yield return new object[] + { + "$C$2", + new ReferenceArea(new RowCol(Absolute, 2, Absolute, 3, A1)), + }; + yield return new object[] + { + "AB123", + new ReferenceArea(new RowCol(Relative, 123, Relative, 28, A1)), + }; + yield return new object[] + { + "$C$2:E7", + new ReferenceArea(new RowCol(Absolute, 2, Absolute, 3, A1), new RowCol(Relative, 7, Relative, 5, A1)), + }; + yield return new object[] + { + "$C:F", + new ReferenceArea(new RowCol(None, 0, Absolute, 3, A1), new RowCol(None, 0, Relative, 6, A1)), + }; + yield return new object[] + { + "10:$15", + new ReferenceArea(new RowCol(Relative, 10, None, 0, A1), new RowCol(Absolute, 15, None, 0, A1)), + }; + } + } +} diff --git a/src/ClosedXML.Parser/ReferenceArea.cs b/src/ClosedXML.Parser/ReferenceArea.cs index 3980970..8460d6c 100644 --- a/src/ClosedXML.Parser/ReferenceArea.cs +++ b/src/ClosedXML.Parser/ReferenceArea.cs @@ -112,115 +112,6 @@ public string GetDisplayStringR1C1() .ToString(); } - /// - /// Parses area reference in A1 form. The possibilities are - /// - /// Cell (e.g. F8). - /// Area (e.g. B2:$D7). - /// Colspan (e.g. $D:$G). - /// Rowspan (e.g. 14:$15). - /// - /// Doesn't allow any whitespaces or extra values inside. - /// - /// Text to parse. - /// Parsed area. - /// true if parsing was a success, false otherwise. - [PublicAPI] - public static bool TryParseA1(string text, out ReferenceArea area) - { - if (text is null) - throw new ArgumentNullException(); - - // a1_reference : A1_CELL - // | A1_CELL COLON A1_CELL - // | A1_SPAN_REFERENCE - var tokens = RolexLexer.GetTokensA1(text.AsSpan()); - var isValid = IsA1Reference(tokens); - if (!isValid) - { - area = default; - return false; - } - - area = TokenParser.ParseReference(text.AsSpan(), isA1: true); - return true; - } - - /// - /// Parses area reference in A1 form. The possibilities are - /// - /// Cell (e.g. F8). - /// Area (e.g. B2:$D7). - /// Colspan (e.g. $D:$G). - /// Rowspan (e.g. 14:$15). - /// - /// Doesn't allow any whitespaces or extra values inside. - /// - /// Invalid input. - [PublicAPI] - public static ReferenceArea ParseA1(string text) - { - if (!TryParseA1(text, out var area)) - throw new ParsingException($"Unable to parse '{text}'."); - - return area; - } - - /// - /// Try to parse a A1 reference that has a sheet (e.g. 'Data values'!A$1:F10). - /// If contains only reference without a sheet or anything - /// else (e.g. A1), return false. - /// - /// - /// The method doesn't accept - /// - /// Sheet names, e.g. Sheet!name. - /// External sheet references, e.g. [1]Sheet!A1. - /// Sheet errors, e.g. Sheet5!$REF!. - /// - /// - /// Text to parse. - /// Name of the sheet, unescaped (e.g. the sheetName will contain Jane's for 'Jane''s'!A1). - /// Parsed reference. - /// true if parsing was a success, false otherwise. - [PublicAPI] - public static bool TryParseSheetA1(string text, out string sheetName, out ReferenceArea area) - { - if (text is null) - throw new ArgumentNullException(nameof(text)); - - var tokens = RolexLexer.GetTokensA1(text.AsSpan()); - if (tokens.Count == 0 || - tokens[0].SymbolId != Token.SINGLE_SHEET_PREFIX) - { - sheetName = string.Empty; - area = default; - return false; - } - - var sheetPrefixToken = tokens[0]; - var sheetPrefix = text.AsSpan(sheetPrefixToken.StartIndex, sheetPrefixToken.Length); - TokenParser.ParseSingleSheetPrefix(sheetPrefix, out int? workbookIndex, out sheetName); - if (workbookIndex is not null) - { - sheetName = string.Empty; - area = default; - return false; - } - - tokens.RemoveAt(0); - if (!IsA1Reference(tokens)) - { - sheetName = string.Empty; - area = default; - return false; - } - - var referenceArea = text.AsSpan().Slice(sheetPrefixToken.Length); - area = TokenParser.ParseReference(referenceArea, isA1: true); - return true; - } - /// /// Convert A1 reference to R1C1. /// @@ -292,19 +183,4 @@ internal StringBuilder AppendR1C1(StringBuilder sb) return sb; } - - private static bool IsA1Reference(IReadOnlyList tokens) - { - var isValid = tokens.Count switch - { - 2 => tokens[0].SymbolId is Token.A1_CELL or Token.A1_SPAN_REFERENCE && - tokens[1].SymbolId == Token.EofSymbolId, - 4 => tokens[0].SymbolId == Token.A1_CELL && - tokens[1].SymbolId == Token.COLON && - tokens[2].SymbolId == Token.A1_CELL && - tokens[3].SymbolId == Token.EofSymbolId, - _ => false, - }; - return isValid; - } -} \ No newline at end of file +} diff --git a/src/ClosedXML.Parser/ReferenceParser.cs b/src/ClosedXML.Parser/ReferenceParser.cs new file mode 100644 index 0000000..2b35d59 --- /dev/null +++ b/src/ClosedXML.Parser/ReferenceParser.cs @@ -0,0 +1,136 @@ +using ClosedXML.Parser.Rolex; +using JetBrains.Annotations; +using System; +using System.Collections.Generic; + +namespace ClosedXML.Parser; + +/// +/// A utility class that parses various types of references. +/// +public static class ReferenceParser +{ + /// + /// Parses area reference in A1 form. The possibilities are + /// + /// Cell (e.g. F8). + /// Area (e.g. B2:$D7). + /// Colspan (e.g. $D:$G). + /// Rowspan (e.g. 14:$15). + /// + /// Doesn't allow any whitespaces or extra values inside. + /// + /// Text to parse. + /// Parsed area. + /// true if parsing was a success, false otherwise. + [PublicAPI] + public static bool TryParseA1(string text, out ReferenceArea area) + { + if (text is null) + throw new ArgumentNullException(); + + // a1_reference : A1_CELL + // | A1_CELL COLON A1_CELL + // | A1_SPAN_REFERENCE + var tokens = RolexLexer.GetTokensA1(text.AsSpan()); + var isValid = IsA1Reference(tokens); + if (!isValid) + { + area = default; + return false; + } + + area = TokenParser.ParseReference(text.AsSpan(), isA1: true); + return true; + } + + /// + /// Parses area reference in A1 form. The possibilities are + /// + /// Cell (e.g. F8). + /// Area (e.g. B2:$D7). + /// Colspan (e.g. $D:$G). + /// Rowspan (e.g. 14:$15). + /// + /// Doesn't allow any whitespaces or extra values inside. + /// + /// Invalid input. + [PublicAPI] + public static ReferenceArea ParseA1(string text) + { + if (!TryParseA1(text, out var area)) + throw new ParsingException($"Unable to parse '{text}'."); + + return area; + } + + /// + /// Try to parse a A1 reference that has a sheet (e.g. 'Data values'!A$1:F10). + /// If contains only reference without a sheet or anything + /// else (e.g. A1), return false. + /// + /// + /// The method doesn't accept + /// + /// Sheet names, e.g. Sheet!name. + /// External sheet references, e.g. [1]Sheet!A1. + /// Sheet errors, e.g. Sheet5!$REF!. + /// + /// + /// Text to parse. + /// Name of the sheet, unescaped (e.g. the sheetName will contain Jane's for 'Jane''s'!A1). + /// Parsed reference. + /// true if parsing was a success, false otherwise. + [PublicAPI] + public static bool TryParseSheetA1(string text, out string sheetName, out ReferenceArea area) + { + if (text is null) + throw new ArgumentNullException(nameof(text)); + + var tokens = RolexLexer.GetTokensA1(text.AsSpan()); + if (tokens.Count == 0 || + tokens[0].SymbolId != Token.SINGLE_SHEET_PREFIX) + { + sheetName = string.Empty; + area = default; + return false; + } + + var sheetPrefixToken = tokens[0]; + var sheetPrefix = text.AsSpan(sheetPrefixToken.StartIndex, sheetPrefixToken.Length); + TokenParser.ParseSingleSheetPrefix(sheetPrefix, out int? workbookIndex, out sheetName); + if (workbookIndex is not null) + { + sheetName = string.Empty; + area = default; + return false; + } + + tokens.RemoveAt(0); + if (!IsA1Reference(tokens)) + { + sheetName = string.Empty; + area = default; + return false; + } + + var referenceArea = text.AsSpan().Slice(sheetPrefixToken.Length); + area = TokenParser.ParseReference(referenceArea, isA1: true); + return true; + } + + private static bool IsA1Reference(IReadOnlyList tokens) + { + var isValid = tokens.Count switch + { + 2 => tokens[0].SymbolId is Token.A1_CELL or Token.A1_SPAN_REFERENCE && + tokens[1].SymbolId == Token.EofSymbolId, + 4 => tokens[0].SymbolId == Token.A1_CELL && + tokens[1].SymbolId == Token.COLON && + tokens[2].SymbolId == Token.A1_CELL && + tokens[3].SymbolId == Token.EofSymbolId, + _ => false, + }; + return isValid; + } +}