grammar/hooks.js

  1. const TextFunctions = require('../formulas/functions/text');
  2. const MathFunctions = require('../formulas/functions/math');
  3. const TrigFunctions = require('../formulas/functions/trigonometry');
  4. const LogicalFunctions = require('../formulas/functions/logical');
  5. const EngFunctions = require('../formulas/functions/engineering');
  6. const ReferenceFunctions = require('../formulas/functions/reference');
  7. const InformationFunctions = require('../formulas/functions/information');
  8. const StatisticalFunctions = require('../formulas/functions/statistical');
  9. const DateFunctions = require('../formulas/functions/date');
  10. const WebFunctions = require('../formulas/functions/web');
  11. const FormulaError = require('../formulas/error');
  12. const {FormulaHelpers} = require('../formulas/helpers');
  13. const {Parser, allTokens} = require('./parsing');
  14. const lexer = require('./lexing');
  15. const Utils = require('./utils');
  16. /**
  17. * A Excel Formula Parser & Evaluator
  18. */
  19. class FormulaParser {
  20. /**
  21. * @param {{functions: {}, functionsNeedContext: {}, onVariable: function, onCell: function, onRange: function}} [config]
  22. * @param isTest - is in testing environment
  23. */
  24. constructor(config, isTest = false) {
  25. this.logs = [];
  26. this.isTest = isTest;
  27. this.utils = new Utils(this);
  28. config = Object.assign({
  29. functions: {},
  30. functionsNeedContext: {},
  31. onVariable: () => null,
  32. onCell: () => 0,
  33. onRange: () => [[0]],
  34. }, config);
  35. this.onVariable = config.onVariable;
  36. this.functions = Object.assign({}, DateFunctions, StatisticalFunctions, InformationFunctions, ReferenceFunctions,
  37. EngFunctions, LogicalFunctions, TextFunctions, MathFunctions, TrigFunctions, WebFunctions,
  38. config.functions, config.functionsNeedContext);
  39. this.onRange = config.onRange;
  40. this.onCell = config.onCell;
  41. // functions treat null as 0, other functions treats null as ""
  42. this.funsNullAs0 = Object.keys(MathFunctions)
  43. .concat(Object.keys(TrigFunctions))
  44. .concat(Object.keys(LogicalFunctions))
  45. .concat(Object.keys(EngFunctions))
  46. .concat(Object.keys(ReferenceFunctions))
  47. .concat(Object.keys(StatisticalFunctions))
  48. .concat(Object.keys(DateFunctions));
  49. // functions need context and don't need to retrieve references
  50. this.funsNeedContextAndNoDataRetrieve = ['ROW', 'ROWS', 'COLUMN', 'COLUMNS', 'SUMIF', 'INDEX', 'AVERAGEIF', 'IF'];
  51. // functions need parser context
  52. this.funsNeedContext = [...Object.keys(config.functionsNeedContext), ...this.funsNeedContextAndNoDataRetrieve,
  53. 'INDEX', 'OFFSET', 'INDIRECT', 'IF', 'CHOOSE', 'WEBSERVICE'];
  54. // functions preserve reference in arguments
  55. this.funsPreserveRef = Object.keys(InformationFunctions);
  56. this.parser = new Parser(this, this.utils);
  57. }
  58. /**
  59. * Get all lexing token names. Webpack needs this.
  60. * @return {Array.<string>} - All token names that should not be minimized.
  61. */
  62. static get allTokens() {
  63. return allTokens;
  64. }
  65. /**
  66. * Get value from the cell reference
  67. * @param ref
  68. * @return {*}
  69. */
  70. getCell(ref) {
  71. // console.log('get cell', JSON.stringify(ref));
  72. if (ref.sheet == null)
  73. ref.sheet = this.position ? this.position.sheet : undefined;
  74. return this.onCell(ref);
  75. }
  76. /**
  77. * Get values from the range reference.
  78. * @param ref
  79. * @return {*}
  80. */
  81. getRange(ref) {
  82. // console.log('get range', JSON.stringify(ref));
  83. if (ref.sheet == null)
  84. ref.sheet = this.position ? this.position.sheet : undefined;
  85. return this.onRange(ref)
  86. }
  87. /**
  88. * TODO:
  89. * Get references or values from a user defined variable.
  90. * @param name
  91. * @return {*}
  92. */
  93. getVariable(name) {
  94. // console.log('get variable', name);
  95. const res = {ref: this.onVariable(name, this.position.sheet, this.position)};
  96. if (res.ref == null)
  97. return FormulaError.NAME;
  98. return res;
  99. }
  100. /**
  101. * Retrieve values from the given reference.
  102. * @param valueOrRef
  103. * @return {*}
  104. */
  105. retrieveRef(valueOrRef) {
  106. if (FormulaHelpers.isRangeRef(valueOrRef)) {
  107. return this.getRange(valueOrRef.ref);
  108. }
  109. if (FormulaHelpers.isCellRef(valueOrRef)) {
  110. return this.getCell(valueOrRef.ref)
  111. }
  112. return valueOrRef;
  113. }
  114. /**
  115. * Call an excel function.
  116. * @param name - Function name.
  117. * @param args - Arguments that pass to the function.
  118. * @return {*}
  119. */
  120. _callFunction(name, args) {
  121. if (name.indexOf('_xlfn.') === 0)
  122. name = name.slice(6);
  123. name = name.toUpperCase();
  124. // if one arg is null, it means 0 or "" depends on the function it calls
  125. const nullValue = this.funsNullAs0.includes(name) ? 0 : '';
  126. if (!this.funsNeedContextAndNoDataRetrieve.includes(name)) {
  127. // retrieve reference
  128. args = args.map(arg => {
  129. if (arg === null)
  130. return {value: nullValue, isArray: false, omitted: true};
  131. const res = this.utils.extractRefValue(arg);
  132. if (this.funsPreserveRef.includes(name)) {
  133. return {value: res.val, isArray: res.isArray, ref: arg.ref};
  134. }
  135. return {
  136. value: res.val,
  137. isArray: res.isArray,
  138. isRangeRef: !!FormulaHelpers.isRangeRef(arg),
  139. isCellRef: !!FormulaHelpers.isCellRef(arg)
  140. };
  141. });
  142. }
  143. // console.log('callFunction', name, args)
  144. if (this.functions[name]) {
  145. let res;
  146. try {
  147. if (!this.funsNeedContextAndNoDataRetrieve.includes(name) && !this.funsNeedContext.includes(name))
  148. res = (this.functions[name](...args));
  149. else
  150. res = (this.functions[name](this, ...args));
  151. } catch (e) {
  152. // allow functions throw FormulaError, this make functions easier to implement!
  153. if (e instanceof FormulaError) {
  154. return e;
  155. } else {
  156. throw e;
  157. }
  158. }
  159. if (res === undefined) {
  160. // console.log(`Function ${name} may be not implemented.`);
  161. if (this.isTest) {
  162. if (!this.logs.includes(name)) this.logs.push(name);
  163. return {value: 0, ref: {}};
  164. }
  165. throw FormulaError.NOT_IMPLEMENTED(name);
  166. }
  167. return res;
  168. } else {
  169. // console.log(`Function ${name} is not implemented`);
  170. if (this.isTest) {
  171. if (!this.logs.includes(name)) this.logs.push(name);
  172. return {value: 0, ref: {}};
  173. }
  174. throw FormulaError.NOT_IMPLEMENTED(name);
  175. }
  176. }
  177. async callFunctionAsync(name, args) {
  178. const awaitedArgs = [];
  179. for (const arg of args) {
  180. awaitedArgs.push(await arg);
  181. }
  182. const res = await this._callFunction(name, awaitedArgs);
  183. return FormulaHelpers.checkFunctionResult(res)
  184. }
  185. callFunction(name, args) {
  186. if (this.async) {
  187. return this.callFunctionAsync(name, args);
  188. } else {
  189. const res = this._callFunction(name, args);
  190. return FormulaHelpers.checkFunctionResult(res);
  191. }
  192. }
  193. /**
  194. * Return currently supported functions.
  195. * @return {this}
  196. */
  197. supportedFunctions() {
  198. const supported = [];
  199. const functions = Object.keys(this.functions);
  200. functions.forEach(fun => {
  201. try {
  202. const res = this.functions[fun](0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
  203. if (res === undefined) return;
  204. supported.push(fun);
  205. } catch (e) {
  206. if (e instanceof Error)
  207. supported.push(fun);
  208. }
  209. });
  210. return supported.sort();
  211. }
  212. /**
  213. * Check and return the appropriate formula result.
  214. * @param result
  215. * @param {boolean} [allowReturnArray] - If the formula can return an array
  216. * @return {*}
  217. */
  218. checkFormulaResult(result, allowReturnArray = false) {
  219. const type = typeof result;
  220. // number
  221. if (type === 'number') {
  222. if (isNaN(result)) {
  223. return FormulaError.VALUE;
  224. } else if (!isFinite(result)) {
  225. return FormulaError.NUM;
  226. }
  227. result += 0; // make -0 to 0
  228. } else if (type === 'object') {
  229. if (result instanceof FormulaError)
  230. return result;
  231. if (allowReturnArray) {
  232. if (result.ref) {
  233. result = this.retrieveRef(result);
  234. }
  235. // Disallow union, and other unknown data types.
  236. // e.g. `=(A1:C1, A2:E9)` -> #VALUE!
  237. if (typeof result === 'object' && !Array.isArray(result) && result != null) {
  238. return FormulaError.VALUE;
  239. }
  240. } else {
  241. if (result.ref && result.ref.row && !result.ref.from) {
  242. // single cell reference
  243. result = this.retrieveRef(result);
  244. } else if (result.ref && result.ref.from && result.ref.from.col === result.ref.to.col) {
  245. // single Column reference
  246. result = this.retrieveRef({
  247. ref: {
  248. row: result.ref.from.row, col: result.ref.from.col
  249. }
  250. });
  251. } else if (Array.isArray(result)) {
  252. result = result[0][0]
  253. } else {
  254. // array, range reference, union collections
  255. return FormulaError.VALUE;
  256. }
  257. }
  258. }
  259. return result;
  260. }
  261. /**
  262. * Parse an excel formula.
  263. * @param {string} inputText
  264. * @param {{row: number, col: number}} [position] - The position of the parsed formula
  265. * e.g. {row: 1, col: 1}
  266. * @param {boolean} [allowReturnArray] - If the formula can return an array. Useful when parsing array formulas,
  267. * or data validation formulas.
  268. * @returns {*}
  269. */
  270. parse(inputText, position, allowReturnArray = false) {
  271. if (inputText.length === 0) throw Error('Input must not be empty.');
  272. this.position = position;
  273. this.async = false;
  274. const lexResult = lexer.lex(inputText);
  275. this.parser.input = lexResult.tokens;
  276. let res;
  277. try {
  278. res = this.parser.formulaWithBinaryOp();
  279. res = this.checkFormulaResult(res, allowReturnArray);
  280. if (res instanceof FormulaError) {
  281. return res;
  282. }
  283. } catch (e) {
  284. throw FormulaError.ERROR(e.message, e);
  285. }
  286. if (this.parser.errors.length > 0) {
  287. const error = this.parser.errors[0];
  288. throw Utils.formatChevrotainError(error, inputText);
  289. }
  290. return res;
  291. }
  292. /**
  293. * Parse an excel formula asynchronously.
  294. * Use when providing custom async functions.
  295. * @param {string} inputText
  296. * @param {{row: number, col: number}} [position] - The position of the parsed formula
  297. * e.g. {row: 1, col: 1}
  298. * @param {boolean} [allowReturnArray] - If the formula can return an array. Useful when parsing array formulas,
  299. * or data validation formulas.
  300. * @returns {*}
  301. */
  302. async parseAsync(inputText, position, allowReturnArray = false) {
  303. if (inputText.length === 0) throw Error('Input must not be empty.');
  304. this.position = position;
  305. this.async = true;
  306. const lexResult = lexer.lex(inputText);
  307. this.parser.input = lexResult.tokens;
  308. let res;
  309. try {
  310. res = await this.parser.formulaWithBinaryOp();
  311. res = this.checkFormulaResult(res, allowReturnArray);
  312. if (res instanceof FormulaError) {
  313. return res;
  314. }
  315. } catch (e) {
  316. throw FormulaError.ERROR(e.message, e);
  317. }
  318. if (this.parser.errors.length > 0) {
  319. const error = this.parser.errors[0];
  320. throw Utils.formatChevrotainError(error, inputText);
  321. }
  322. return res;
  323. }
  324. }
  325. module.exports = {
  326. FormulaParser,
  327. FormulaHelpers,
  328. };