@borgar/fx 4.7.1 → 4.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,7 +6,7 @@ This document specifies the core AST node types that support the Excel grammar.
6
6
 
7
7
  All AST nodes are represented by `Node` objects. They may have any prototype inheritance but implement the following basic interface:
8
8
 
9
- ```
9
+ ```ts
10
10
  interface Node {
11
11
  type: string;
12
12
  loc?: Location | null;
@@ -17,7 +17,7 @@ The `type` field is a string representing the AST variant type. Each subtype of
17
17
 
18
18
  The `loc` field represents the source location information of the node. If the node contains no information about the source location, the field is `null`; otherwise it is an array consisting of a two numbers: A start offset (the position of the first character of the parsed source region) and an end offset (the position of the first character after the parsed source region):
19
19
 
20
- ```
20
+ ```ts
21
21
  interface Location extends Array<number> {
22
22
  0: number;
23
23
  1: number;
@@ -26,30 +26,31 @@ interface Location extends Array<number> {
26
26
 
27
27
  ## Identifier
28
28
 
29
- ```
29
+ ```ts
30
30
  interface Identifier extends Node {
31
31
  type: "Identifier";
32
32
  name: string;
33
33
  }
34
34
  ```
35
35
 
36
- An identifier. These only appear on `CallExpression` and will always be a static string representing the name of a function call.
36
+ An identifier. These appear on `CallExpression`, `LambdaExpression`, and `LetExpression` and will always be a static string representing the name of a function call or parameter.
37
37
 
38
38
  ## ReferenceIdentifier
39
39
 
40
- ```
40
+ ```ts
41
41
  interface ReferenceIdentifier extends Node {
42
42
  type: "ReferenceIdentifier";
43
43
  value: string;
44
+ kind: "name" | "range" | "beam" | "table";
44
45
  }
45
46
  ```
46
47
 
47
- A range identifier.
48
+ An identifier for a range or a named. The
48
49
 
49
50
 
50
51
  ## Literal
51
52
 
52
- ```
53
+ ```ts
53
54
  interface Literal extends Node {
54
55
  type: "Literal";
55
56
  raw: string;
@@ -61,7 +62,7 @@ A literal token. Captures numbers, strings, and booleans. Literal errors have th
61
62
 
62
63
  ## ErrorLiteral
63
64
 
64
- ```
65
+ ```ts
65
66
  interface ErrorLiteral extends Node {
66
67
  type: "ErrorLiteral";
67
68
  raw: string;
@@ -73,7 +74,7 @@ An Error expression.
73
74
 
74
75
  ## UnaryExpression
75
76
 
76
- ```
77
+ ```ts
77
78
  interface UnaryExpression extends Node {
78
79
  type: "UnaryExpression";
79
80
  operator: UnaryOperator;
@@ -85,7 +86,7 @@ A unary operator expression.
85
86
 
86
87
  ### UnaryOperator
87
88
 
88
- ```
89
+ ```ts
89
90
  type UnaryOperator = (
90
91
  "+" | "-" | "%" | "#" | "@"
91
92
  )
@@ -95,7 +96,7 @@ A unary operator token.
95
96
 
96
97
  ## BinaryExpression
97
98
 
98
- ```
99
+ ```ts
99
100
  interface BinaryExpression extends Node {
100
101
  type: "BinaryExpression";
101
102
  operator: BinaryOperator;
@@ -107,7 +108,7 @@ A binary operator expression.
107
108
 
108
109
  ### BinaryOperator
109
110
 
110
- ```
111
+ ```ts
111
112
  type BinaryOperator = (
112
113
  "=" | "<" | ">" | "<=" | ">=" | "<>" |
113
114
  "-" | "+" | "*" | "/" | "^" |
@@ -120,7 +121,7 @@ A binary operator token. Note that Excels union operator is whitespace so a pars
120
121
 
121
122
  ## CallExpression
122
123
 
123
- ```
124
+ ```ts
124
125
  interface CallExpression extends Node {
125
126
  type: "CallExpression";
126
127
  callee: Identifier;
@@ -132,13 +133,42 @@ A function call expression.
132
133
 
133
134
  ## ArrayExpression
134
135
 
135
- ```
136
+ ```ts
136
137
  interface ArrayExpression extends Node {
137
138
  type: "ArrayExpression";
138
- elements: Array<Array<Literal | Error | ReferenceIdentifier>>;
139
+ elements: Array<Array<ReferenceIdentifier | Literal | ErrorLiteral | CallExpression>>;
139
140
  }
140
141
  ```
141
142
 
142
143
  An array expression. Excel does not have empty or sparse arrays and restricts array elements to literals. Google Sheets allows `ReferenceIdentifier`s as elements of arrays, the fx parser as an option for this but it is off by default.
143
144
 
145
+ ## LambdaExpression
146
+
147
+ ```ts
148
+ interface LambdaExpression extends Node {
149
+ type: "LambdaExpression";
150
+ params: Array<Identifier>;
151
+ body: null | Node;
152
+ }
153
+ ```
154
+
155
+ ## LetExpression
156
+
157
+ ```ts
158
+ interface LetExpression extends Node {
159
+ type: "LetExpression";
160
+ declarations: Array<LetDeclarator>;
161
+ body: null | Node;
162
+ }
163
+ ```
164
+
165
+ ## LetDeclarator
166
+
167
+ ```ts
168
+ interface LetDeclarator extends Node {
169
+ type: "LetDeclarator";
170
+ id: Identifier;
171
+ init: null | Node;
172
+ }
173
+ ```
144
174
 
@@ -0,0 +1,96 @@
1
+ /* eslint-disable jsdoc/require-property-description */
2
+
3
+ /**
4
+ * @typedef {number[]} SourceLocation
5
+ */
6
+
7
+ /**
8
+ * @typedef {Node} Identifier
9
+ * @property {"Identifier"} type
10
+ * @property {SourceLocation} [loc]
11
+ * @property {string} name
12
+ */
13
+
14
+ /**
15
+ * @typedef {Node} ReferenceIdentifier
16
+ * @property {"ReferenceIdentifier"} type
17
+ * @property {SourceLocation} [loc]
18
+ * @property {string} value
19
+ * @property {"name" | "range" | "beam" | "table"} kind
20
+ */
21
+
22
+ /**
23
+ * @typedef {Node} Literal
24
+ * @property {"Literal"} type
25
+ * @property {SourceLocation} [loc]
26
+ * @property {string} raw
27
+ * @property {string | number | boolean} value
28
+ */
29
+
30
+ /**
31
+ * @typedef {Node} ErrorLiteral
32
+ * @property {"ErrorLiteral"} type
33
+ * @property {SourceLocation} [loc]
34
+ * @property {string} raw
35
+ * @property {string} value
36
+ */
37
+
38
+ /**
39
+ * @typedef {Node} UnaryExpression
40
+ * @property {"UnaryExpression"} type
41
+ * @property {SourceLocation} [loc]
42
+ * @property {"+" | "-" | "%" | "#" | "@"} operator
43
+ * @property {AstExpression[]} arguments
44
+ */
45
+
46
+ /**
47
+ * @typedef {Node} BinaryExpression
48
+ * @property {"BinaryExpression"} type
49
+ * @property {SourceLocation} [loc]
50
+ * @property {"=" | "<" | ">" | "<=" | ">=" | "<>" | "-" | "+" | "*" | "/" | "^" | ":" | " " | "," | "&"} operator
51
+ * @property {AstExpression[]} arguments
52
+ */
53
+
54
+ /**
55
+ * @typedef {Node} CallExpression
56
+ * @property {"CallExpression"} type
57
+ * @property {SourceLocation} [loc]
58
+ * @property {Identifier} callee
59
+ * @property {AstExpression[]} arguments
60
+ */
61
+
62
+ // FIXME: the awkward naming is because tooling fails, fix tooling :)
63
+ /**
64
+ * @typedef {Node} MatrixExpression
65
+ * @property {"ArrayExpression"} type
66
+ * @property {SourceLocation} [loc]
67
+ * @property {Array<Array<ReferenceIdentifier | Literal | ErrorLiteral | CallExpression>>} arguments
68
+ */
69
+
70
+ /**
71
+ * @typedef {Node} LambdaExpression
72
+ * @property {"LambdaExpression"} type
73
+ * @property {SourceLocation} [loc]
74
+ * @property {Identifier[]} params
75
+ * @property {null | AstExpression} body
76
+ */
77
+
78
+ /**
79
+ * @typedef {Node} LetExpression
80
+ * @property {"LetExpression"} type
81
+ * @property {SourceLocation} [loc]
82
+ * @property {LetDeclarator[]} declarations
83
+ * @property {null | AstExpression} body
84
+ */
85
+
86
+ /**
87
+ * @typedef {Node} LetDeclarator
88
+ * @property {"LetDeclarator"} type
89
+ * @property {SourceLocation} [loc]
90
+ * @property {Identifier} id
91
+ * @property {null | AstExpression} init
92
+ */
93
+
94
+ /**
95
+ * @typedef {ReferenceIdentifier | Literal | ErrorLiteral | UnaryExpression | BinaryExpression | CallExpression | MatrixExpression | LambdaExpression | LetExpression} AstExpression
96
+ */
package/lib/constants.js CHANGED
@@ -22,8 +22,11 @@ export const REFERENCE = 'ReferenceIdentifier';
22
22
  export const LITERAL = 'Literal';
23
23
  export const ERROR_LITERAL = 'ErrorLiteral';
24
24
  export const CALL = 'CallExpression';
25
+ export const LAMBDA = 'LambdaExpression';
26
+ export const LET = 'LetExpression';
25
27
  export const ARRAY = 'ArrayExpression';
26
28
  export const IDENTIFIER = 'Identifier';
29
+ export const LET_DECL = 'LetDeclarator';
27
30
 
28
31
  export const MAX_COLS = (2 ** 14) - 1; // 16383
29
32
  export const MAX_ROWS = (2 ** 20) - 1; // 1048575
package/lib/fixRanges.js CHANGED
@@ -44,6 +44,7 @@ import { REF_STRUCT } from './constants.js';
44
44
  * @param {object} [options={}] Options
45
45
  * @param {boolean} [options.addBounds=false] Fill in any undefined bounds of range objects. Top to 0, bottom to 1048575, left to 0, and right to 16383.
46
46
  * @param {boolean} [options.xlsx=false] Switches to the `[1]Sheet1!A1` or `[1]!name` prefix syntax form for external workbooks. See: [Prefixes.md](./Prefixes.md)
47
+ * @param {boolean} [options.thisRow=false] Enforces using the `[#This Row]` instead of the `@` shorthand when serializing structured ranges.
47
48
  * @returns {(string | Array<Token>)} A formula string or token list (depending on which was input)
48
49
  */
49
50
  export function fixRanges (formula, options = { addBounds: false }) {
@@ -55,7 +56,7 @@ export function fixRanges (formula, options = { addBounds: false }) {
55
56
  if (!Array.isArray(formula)) {
56
57
  throw new Error('fixRanges expects an array of tokens');
57
58
  }
58
- const { addBounds, r1c1, xlsx } = options;
59
+ const { addBounds, r1c1, xlsx, thisRow } = options;
59
60
  if (r1c1) {
60
61
  throw new Error('fixRanges does not have an R1C1 mode');
61
62
  }
@@ -68,7 +69,7 @@ export function fixRanges (formula, options = { addBounds: false }) {
68
69
  let offsetDelta = 0;
69
70
  if (token.type === REF_STRUCT) {
70
71
  const sref = parseStructRef(token.value, { xlsx });
71
- const newValue = stringifyStructRef(sref, { xlsx });
72
+ const newValue = stringifyStructRef(sref, { xlsx, thisRow });
72
73
  offsetDelta = newValue.length - token.value.length;
73
74
  token.value = newValue;
74
75
  }
package/lib/lexer.js CHANGED
@@ -38,11 +38,43 @@ const causesBinaryMinus = token => {
38
38
  );
39
39
  };
40
40
 
41
+ function fixRCNames (tokens) {
42
+ let withinCall = 0;
43
+ let parenDepth = 0;
44
+ let lastToken;
45
+ for (const token of tokens) {
46
+ if (token.type === OPERATOR) {
47
+ if (token.value === '(') {
48
+ parenDepth++;
49
+ if (lastToken.type === FUNCTION) {
50
+ const v = lastToken.value.toLowerCase();
51
+ if (v === 'lambda' || v === 'let') {
52
+ withinCall = parenDepth;
53
+ }
54
+ }
55
+ }
56
+ else if (token.value === ')') {
57
+ parenDepth--;
58
+ if (parenDepth < withinCall) {
59
+ withinCall = 0;
60
+ }
61
+ }
62
+ }
63
+ else if (withinCall && token.type === UNKNOWN && /^[rc]$/.test(token.value)) {
64
+ token.type = REF_NAMED;
65
+ }
66
+ lastToken = token;
67
+ }
68
+ return tokens;
69
+ }
70
+
41
71
  export function getTokens (fx, tokenHandlers, options = {}) {
42
72
  const opts = Object.assign({}, defaultOptions, options);
43
73
  const { withLocation, mergeRefs, negativeNumbers } = opts;
44
74
  const tokens = [];
45
75
  let pos = 0;
76
+ let letOrLambda = 0;
77
+ let unknownRC = 0;
46
78
 
47
79
  let tail0 = null; // last non-whitespace token
48
80
  let tail1 = null; // penultimate non-whitespace token
@@ -110,6 +142,19 @@ export function getTokens (fx, tokenHandlers, options = {}) {
110
142
  ...(withLocation ? { loc: [ startPos, pos ] } : {})
111
143
  };
112
144
 
145
+ // make a note if we found a let/lambda call
146
+ if (lastToken && lastToken.type === FUNCTION && tokenValue === '(') {
147
+ const lastLC = lastToken.value.toLowerCase();
148
+ if (lastLC === 'lambda' || lastLC === 'let') {
149
+ letOrLambda++;
150
+ }
151
+ }
152
+ // make a note if we found a R or C unknown
153
+ if (tokenType === UNKNOWN) {
154
+ const valLC = tokenValue.toLowerCase();
155
+ unknownRC += (valLC === 'r' || valLC === 'c') ? 1 : 0;
156
+ }
157
+
113
158
  // check for termination
114
159
  if (tokenType === STRING) {
115
160
  const l = tokenValue.length;
@@ -157,6 +202,12 @@ export function getTokens (fx, tokenHandlers, options = {}) {
157
202
  pushToken(token);
158
203
  }
159
204
 
205
+ // if we encountered both a LAMBDA/LET call, and unknown 'r' or 'c' tokens
206
+ // we'll turn the unknown tokens into names within the call.
207
+ if (unknownRC && letOrLambda) {
208
+ fixRCNames(tokens);
209
+ }
210
+
160
211
  if (mergeRefs) {
161
212
  return mergeRefTokens(tokens);
162
213
  }
package/lib/lexer.spec.js CHANGED
@@ -1462,6 +1462,18 @@ test('unknowns, named ranges and functions', t => {
1462
1462
  { type: FX_PREFIX, value: '=' },
1463
1463
  { type: REF_NAMED, value: '\\foo' }
1464
1464
  ]);
1465
+ t.isTokens('=\\fo', [
1466
+ { type: FX_PREFIX, value: '=' },
1467
+ { type: REF_NAMED, value: '\\fo' }
1468
+ ]);
1469
+ t.isTokens('=\\f', [
1470
+ { type: FX_PREFIX, value: '=' },
1471
+ { type: UNKNOWN, value: '\\f' }
1472
+ ]);
1473
+ t.isTokens('=\\', [
1474
+ { type: FX_PREFIX, value: '=' },
1475
+ { type: UNKNOWN, value: '\\' }
1476
+ ]);
1465
1477
  t.isTokens('=æði', [
1466
1478
  { type: FX_PREFIX, value: '=' },
1467
1479
  { type: REF_NAMED, value: 'æði' }
@@ -1769,3 +1781,54 @@ test('tokenize external refs syntax from XLSX files', t => {
1769
1781
 
1770
1782
  t.end();
1771
1783
  });
1784
+
1785
+ test('tokenize r and c as names within LET and LAMBDA calls', t => {
1786
+ t.isTokens('=c*(LAMBDA(r,c,r*c)+r)+r', [
1787
+ { type: FX_PREFIX, value: '=' },
1788
+ { type: UNKNOWN, value: 'c' },
1789
+ { type: OPERATOR, value: '*' },
1790
+ { type: OPERATOR, value: '(' },
1791
+ { type: FUNCTION, value: 'LAMBDA' },
1792
+ { type: OPERATOR, value: '(' },
1793
+ { type: REF_NAMED, value: 'r' },
1794
+ { type: OPERATOR, value: ',' },
1795
+ { type: REF_NAMED, value: 'c' },
1796
+ { type: OPERATOR, value: ',' },
1797
+ { type: REF_NAMED, value: 'r' },
1798
+ { type: OPERATOR, value: '*' },
1799
+ { type: REF_NAMED, value: 'c' },
1800
+ { type: OPERATOR, value: ')' },
1801
+ { type: OPERATOR, value: '+' },
1802
+ { type: UNKNOWN, value: 'r' },
1803
+ { type: OPERATOR, value: ')' },
1804
+ { type: OPERATOR, value: '+' },
1805
+ { type: UNKNOWN, value: 'r' }
1806
+ ]);
1807
+ t.isTokens('=c*(LET(r,A1,c,B2,r*c)+r)+r', [
1808
+ { type: FX_PREFIX, value: '=' },
1809
+ { type: UNKNOWN, value: 'c' },
1810
+ { type: OPERATOR, value: '*' },
1811
+ { type: OPERATOR, value: '(' },
1812
+ { type: FUNCTION, value: 'LET' },
1813
+ { type: OPERATOR, value: '(' },
1814
+ { type: REF_NAMED, value: 'r' },
1815
+ { type: OPERATOR, value: ',' },
1816
+ { type: REF_RANGE, value: 'A1' },
1817
+ { type: OPERATOR, value: ',' },
1818
+ { type: REF_NAMED, value: 'c' },
1819
+ { type: OPERATOR, value: ',' },
1820
+ { type: REF_RANGE, value: 'B2' },
1821
+ { type: OPERATOR, value: ',' },
1822
+ { type: REF_NAMED, value: 'r' },
1823
+ { type: OPERATOR, value: '*' },
1824
+ { type: REF_NAMED, value: 'c' },
1825
+ { type: OPERATOR, value: ')' },
1826
+ { type: OPERATOR, value: '+' },
1827
+ { type: UNKNOWN, value: 'r' },
1828
+ { type: OPERATOR, value: ')' },
1829
+ { type: OPERATOR, value: '+' },
1830
+ { type: UNKNOWN, value: 'r' }
1831
+ ]);
1832
+
1833
+ t.end();
1834
+ });
package/lib/lexerParts.js CHANGED
@@ -67,6 +67,11 @@ function lexNamed (str) {
67
67
  const m = re_NAMED.exec(str);
68
68
  if (m) {
69
69
  const lc = m[0].toLowerCase();
70
+ // names starting with \ must be at least 3 char long
71
+ if (lc[0] === '\\' && m[0].length < 3) {
72
+ return null;
73
+ }
74
+ // single characters R and C are forbidden as names
70
75
  if (lc === 'r' || lc === 'c') {
71
76
  return null;
72
77
  }