Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for structured references #38

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
20 changes: 20 additions & 0 deletions grammar/dependency/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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.
Expand Down
14 changes: 14 additions & 0 deletions grammar/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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.
Expand Down
17 changes: 17 additions & 0 deletions grammar/lexing.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -214,6 +228,9 @@ const allTokens = [
FormulaErrorT,
RefError,
Sheet,
TableName,
ColumnName,
SpecialItem,
Cell,
Boolean,
Column,
Expand Down
53 changes: 52 additions & 1 deletion grammar/parsing.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const lexer = require('./lexing');
const {EmbeddedActionsParser} = require("chevrotain");
const tokenVocabulary = lexer.tokenVocabulary;
const {
At,
String,
SheetQuoted,
ExcelRefFunction,
Expand All @@ -15,6 +16,9 @@ const {
Number,
Boolean,
Column,
TableName,
ColumnName,
SpecialItem,

// At,
Comma,
Expand Down Expand Up @@ -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, "'")},
Expand Down
70 changes: 70 additions & 0 deletions test/structuredreferences/structuredreferences.js
Original file line number Diff line number Diff line change
@@ -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);
});

});
1 change: 1 addition & 0 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,4 @@ require('./grammar/errors');
require('./grammar/collection');
require('./grammar/depParser');
require('./formulas');
require('./structuredreferences/structuredreferences')