Skip to content

Commit bdb6f75

Browse files
authored
Merge pull request #157 from spreadsheetlab/improve-structured-references
Improve structured references
2 parents 9a2afac + bd94569 commit bdb6f75

File tree

4 files changed

+158
-70
lines changed

4 files changed

+158
-70
lines changed

src/XLParser.Tests/FormulaAnalysisTest.cs

Lines changed: 106 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,8 @@ public void StructuredTableReferenceHeaders()
257257
Assert.AreEqual(1, references.Count);
258258
Assert.AreEqual(ReferenceType.Table, references.First().ReferenceType);
259259
Assert.AreEqual("Table1", references.First().Name);
260+
CollectionAssert.AreEqual(new[] {"#Headers"}, references.First().TableSpecifiers);
261+
CollectionAssert.AreEqual(new string[] {}, references.First().TableColumns);
260262
}
261263

262264
[TestMethod]
@@ -267,6 +269,8 @@ public void StructuredTableReferenceThisRow()
267269
Assert.AreEqual(1, references.Count);
268270
Assert.AreEqual(ReferenceType.Table, references.First().ReferenceType);
269271
Assert.AreEqual("Table1", references.First().Name);
272+
CollectionAssert.AreEqual(new[] {"#This Row"}, references.First().TableSpecifiers);
273+
CollectionAssert.AreEqual(new[] {"b"}, references.First().TableColumns);
270274
}
271275

272276
[TestMethod]
@@ -277,6 +281,8 @@ public void StructuredTableReferenceCurrentRow()
277281
Assert.AreEqual(1, references.Count);
278282
Assert.AreEqual(ReferenceType.Table, references.First().ReferenceType);
279283
Assert.AreEqual("Table1", references.First().Name);
284+
CollectionAssert.AreEqual(new[] {"@"}, references.First().TableSpecifiers);
285+
CollectionAssert.AreEqual(new[] {"Region"}, references.First().TableColumns);
280286
}
281287

282288
[TestMethod]
@@ -287,6 +293,8 @@ public void StructuredTableReferenceColumns()
287293
Assert.AreEqual(1, references.Count);
288294
Assert.AreEqual(ReferenceType.Table, references.First().ReferenceType);
289295
Assert.AreEqual("Table1", references.First().Name);
296+
CollectionAssert.AreEqual(new string[] {}, references.First().TableSpecifiers);
297+
CollectionAssert.AreEqual(new[] {"Date", "Color"}, references.First().TableColumns);
290298
}
291299

292300
[TestMethod]
@@ -297,46 +305,56 @@ public void StructuredTableReferenceRowAndColumn()
297305
Assert.AreEqual(1, references.Count);
298306
Assert.AreEqual(ReferenceType.Table, references.First().ReferenceType);
299307
Assert.AreEqual("Table1", references.First().Name);
308+
CollectionAssert.AreEqual(new[] {"#Totals"}, references.First().TableSpecifiers);
309+
CollectionAssert.AreEqual(new[] {"Qty"}, references.First().TableColumns);
300310
}
301311

302312
[TestMethod]
303313
public void StructuredTableReferenceWithEscapedCharacterPound()
304314
{
305-
List<ParserReference> references = new FormulaAnalyzer("=SUBTOTAL(109,Table1[column header with '[])").ParserReferences().ToList();
315+
List<ParserReference> references = new FormulaAnalyzer("=SUBTOTAL(109,Table1['#NumItems])").ParserReferences().ToList();
306316

307317
Assert.AreEqual(1, references.Count);
308318
Assert.AreEqual(ReferenceType.Table, references.First().ReferenceType);
309319
Assert.AreEqual("Table1", references.First().Name);
320+
CollectionAssert.AreEqual(new string[] {}, references.First().TableSpecifiers);
321+
CollectionAssert.AreEqual(new[] {"#NumItems"}, references.First().TableColumns);
310322
}
311323

312324
[TestMethod]
313-
public void StructuredTableReferenceWithEscapedCharacterBracketOpen()
325+
public void StructuredTableReferenceWithEscapedCharacterBracket()
314326
{
315-
List<ParserReference> references = new FormulaAnalyzer("COUNTA(Table1['[Header])").ParserReferences().ToList();
327+
List<ParserReference> references = new FormulaAnalyzer("COUNTA(Table1['[Header']])").ParserReferences().ToList();
316328

317329
Assert.AreEqual(1, references.Count);
318330
Assert.AreEqual(ReferenceType.Table, references.First().ReferenceType);
319331
Assert.AreEqual("Table1", references.First().Name);
332+
CollectionAssert.AreEqual(new string[] {}, references.First().TableSpecifiers);
333+
CollectionAssert.AreEqual(new[] {"[Header]"}, references.First().TableColumns);
320334
}
321335

322336
[TestMethod]
323-
public void StructuredTableReferenceWithEscapedCharacterBracketClose()
337+
public void StructuredTableReferenceWithEscapedCharacterQuote()
324338
{
325-
List<ParserReference> references = new FormulaAnalyzer("COUNTA(Table1[']Header])").ParserReferences().ToList();
339+
List<ParserReference> references = new FormulaAnalyzer("COUNTA(Table1[''Test''])").ParserReferences().ToList();
326340

327341
Assert.AreEqual(1, references.Count);
328342
Assert.AreEqual(ReferenceType.Table, references.First().ReferenceType);
329343
Assert.AreEqual("Table1", references.First().Name);
344+
CollectionAssert.AreEqual(new string[] {}, references.First().TableSpecifiers);
345+
CollectionAssert.AreEqual(new[] {"'Test'"}, references.First().TableColumns);
330346
}
331347

332348
[TestMethod]
333-
public void StructuredTableReferenceWithEscapedCharacterQuote()
349+
public void StructuredTableReferenceUnqualified()
334350
{
335-
List<ParserReference> references = new FormulaAnalyzer("COUNTA(Table1[''Header])").ParserReferences().ToList();
351+
List<ParserReference> references = new FormulaAnalyzer("=SUBTOTAL(109,[Sales])").ParserReferences().ToList();
336352

337353
Assert.AreEqual(1, references.Count);
338354
Assert.AreEqual(ReferenceType.Table, references.First().ReferenceType);
339-
Assert.AreEqual("Table1", references.First().Name);
355+
Assert.AreEqual(null, references.First().Name);
356+
CollectionAssert.AreEqual(new string[] {}, references.First().TableSpecifiers);
357+
CollectionAssert.AreEqual(new[] {"Sales"}, references.First().TableColumns);
340358
}
341359

342360
[TestMethod]
@@ -347,26 +365,101 @@ public void StructuredTableReferenceUnqualifiedWithSpaceInName()
347365
Assert.AreEqual(1, references.Count);
348366
Assert.AreEqual(ReferenceType.Table, references.First().ReferenceType);
349367
Assert.AreEqual(null, references.First().Name);
368+
CollectionAssert.AreEqual(new string[] {}, references.First().TableSpecifiers);
369+
CollectionAssert.AreEqual(new[] {"Sales Amount"}, references.First().TableColumns);
350370
}
351371

352372
[TestMethod]
353-
public void StructuredTableReferenceUnqualified()
373+
public void StructuredTableReferenceUnqualifiedWithNumbers()
354374
{
355-
List<ParserReference> references = new FormulaAnalyzer("=SUBTOTAL(109,[SalesAmount])").ParserReferences().ToList();
375+
List<ParserReference> references = new FormulaAnalyzer("=SUBTOTAL(109,[2016])").ParserReferences().ToList();
356376

357377
Assert.AreEqual(1, references.Count);
358378
Assert.AreEqual(ReferenceType.Table, references.First().ReferenceType);
359379
Assert.AreEqual(null, references.First().Name);
380+
CollectionAssert.AreEqual(new string[] {}, references.First().TableSpecifiers);
381+
CollectionAssert.AreEqual(new[] {"2016"}, references.First().TableColumns);
360382
}
361383

362384
[TestMethod]
363-
public void StructuredTableReferenceUnqualifiedWithNumbers()
385+
public void StructuredTableReferenceSpecifierAndColumns()
364386
{
365-
List<ParserReference> references = new FormulaAnalyzer("=SUBTOTAL(109,[2016])").ParserReferences().ToList();
387+
List<ParserReference> references = new FormulaAnalyzer("=DeptSales[[#All],[Sales Amount]:[Commission Amount]]").ParserReferences().ToList();
366388

367389
Assert.AreEqual(1, references.Count);
368390
Assert.AreEqual(ReferenceType.Table, references.First().ReferenceType);
369-
Assert.AreEqual(null, references.First().Name);
391+
Assert.AreEqual("DeptSales", references.First().Name);
392+
CollectionAssert.AreEqual(new[] {"#All"}, references.First().TableSpecifiers);
393+
CollectionAssert.AreEqual(new[] {"Sales Amount", "Commission Amount"}, references.First().TableColumns);
394+
}
395+
396+
[TestMethod]
397+
public void StructuredTableReferenceSpecifiersAndColumn()
398+
{
399+
List<ParserReference> references = new FormulaAnalyzer("=DeptSales[[#Headers],[#Data],[% Commission]]").ParserReferences().ToList();
400+
401+
Assert.AreEqual(1, references.Count);
402+
Assert.AreEqual(ReferenceType.Table, references.First().ReferenceType);
403+
Assert.AreEqual("DeptSales", references.First().Name);
404+
CollectionAssert.AreEqual(new[] {"#Headers", "#Data"}, references.First().TableSpecifiers);
405+
CollectionAssert.AreEqual(new[] {"% Commission"}, references.First().TableColumns);
406+
}
407+
408+
[TestMethod]
409+
public void StructuredTableReferenceMultipleRows()
410+
{
411+
List<ParserReference> references = new FormulaAnalyzer("=SUM([@Jan]:[@Feb])").ParserReferences().ToList();
412+
413+
Assert.AreEqual(2, references.Count);
414+
415+
Assert.AreEqual(ReferenceType.Table, references[0].ReferenceType);
416+
Assert.AreEqual(null, references[0].Name);
417+
CollectionAssert.AreEqual(new[] {"@"}, references[0].TableSpecifiers);
418+
CollectionAssert.AreEqual(new[] {"Jan"}, references[0].TableColumns);
419+
420+
Assert.AreEqual(ReferenceType.Table, references[1].ReferenceType);
421+
Assert.AreEqual(null, references[1].Name);
422+
CollectionAssert.AreEqual(new[] {"@"}, references[1].TableSpecifiers);
423+
CollectionAssert.AreEqual(new[] {"Feb"}, references[1].TableColumns);
424+
}
425+
426+
[TestMethod]
427+
public void StructuredTableReferenceMultipleColumns()
428+
{
429+
List<ParserReference> references = new FormulaAnalyzer("=XLOOKUP($G7,Sales[[Region]:[Region]],Sales[Mar])").ParserReferences().ToList();
430+
431+
Assert.AreEqual(3, references.Count);
432+
433+
Assert.AreEqual(ReferenceType.Cell, references[0].ReferenceType);
434+
Assert.AreEqual("$G7", references[0].MinLocation);
435+
436+
Assert.AreEqual(ReferenceType.Table, references[1].ReferenceType);
437+
Assert.AreEqual("Sales", references[1].Name);
438+
CollectionAssert.AreEqual(new string[] {}, references[1].TableSpecifiers);
439+
CollectionAssert.AreEqual(new[] {"Region", "Region"}, references[1].TableColumns);
440+
441+
Assert.AreEqual(ReferenceType.Table, references[2].ReferenceType);
442+
Assert.AreEqual("Sales", references[2].Name);
443+
CollectionAssert.AreEqual(new string[] {}, references[2].TableSpecifiers);
444+
CollectionAssert.AreEqual(new[] {"Mar"}, references[2].TableColumns);
445+
}
446+
447+
[TestMethod]
448+
public void StructuredTableReferenceMultipleHeaders()
449+
{
450+
List<ParserReference> references = new FormulaAnalyzer("=COUNTA(Sales_5[[#Headers],[Jan]]:Sales_5[[#Headers],[Mar]])").ParserReferences().ToList();
451+
452+
Assert.AreEqual(2, references.Count);
453+
454+
Assert.AreEqual(ReferenceType.Table, references[0].ReferenceType);
455+
Assert.AreEqual("Sales_5", references[0].Name);
456+
CollectionAssert.AreEqual(new[] {"#Headers"}, references[0].TableSpecifiers);
457+
CollectionAssert.AreEqual(new[] {"Jan"}, references[0].TableColumns);
458+
459+
Assert.AreEqual(ReferenceType.Table, references[1].ReferenceType);
460+
Assert.AreEqual("Sales_5", references[1].Name);
461+
CollectionAssert.AreEqual(new[] {"#Headers"}, references[1].TableSpecifiers);
462+
CollectionAssert.AreEqual(new[] {"Mar"}, references[1].TableColumns);
370463
}
371464

372465
[TestMethod]

src/XLParser/ExcelFormulaGrammar.cs

Lines changed: 35 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -136,13 +136,13 @@ public class ExcelFormulaGrammar : Grammar
136136
{ Priority = TerminalPriority.ReservedName };
137137

138138
#region Structured References
139-
private const string SRColumnRegex = @"\[@?(?:[^\[\]'#@]|(?:'['\[\]#@]))+\]";
140-
public Terminal SRColumnToken = new RegexBasedTerminal(GrammarNames.TokenSRColumn, SRColumnRegex)
141-
{ Priority = TerminalPriority.SRColumn };
139+
private const string SRSpecifierRegex = @"#(All|Data|Headers|Totals|This Row)";
140+
public Terminal SRSpecifierToken = new RegexBasedTerminal(GrammarNames.TokenSRSpecifier, SRSpecifierRegex)
141+
{ Priority = TerminalPriority.StructuredReference };
142142

143-
private const string SRKeywordRegex = @"\[#(All|Data|Headers|Totals|This Row)\]";
144-
public Terminal SRKeywordToken = new RegexBasedTerminal(GrammarNames.TokenSRKeyword, SRKeywordRegex)
145-
{ Priority = TerminalPriority.SRColumn };
143+
private const string SRColumnRegex = @"(?:[^\[\]'#@]|(?:'['\[\]#@]))+";
144+
public Terminal SRColumnToken = new RegexBasedTerminal(GrammarNames.TokenSRColumn, SRColumnRegex)
145+
{ Priority = TerminalPriority.StructuredReference };
146146
#endregion
147147

148148
#region Prefixes
@@ -233,9 +233,10 @@ public class ExcelFormulaGrammar : Grammar
233233
public NonTerminal Sheet{ get; } = new NonTerminal(GrammarNames.Sheet);
234234
public NonTerminal Start{ get; } = new NonTerminal(GrammarNames.TransientStart);
235235
public NonTerminal StructuredReference { get; } = new NonTerminal(GrammarNames.StructuredReference);
236-
public NonTerminal StructuredReferenceElement { get; } = new NonTerminal(GrammarNames.StructuredReferenceElement);
236+
public NonTerminal StructuredReferenceColumn { get; } = new NonTerminal(GrammarNames.StructuredReferenceColumn);
237237
public NonTerminal StructuredReferenceExpression { get; } = new NonTerminal(GrammarNames.StructuredReferenceExpression);
238-
public NonTerminal StructuredReferenceTable { get; } = new NonTerminal(GrammarNames.StructuredReferenceTable);
238+
public NonTerminal StructuredReferenceSpecifier { get; } = new NonTerminal(GrammarNames.StructuredReferenceSpecifier);
239+
public NonTerminal StructuredReferenceQualifier { get; } = new NonTerminal(GrammarNames.StructuredReferenceQualifier);
239240
public NonTerminal Text{ get; } = new NonTerminal(GrammarNames.Text);
240241
public NonTerminal UDFName{ get; } = new NonTerminal(GrammarNames.UDFName);
241242
public NonTerminal UDFunctionCall{ get; } = new NonTerminal(GrammarNames.UDFunctionCall);
@@ -248,7 +249,6 @@ public ExcelFormulaGrammar() : base(false)
248249

249250
#region Punctuation
250251
MarkPunctuation(OpenParen, CloseParen);
251-
MarkPunctuation(OpenSquareParen, CloseSquareParen);
252252
MarkPunctuation(OpenCurlyParen, CloseCurlyParen);
253253
#endregion
254254

@@ -403,27 +403,32 @@ public ExcelFormulaGrammar() : base(false)
403403
| RefErrorToken
404404
;
405405

406-
StructuredReferenceTable.Rule = NameToken;
406+
StructuredReferenceQualifier.Rule = NameToken;
407+
408+
StructuredReferenceSpecifier.Rule =
409+
SRSpecifierToken
410+
| OpenSquareParen + SRSpecifierToken + CloseSquareParen;
407411

408-
StructuredReferenceElement.Rule = SRColumnToken | SRKeywordToken;
412+
StructuredReferenceColumn.Rule =
413+
SRColumnToken
414+
| OpenSquareParen + SRColumnToken + CloseSquareParen;
409415

410416
StructuredReferenceExpression.Rule =
411-
StructuredReferenceElement
412-
| at + StructuredReferenceElement
413-
| StructuredReferenceElement + colon + StructuredReferenceElement
414-
| at + StructuredReferenceElement + colon + StructuredReferenceElement
415-
| StructuredReferenceElement + comma + StructuredReferenceElement
416-
| StructuredReferenceElement + comma + StructuredReferenceElement + colon + StructuredReferenceElement
417-
| StructuredReferenceElement + comma + StructuredReferenceElement + comma + StructuredReferenceElement
418-
| StructuredReferenceElement + comma + StructuredReferenceElement + comma + StructuredReferenceElement + colon + StructuredReferenceElement
417+
StructuredReferenceColumn
418+
| StructuredReferenceColumn + colon + StructuredReferenceColumn
419+
| at + StructuredReferenceColumn
420+
| at + StructuredReferenceColumn + colon + StructuredReferenceColumn
421+
| StructuredReferenceSpecifier
422+
| StructuredReferenceSpecifier + comma + StructuredReferenceColumn
423+
| StructuredReferenceSpecifier + comma + StructuredReferenceColumn + colon + StructuredReferenceColumn
424+
| StructuredReferenceSpecifier + comma + StructuredReferenceSpecifier + comma + StructuredReferenceColumn
425+
| StructuredReferenceSpecifier + comma + StructuredReferenceSpecifier + comma + StructuredReferenceColumn + colon + StructuredReferenceColumn
419426
;
420427

421428
StructuredReference.Rule =
422-
StructuredReferenceElement
423-
| OpenSquareParen + StructuredReferenceExpression + CloseSquareParen
424-
| StructuredReferenceTable + StructuredReferenceElement
425-
| StructuredReferenceTable + OpenSquareParen + CloseSquareParen
426-
| StructuredReferenceTable + OpenSquareParen + StructuredReferenceExpression + CloseSquareParen
429+
OpenSquareParen + StructuredReferenceExpression + CloseSquareParen
430+
| StructuredReferenceQualifier + OpenSquareParen + CloseSquareParen
431+
| StructuredReferenceQualifier + OpenSquareParen + StructuredReferenceExpression + CloseSquareParen
427432
;
428433
#endregion
429434

@@ -487,7 +492,7 @@ private static class TerminalPriority
487492
public const int Name = -800;
488493
public const int ReservedName = -700;
489494

490-
public const int SRColumn = -500;
495+
public const int StructuredReference = -500;
491496

492497
public const int FileName = -500;
493498
public const int FileNamePath = -800;
@@ -573,9 +578,10 @@ public static class GrammarNames
573578
public const string ReservedName = "ReservedName";
574579
public const string Sheet = "Sheet";
575580
public const string StructuredReference = "StructuredReference";
576-
public const string StructuredReferenceElement = "StructuredReferenceElement";
581+
public const string StructuredReferenceColumn = "StructuredReferenceColumn";
577582
public const string StructuredReferenceExpression = "StructuredReferenceExpression";
578-
public const string StructuredReferenceTable = "StructuredReferenceTable";
583+
public const string StructuredReferenceSpecifier = "StructuredReferenceSpecifier";
584+
public const string StructuredReferenceQualifier = "StructuredReferenceQualifier";
579585
public const string Text = "Text";
580586
public const string UDFName = "UDFName";
581587
public const string UDFunctionCall = "UDFunctionCall";
@@ -614,8 +620,8 @@ public static class GrammarNames
614620
public const string TokenSingleQuotedString = "SingleQuotedString";
615621
public const string TokenSheet = "SheetNameToken";
616622
public const string TokenSheetQuoted = "SheetNameQuotedToken";
617-
public const string TokenSRColumn = "SRColumn";
618-
public const string TokenSRKeyword = "SRKeyword";
623+
public const string TokenSRColumn = "SRColumnToken";
624+
public const string TokenSRSpecifier = "SRSpecifierToken";
619625
public const string TokenText = "TextToken";
620626
public const string TokenUDF = "UDFToken";
621627
public const string TokenUnionOperator = ",";

0 commit comments

Comments
 (0)