From 5f281e2baa91ae4531ee7b5057a956b6e2f44feb Mon Sep 17 00:00:00 2001 From: Vinay Date: Tue, 11 May 2021 17:38:03 +0800 Subject: [PATCH] Add support for structured references --- grammar/dependency/hooks.js | 20 ++++++ grammar/hooks.js | 14 ++++ grammar/lexing.js | 17 +++++ grammar/parsing.js | 53 +++++++++++++- .../structuredreferences.js | 70 +++++++++++++++++++ test/test.js | 1 + 6 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 test/structuredreferences/structuredreferences.js diff --git a/grammar/dependency/hooks.js b/grammar/dependency/hooks.js index a2eb215a..21fe5d1d 100644 --- a/grammar/dependency/hooks.js +++ b/grammar/dependency/hooks.js @@ -20,6 +20,7 @@ class DepParser { this.utils = new Utils(this); this.onVariable = config.onVariable; + this.onStructuredReference = config.onStructuredReference this.functions = {} this.parser = new Parser(this, this.utils); @@ -67,6 +68,25 @@ class DepParser { return [[0]] } + /** + * Get references or values for a structured referte + * @param {string} tableName + * @param {string} columnName + * @param {boolean} thisRow + */ + getStructuredReference (tableName, columnName, thisRow, specialItem) { + const res = {ref: this.onStructuredReference(tableName, columnName, thisRow, specialItem, this.position.sheet, this.position)}; + if (res.ref == null) + return FormulaError.NAME; + if (FormulaHelpers.isCellRef(res)) + this.getCell(res.ref); + else { + this.getRange(res.ref); + } + return 0; + } + + /** * TODO: * Get references or values from a user defined variable. diff --git a/grammar/hooks.js b/grammar/hooks.js index 195d9a1c..3dfdd319 100644 --- a/grammar/hooks.js +++ b/grammar/hooks.js @@ -36,6 +36,7 @@ class FormulaParser { }, config); this.onVariable = config.onVariable; + this.onStructuredReference = config.onStructuredReference this.functions = Object.assign({}, DateFunctions, StatisticalFunctions, InformationFunctions, ReferenceFunctions, EngFunctions, LogicalFunctions, TextFunctions, MathFunctions, TrigFunctions, WebFunctions, config.functions, config.functionsNeedContext); @@ -96,6 +97,19 @@ class FormulaParser { return this.onRange(ref) } + /** + * Get references or values for a structured referte + * @param {string} tableName + * @param {string} columnName + * @param {boolean} thisRow + */ + getStructuredReference (tableName, columnName, thisRow, specialItem) { + const res = {ref: this.onStructuredReference(tableName, columnName, thisRow, specialItem, this.position.sheet, this.position)}; + if (res.ref == null) + return FormulaError.NAME; + return res + } + /** * TODO: * Get references or values from a user defined variable. diff --git a/grammar/lexing.js b/grammar/lexing.js index 96613aa1..56b77e3a 100644 --- a/grammar/lexing.js +++ b/grammar/lexing.js @@ -73,6 +73,20 @@ const Column = createToken({ longer_alt: Name }); +const TableName = createToken({ + name: 'TableName', + pattern: /[A-Za-z_.\d\s\u007F-\uFFFF]+\[/ +}) + +const ColumnName = createToken({ + name: 'ColumnName', + pattern: /[\[]?[@]?[\[+]?[A-Za-z_.\d\s\u007F-\uFFFF]{0,}\]+/ +}) + +const SpecialItem = createToken({ + name: 'SpecialItem', + pattern: /[\[]?(#All|#Data|#Headers|#Total|#This Row)\][,]?/ +}) /** * Symbols and operators @@ -214,6 +228,9 @@ const allTokens = [ FormulaErrorT, RefError, Sheet, + TableName, + ColumnName, + SpecialItem, Cell, Boolean, Column, diff --git a/grammar/parsing.js b/grammar/parsing.js index 6ffe3b02..b3401ba3 100644 --- a/grammar/parsing.js +++ b/grammar/parsing.js @@ -2,6 +2,7 @@ const lexer = require('./lexing'); const {EmbeddedActionsParser} = require("chevrotain"); const tokenVocabulary = lexer.tokenVocabulary; const { + At, String, SheetQuoted, ExcelRefFunction, @@ -15,6 +16,9 @@ const { Number, Boolean, Column, + TableName, + ColumnName, + SpecialItem, // At, Comma, @@ -376,9 +380,56 @@ class Parsing extends EmbeddedActionsParser { } }, // {ALT: () => $.SUBRULE($.udfFunctionCall)}, - // {ALT: () => $.SUBRULE($.structuredReference)}, + {ALT: () => $.SUBRULE($.structuredReference)}, ])); + $.RULE('structuredReference', () => { + let tableName + let columnName; + let thisRow = false + let specialItem + $.OPTION3(() => { + tableName = $.CONSUME(TableName).image.slice(0,-1) + }) + + $.OPTION(() => { + specialItem = $.CONSUME(SpecialItem).image + specialItem = specialItem.replace(/\[|\]|\,/gi, '') + }) + + columnName = $.SUBRULE($.columnNameWithRange) + + return $.ACTION(() => { + if (Array.isArray(columnName)) { + columnName = columnName.map((name) => name.replace(/\@|\[|\]/gi, '')) + } else { + thisRow = columnName.indexOf('@') !== -1 + columnName = columnName.replace(/\@|\[|\]/gi, '') + } + return context.getStructuredReference(tableName, columnName, thisRow, specialItem) + }); + }) + + $.RULE('tableColumnName', () => { + return $.CONSUME(ColumnName).image + }) + + // TODO Support range columns + $.RULE('columnNameWithRange', () => { + // e.g. 'A1:C3' or 'A1:A3:C4', can be any number of references, at lease 2 + const ref1 = $.SUBRULE($.tableColumnName); + const refs = [ref1]; + $.MANY(() => { + $.CONSUME(Colon); + refs.push($.SUBRULE2($.tableColumnName)); + }); + if (refs.length > 1) + return $.ACTION(() => $.ACTION(() => { + return refs + })); + return ref1; + }) + $.RULE('prefixName', () => $.OR([ {ALT: () => $.CONSUME(Sheet).image.slice(0, -1)}, {ALT: () => $.CONSUME(SheetQuoted).image.slice(1, -2).replace(/''/g, "'")}, diff --git a/test/structuredreferences/structuredreferences.js b/test/structuredreferences/structuredreferences.js new file mode 100644 index 00000000..abb082e6 --- /dev/null +++ b/test/structuredreferences/structuredreferences.js @@ -0,0 +1,70 @@ +const {FormulaParser} = require('../../grammar/hooks'); +const { expect } = require('chai'); + +const parser = new FormulaParser({ + onCell: (ref) => { + return 1 + }, + onRange: () => { + return [[1, 2]] + }, + onStructuredReference: (tableName, columnName, thisRow, specialItem, sheet, position) => { + if (thisRow || specialItem) { + // Single cell + return {row: 2, col: 2} + } else { + // Full column + return { + sheet: 'Sheet 1', + from: { + row: 1, + col: 1 + }, + to: { + row: 10, + col: 1 + } + } + } + } +}); + +const position = {row: 1, col: 1, sheet: 'Sheet1'}; + +describe('Structured References', function () { + it('should parse table and column reference', async () => { + let actual = await parser.parseAsync('Table Name[@COLUMN_NAME]', position, true); + expect(actual).to.eq(1); + }); + + it('thisRow will be false', async () => { + let actual = await parser.parseAsync('TABLE[COLUMN_NAME]', position, true); + expect(actual).to.deep.eq([[1,2]]); + }); + + it('can detect columns without table', async () => { + let actual = await parser.parseAsync('[@COLUMN_NAME]', position, true); + expect(actual).to.deep.eq(1); + }); + + it('can detect columns without table', async () => { + let actual = await parser.parseAsync('[@[Commission]]', position, true); + expect(actual).to.deep.eq(1); + }); + + it('can parse single @', async () => { + let actual = await parser.parseAsync('TableName[@]', position, true); + expect(actual).to.eq(1); + }); + + it('can parse headers', async () => { + let actual = await parser.parseAsync('DeptSales[[#Headers],[Region]:[Commission Amount]]', position, true); + expect(actual).to.eq(1); + }); + + it('can parse empty headers', async () => { + let actual = await parser.parseAsync('DeptSales[[#Headers]]', position, true); + expect(actual).to.eq(1); + }); + +}); diff --git a/test/test.js b/test/test.js index 00bf10bf..f46ec135 100644 --- a/test/test.js +++ b/test/test.js @@ -106,3 +106,4 @@ require('./grammar/errors'); require('./grammar/collection'); require('./grammar/depParser'); require('./formulas'); +require('./structuredreferences/structuredreferences') \ No newline at end of file