diff --git a/lib/mergeRefTokens.ts b/lib/mergeRefTokens.ts index bd211db..49c4e25 100644 --- a/lib/mergeRefTokens.ts +++ b/lib/mergeRefTokens.ts @@ -193,6 +193,11 @@ const matcher = ( return best; }; +function isBangAfter (tokenlist: Token[], i: number): boolean { + const next = tokenlist[i + 1]; + return !!next && next.type === OPERATOR && next.value === '!'; +} + function commonMergeRefTokens (tokenlist: Token[], xlsx: boolean): Token[] { const finalTokens = []; // this seeks backwards because it's really the range part @@ -217,6 +222,11 @@ function commonMergeRefTokens (tokenlist: Token[], xlsx: boolean): Token[] { i -= valid - 1; } } + // A quoted scope with no `!` after it is a name, not a scope. One that has its `!` stays a + // scope, merged or not. + if (token.type === CONTEXT_QUOTE && !isBangAfter(tokenlist, i)) { + token = { ...token, type: REF_NAMED }; + } finalTokens[finalTokens.length] = token; } return finalTokens.reverse(); diff --git a/lib/parse.spec.ts b/lib/parse.spec.ts index d9deaa6..d0b335c 100644 --- a/lib/parse.spec.ts +++ b/lib/parse.spec.ts @@ -137,6 +137,28 @@ describe('parser', () => { isParsed("'A1:B2'!C3", { type: 'ReferenceIdentifier', value: "'A1:B2'!C3", kind: 'range' }); }); + // Excel reads both as the range operator over the quoted name, and stores them with the + // second name quoted: `'Alpha:Beta':'Gamma'!A1` and `Alpha:'[1]Gamma'!A1`. In the second it + // is the workbook bracket that rules out a sheet range, not the quote, which Excel drops. + test('a quoted scope with no bang after it is a name operand', () => { + isParsed("'Alpha':[Book.xlsx]Gamma!A1", { + type: 'BinaryExpression', + operator: ':', + arguments: [ + { type: 'ReferenceIdentifier', value: "'Alpha'", kind: 'name' }, + { type: 'ReferenceIdentifier', value: '[Book.xlsx]Gamma!A1', kind: 'range' } + ] + }); + isParsed("'Alpha:Beta':Gamma!A1", { + type: 'BinaryExpression', + operator: ':', + arguments: [ + { type: 'ReferenceIdentifier', value: "'Alpha:Beta'", kind: 'name' }, + { type: 'ReferenceIdentifier', value: 'Gamma!A1', kind: 'range' } + ] + }); + }); + test('"$" is not allowed on an unquoted sheet name', () => { isInvalidExpr('=SUM($Jan:$Mar!A1)'); isParsed("'$Jan:$Mar'!A1", { type: 'ReferenceIdentifier', value: "'$Jan:$Mar'!A1", kind: 'range' }); diff --git a/lib/tokenize-3d.spec.ts b/lib/tokenize-3d.spec.ts index 51bbdf6..8b03bd0 100644 --- a/lib/tokenize-3d.spec.ts +++ b/lib/tokenize-3d.spec.ts @@ -132,7 +132,7 @@ describe('lexer: 3d ranges', () => { { type: REF_RANGE, value: 'A1' } ]); expect(tokenize("'Alpha:Beta':Gamma!A1")).toEqual([ - { type: CONTEXT_QUOTE, value: "'Alpha:Beta'" }, + { type: REF_NAMED, value: "'Alpha:Beta'" }, { type: OPERATOR, value: ':' }, { type: REF_RANGE, value: 'Gamma!A1' } ]); @@ -200,7 +200,7 @@ describe('lexer: 3d ranges', () => { expect(tokenize("Alpha:'Beta:Gamma':Delta!A1")).toEqual([ { type: REF_NAMED, value: 'Alpha' }, { type: OPERATOR, value: ':' }, - { type: CONTEXT_QUOTE, value: "'Beta:Gamma'" }, + { type: REF_NAMED, value: "'Beta:Gamma'" }, { type: OPERATOR, value: ':' }, { type: REF_RANGE, value: 'Delta!A1' } ]); @@ -317,8 +317,10 @@ describe('lexer: 3d ranges', () => { { type: OPERATOR, value: '!' }, { type: REF_RANGE, value: 'A1' } ]); + // Nothing merges the first scope, because a workbook specifier is not allowed in the + // second. What is left has no `!` after it, so it is the operand Excel reads as a name. expect(tokenize("'Alpha':[Book.xlsx]Gamma!A1")).toEqual([ - { type: CONTEXT_QUOTE, value: "'Alpha'" }, + { type: REF_NAMED, value: "'Alpha'" }, { type: OPERATOR, value: ':' }, { type: REF_RANGE, value: '[Book.xlsx]Gamma!A1' } ]); @@ -380,7 +382,7 @@ describe('lexer: 3d ranges', () => { { type: REF_RANGE, value: 'A1' } ]); expect(tokenize("'Alpha':'[Book.xlsx]Gamma'!A1")).toEqual([ - { type: CONTEXT_QUOTE, value: "'Alpha'" }, + { type: REF_NAMED, value: "'Alpha'" }, { type: OPERATOR, value: ':' }, { type: REF_RANGE, value: "'[Book.xlsx]Gamma'!A1" } ]);