grammar/parsing.js

  1. const lexer = require('./lexing');
  2. const {EmbeddedActionsParser} = require("chevrotain");
  3. const tokenVocabulary = lexer.tokenVocabulary;
  4. const {
  5. String,
  6. SheetQuoted,
  7. ExcelRefFunction,
  8. ExcelConditionalRefFunction,
  9. Function,
  10. FormulaErrorT,
  11. RefError,
  12. Cell,
  13. Sheet,
  14. Name,
  15. Number,
  16. Boolean,
  17. Column,
  18. // At,
  19. Comma,
  20. Colon,
  21. Semicolon,
  22. OpenParen,
  23. CloseParen,
  24. // OpenSquareParen,
  25. // CloseSquareParen,
  26. // ExclamationMark,
  27. OpenCurlyParen,
  28. CloseCurlyParen,
  29. MulOp,
  30. PlusOp,
  31. DivOp,
  32. MinOp,
  33. ConcatOp,
  34. ExOp,
  35. PercentOp,
  36. NeqOp,
  37. GteOp,
  38. LteOp,
  39. GtOp,
  40. EqOp,
  41. LtOp
  42. } = lexer.tokenVocabulary;
  43. class Parsing extends EmbeddedActionsParser {
  44. /**
  45. *
  46. * @param {FormulaParser|DepParser} context
  47. * @param {Utils} utils
  48. */
  49. constructor(context, utils) {
  50. super(tokenVocabulary, {
  51. outputCst: false,
  52. maxLookahead: 1,
  53. skipValidations: true,
  54. // traceInitPerf: true,
  55. });
  56. this.utils = utils;
  57. this.binaryOperatorsPrecedence = [
  58. ['^'],
  59. ['*', '/'],
  60. ['+', '-'],
  61. ['&'],
  62. ['<', '>', '=', '<>', '<=', '>='],
  63. ];
  64. const $ = this;
  65. // Adopted from https://github.com/spreadsheetlab/XLParser/blob/master/src/XLParser/ExcelFormulaGrammar.cs
  66. $.RULE('formulaWithBinaryOp', () => {
  67. const infixes = [];
  68. const values = [$.SUBRULE($.formulaWithPercentOp)];
  69. $.MANY(() => {
  70. // Caching Arrays of Alternatives
  71. // https://sap.github.io/chevrotain/docs/guide/performance.html#caching-arrays-of-alternatives
  72. infixes.push($.OR($.c1 ||
  73. (
  74. $.c1 = [
  75. {ALT: () => $.CONSUME(GtOp).image},
  76. {ALT: () => $.CONSUME(EqOp).image},
  77. {ALT: () => $.CONSUME(LtOp).image},
  78. {ALT: () => $.CONSUME(NeqOp).image},
  79. {ALT: () => $.CONSUME(GteOp).image},
  80. {ALT: () => $.CONSUME(LteOp).image},
  81. {ALT: () => $.CONSUME(ConcatOp).image},
  82. {ALT: () => $.CONSUME(PlusOp).image},
  83. {ALT: () => $.CONSUME(MinOp).image},
  84. {ALT: () => $.CONSUME(MulOp).image},
  85. {ALT: () => $.CONSUME(DivOp).image},
  86. {ALT: () => $.CONSUME(ExOp).image}
  87. ]
  88. )));
  89. values.push($.SUBRULE2($.formulaWithPercentOp));
  90. });
  91. $.ACTION(() => {
  92. // evaluate
  93. for (const ops of this.binaryOperatorsPrecedence) {
  94. for (let index = 0, length = infixes.length; index < length; index++) {
  95. const infix = infixes[index];
  96. if (!ops.includes(infix)) continue;
  97. infixes.splice(index, 1);
  98. values.splice(index, 2, this.utils.applyInfix(values[index], infix, values[index + 1]));
  99. index--;
  100. length--;
  101. }
  102. }
  103. });
  104. return values[0];
  105. });
  106. $.RULE('plusMinusOp', () => $.OR([
  107. {ALT: () => $.CONSUME(PlusOp).image},
  108. {ALT: () => $.CONSUME(MinOp).image}
  109. ]));
  110. $.RULE('formulaWithPercentOp', () => {
  111. let value = $.SUBRULE($.formulaWithUnaryOp);
  112. $.OPTION(() => {
  113. const postfix = $.CONSUME(PercentOp).image;
  114. value = $.ACTION(() => this.utils.applyPostfix(value, postfix));
  115. });
  116. return value;
  117. });
  118. $.RULE('formulaWithUnaryOp', () => {
  119. // support ++---3 => -3
  120. const prefixes = [];
  121. $.MANY(() => {
  122. const op = $.OR([
  123. {ALT: () => $.CONSUME(PlusOp).image},
  124. {ALT: () => $.CONSUME(MinOp).image}
  125. ]);
  126. prefixes.push(op);
  127. });
  128. const formula = $.SUBRULE($.formulaWithIntersect);
  129. if (prefixes.length > 0) return $.ACTION(() => this.utils.applyPrefix(prefixes, formula));
  130. return formula;
  131. });
  132. $.RULE('formulaWithIntersect', () => {
  133. // e.g. 'A1 A2 A3'
  134. let ref1 = $.SUBRULE($.formulaWithRange);
  135. const refs = [ref1];
  136. // console.log('check intersect')
  137. $.MANY({
  138. GATE: () => {
  139. // see https://github.com/SAP/chevrotain/blob/master/examples/grammars/css/css.js#L436-L441
  140. const prevToken = $.LA(0);
  141. const nextToken = $.LA(1);
  142. // This is the only place where the grammar is whitespace sensitive.
  143. return nextToken.startOffset > prevToken.endOffset + 1;
  144. },
  145. DEF: () => {
  146. refs.push($.SUBRULE3($.formulaWithRange));
  147. }
  148. });
  149. if (refs.length > 1) {
  150. return $.ACTION(() => $.ACTION(() => this.utils.applyIntersect(refs)))
  151. }
  152. return ref1;
  153. });
  154. $.RULE('formulaWithRange', () => {
  155. // e.g. 'A1:C3' or 'A1:A3:C4', can be any number of references, at lease 2
  156. const ref1 = $.SUBRULE($.formula);
  157. const refs = [ref1];
  158. $.MANY(() => {
  159. $.CONSUME(Colon);
  160. refs.push($.SUBRULE2($.formula));
  161. });
  162. if (refs.length > 1)
  163. return $.ACTION(() => $.ACTION(() => this.utils.applyRange(refs)));
  164. return ref1;
  165. });
  166. $.RULE('formula', () => $.OR9([
  167. {ALT: () => $.SUBRULE($.referenceWithoutInfix)},
  168. {ALT: () => $.SUBRULE($.paren)},
  169. {ALT: () => $.SUBRULE($.constant)},
  170. {ALT: () => $.SUBRULE($.functionCall)},
  171. {ALT: () => $.SUBRULE($.constantArray)},
  172. ]));
  173. $.RULE('paren', () => {
  174. // formula paren or union paren
  175. $.CONSUME(OpenParen);
  176. let result;
  177. const refs = [];
  178. refs.push($.SUBRULE($.formulaWithBinaryOp));
  179. $.MANY(() => {
  180. $.CONSUME(Comma);
  181. refs.push($.SUBRULE2($.formulaWithBinaryOp));
  182. });
  183. if (refs.length > 1)
  184. result = $.ACTION(() => this.utils.applyUnion(refs));
  185. else
  186. result = refs[0];
  187. $.CONSUME(CloseParen);
  188. return result;
  189. });
  190. $.RULE('constantArray', () => {
  191. // console.log('constantArray');
  192. const arr = [[]];
  193. let currentRow = 0;
  194. $.CONSUME(OpenCurlyParen);
  195. // array must contain at least one item
  196. arr[currentRow].push($.SUBRULE($.constantForArray));
  197. $.MANY(() => {
  198. const sep = $.OR([
  199. {ALT: () => $.CONSUME(Comma).image},
  200. {ALT: () => $.CONSUME(Semicolon).image}
  201. ]);
  202. const constant = $.SUBRULE2($.constantForArray);
  203. if (sep === ',') {
  204. arr[currentRow].push(constant)
  205. } else {
  206. currentRow++;
  207. arr[currentRow] = [];
  208. arr[currentRow].push(constant)
  209. }
  210. });
  211. $.CONSUME(CloseCurlyParen);
  212. return $.ACTION(() => this.utils.toArray(arr));
  213. });
  214. /**
  215. * Used in array
  216. */
  217. $.RULE('constantForArray', () => $.OR([
  218. {
  219. ALT: () => {
  220. const prefix = $.OPTION(() => $.SUBRULE($.plusMinusOp));
  221. const image = $.CONSUME(Number).image;
  222. const number = $.ACTION(() => this.utils.toNumber(image));
  223. if (prefix)
  224. return $.ACTION(() => this.utils.applyPrefix([prefix], number));
  225. return number;
  226. }
  227. }, {
  228. ALT: () => {
  229. const str = $.CONSUME(String).image;
  230. return $.ACTION(() => this.utils.toString(str));
  231. }
  232. }, {
  233. ALT: () => {
  234. const bool = $.CONSUME(Boolean).image;
  235. return $.ACTION(() => this.utils.toBoolean(bool));
  236. }
  237. }, {
  238. ALT: () => {
  239. const err = $.CONSUME(FormulaErrorT).image;
  240. return $.ACTION(() => this.utils.toError(err));
  241. }
  242. }, {
  243. ALT: () => {
  244. const err = $.CONSUME(RefError).image;
  245. return $.ACTION(() => this.utils.toError(err));
  246. }
  247. },
  248. ]));
  249. $.RULE('constant', () => $.OR([
  250. {
  251. ALT: () => {
  252. const number = $.CONSUME(Number).image;
  253. return $.ACTION(() => this.utils.toNumber(number));
  254. }
  255. }, {
  256. ALT: () => {
  257. const str = $.CONSUME(String).image;
  258. return $.ACTION(() => this.utils.toString(str));
  259. }
  260. }, {
  261. ALT: () => {
  262. const bool = $.CONSUME(Boolean).image;
  263. return $.ACTION(() => this.utils.toBoolean(bool));
  264. }
  265. }, {
  266. ALT: () => {
  267. const err = $.CONSUME(FormulaErrorT).image;
  268. return $.ACTION(() => this.utils.toError(err));
  269. }
  270. },
  271. ]));
  272. $.RULE('functionCall', () => {
  273. const functionName = $.CONSUME(Function).image.slice(0, -1);
  274. // console.log('functionName', functionName);
  275. const args = $.SUBRULE($.arguments);
  276. $.CONSUME(CloseParen);
  277. // dependency parser won't call function.
  278. return $.ACTION(() => context.callFunction(functionName, args));
  279. });
  280. $.RULE('arguments', () => {
  281. // console.log('try arguments')
  282. // allows ',' in the front
  283. $.MANY2(() => {
  284. $.CONSUME2(Comma);
  285. });
  286. const args = [];
  287. // allows empty arguments
  288. $.OPTION(() => {
  289. args.push($.SUBRULE($.formulaWithBinaryOp));
  290. $.MANY(() => {
  291. $.CONSUME1(Comma);
  292. args.push(null); // e.g. ROUND(1.5,)
  293. $.OPTION3(() => {
  294. args.pop();
  295. args.push($.SUBRULE2($.formulaWithBinaryOp))
  296. });
  297. });
  298. });
  299. return args;
  300. });
  301. $.RULE('referenceWithoutInfix', () => $.OR([
  302. {ALT: () => $.SUBRULE($.referenceItem)},
  303. {
  304. // sheet name prefix
  305. ALT: () => {
  306. // console.log('try sheetName');
  307. const sheetName = $.SUBRULE($.prefixName);
  308. // console.log('sheetName', sheetName);
  309. const referenceItem = $.SUBRULE2($.formulaWithRange);
  310. $.ACTION(() => {
  311. if (this.utils.isFormulaError(referenceItem))
  312. return referenceItem;
  313. referenceItem.ref.sheet = sheetName
  314. });
  315. return referenceItem;
  316. }
  317. },
  318. // {ALT: () => $.SUBRULE('dynamicDataExchange')},
  319. ]));
  320. $.RULE('referenceItem', () => $.OR([
  321. {
  322. ALT: () => {
  323. const address = $.CONSUME(Cell).image;
  324. return $.ACTION(() => this.utils.parseCellAddress(address));
  325. }
  326. },
  327. {
  328. ALT: () => {
  329. const name = $.CONSUME(Name).image;
  330. return $.ACTION(() => context.getVariable(name))
  331. }
  332. },
  333. {
  334. ALT: () => {
  335. const column = $.CONSUME(Column).image;
  336. return $.ACTION(() => this.utils.parseCol(column))
  337. }
  338. },
  339. // A row check should be here, but the token is same with Number,
  340. // In other to resolve ambiguities, I leave this empty, and
  341. // parse the number to row number when needed.
  342. {
  343. ALT: () => {
  344. const err = $.CONSUME(RefError).image;
  345. return $.ACTION(() => this.utils.toError(err))
  346. }
  347. },
  348. // {ALT: () => $.SUBRULE($.udfFunctionCall)},
  349. // {ALT: () => $.SUBRULE($.structuredReference)},
  350. ]));
  351. $.RULE('prefixName', () => $.OR([
  352. {ALT: () => $.CONSUME(Sheet).image.slice(0, -1)},
  353. {ALT: () => $.CONSUME(SheetQuoted).image.slice(1, -2).replace(/''/g, "'")},
  354. ]));
  355. this.performSelfAnalysis();
  356. }
  357. }
  358. module.exports = {
  359. Parser: Parsing,
  360. };