index.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456
  1. 'use strict';
  2. const internals = {
  3. operators: ['!', '^', '*', '/', '%', '+', '-', '<', '<=', '>', '>=', '==', '!=', '&&', '||', '??'],
  4. operatorCharacters: ['!', '^', '*', '/', '%', '+', '-', '<', '=', '>', '&', '|', '?'],
  5. operatorsOrder: [['^'], ['*', '/', '%'], ['+', '-'], ['<', '<=', '>', '>='], ['==', '!='], ['&&'], ['||', '??']],
  6. operatorsPrefix: ['!', 'n'],
  7. literals: {
  8. '"': '"',
  9. '`': '`',
  10. '\'': '\'',
  11. '[': ']'
  12. },
  13. numberRx: /^(?:[0-9]*(\.[0-9]*)?){1}$/,
  14. tokenRx: /^[\w\$\#\.\@\:\{\}]+$/,
  15. symbol: Symbol('formula'),
  16. settings: Symbol('settings')
  17. };
  18. exports.Parser = class {
  19. constructor(string, options = {}) {
  20. if (!options[internals.settings] &&
  21. options.constants) {
  22. for (const constant in options.constants) {
  23. const value = options.constants[constant];
  24. if (value !== null &&
  25. !['boolean', 'number', 'string'].includes(typeof value)) {
  26. throw new Error(`Formula constant ${constant} contains invalid ${typeof value} value type`);
  27. }
  28. }
  29. }
  30. this.settings = options[internals.settings] ? options : Object.assign({ [internals.settings]: true, constants: {}, functions: {} }, options);
  31. this.single = null;
  32. this._parts = null;
  33. this._parse(string);
  34. }
  35. _parse(string) {
  36. let parts = [];
  37. let current = '';
  38. let parenthesis = 0;
  39. let literal = false;
  40. const flush = (inner) => {
  41. if (parenthesis) {
  42. throw new Error('Formula missing closing parenthesis');
  43. }
  44. const last = parts.length ? parts[parts.length - 1] : null;
  45. if (!literal &&
  46. !current &&
  47. !inner) {
  48. return;
  49. }
  50. if (last &&
  51. last.type === 'reference' &&
  52. inner === ')') { // Function
  53. last.type = 'function';
  54. last.value = this._subFormula(current, last.value);
  55. current = '';
  56. return;
  57. }
  58. if (inner === ')') { // Segment
  59. const sub = new exports.Parser(current, this.settings);
  60. parts.push({ type: 'segment', value: sub });
  61. }
  62. else if (literal) {
  63. if (literal === ']') { // Reference
  64. parts.push({ type: 'reference', value: current });
  65. current = '';
  66. return;
  67. }
  68. parts.push({ type: 'literal', value: current }); // Literal
  69. }
  70. else if (internals.operatorCharacters.includes(current)) { // Operator
  71. if (last &&
  72. last.type === 'operator' &&
  73. internals.operators.includes(last.value + current)) { // 2 characters operator
  74. last.value += current;
  75. }
  76. else {
  77. parts.push({ type: 'operator', value: current });
  78. }
  79. }
  80. else if (current.match(internals.numberRx)) { // Number
  81. parts.push({ type: 'constant', value: parseFloat(current) });
  82. }
  83. else if (this.settings.constants[current] !== undefined) { // Constant
  84. parts.push({ type: 'constant', value: this.settings.constants[current] });
  85. }
  86. else { // Reference
  87. if (!current.match(internals.tokenRx)) {
  88. throw new Error(`Formula contains invalid token: ${current}`);
  89. }
  90. parts.push({ type: 'reference', value: current });
  91. }
  92. current = '';
  93. };
  94. for (const c of string) {
  95. if (literal) {
  96. if (c === literal) {
  97. flush();
  98. literal = false;
  99. }
  100. else {
  101. current += c;
  102. }
  103. }
  104. else if (parenthesis) {
  105. if (c === '(') {
  106. current += c;
  107. ++parenthesis;
  108. }
  109. else if (c === ')') {
  110. --parenthesis;
  111. if (!parenthesis) {
  112. flush(c);
  113. }
  114. else {
  115. current += c;
  116. }
  117. }
  118. else {
  119. current += c;
  120. }
  121. }
  122. else if (c in internals.literals) {
  123. literal = internals.literals[c];
  124. }
  125. else if (c === '(') {
  126. flush();
  127. ++parenthesis;
  128. }
  129. else if (internals.operatorCharacters.includes(c)) {
  130. flush();
  131. current = c;
  132. flush();
  133. }
  134. else if (c !== ' ') {
  135. current += c;
  136. }
  137. else {
  138. flush();
  139. }
  140. }
  141. flush();
  142. // Replace prefix - to internal negative operator
  143. parts = parts.map((part, i) => {
  144. if (part.type !== 'operator' ||
  145. part.value !== '-' ||
  146. i && parts[i - 1].type !== 'operator') {
  147. return part;
  148. }
  149. return { type: 'operator', value: 'n' };
  150. });
  151. // Validate tokens order
  152. let operator = false;
  153. for (const part of parts) {
  154. if (part.type === 'operator') {
  155. if (internals.operatorsPrefix.includes(part.value)) {
  156. continue;
  157. }
  158. if (!operator) {
  159. throw new Error('Formula contains an operator in invalid position');
  160. }
  161. if (!internals.operators.includes(part.value)) {
  162. throw new Error(`Formula contains an unknown operator ${part.value}`);
  163. }
  164. }
  165. else if (operator) {
  166. throw new Error('Formula missing expected operator');
  167. }
  168. operator = !operator;
  169. }
  170. if (!operator) {
  171. throw new Error('Formula contains invalid trailing operator');
  172. }
  173. // Identify single part
  174. if (parts.length === 1 &&
  175. ['reference', 'literal', 'constant'].includes(parts[0].type)) {
  176. this.single = { type: parts[0].type === 'reference' ? 'reference' : 'value', value: parts[0].value };
  177. }
  178. // Process parts
  179. this._parts = parts.map((part) => {
  180. // Operators
  181. if (part.type === 'operator') {
  182. return internals.operatorsPrefix.includes(part.value) ? part : part.value;
  183. }
  184. // Literals, constants, segments
  185. if (part.type !== 'reference') {
  186. return part.value;
  187. }
  188. // References
  189. if (this.settings.tokenRx &&
  190. !this.settings.tokenRx.test(part.value)) {
  191. throw new Error(`Formula contains invalid reference ${part.value}`);
  192. }
  193. if (this.settings.reference) {
  194. return this.settings.reference(part.value);
  195. }
  196. return internals.reference(part.value);
  197. });
  198. }
  199. _subFormula(string, name) {
  200. const method = this.settings.functions[name];
  201. if (typeof method !== 'function') {
  202. throw new Error(`Formula contains unknown function ${name}`);
  203. }
  204. let args = [];
  205. if (string) {
  206. let current = '';
  207. let parenthesis = 0;
  208. let literal = false;
  209. const flush = () => {
  210. if (!current) {
  211. throw new Error(`Formula contains function ${name} with invalid arguments ${string}`);
  212. }
  213. args.push(current);
  214. current = '';
  215. };
  216. for (let i = 0; i < string.length; ++i) {
  217. const c = string[i];
  218. if (literal) {
  219. current += c;
  220. if (c === literal) {
  221. literal = false;
  222. }
  223. }
  224. else if (c in internals.literals &&
  225. !parenthesis) {
  226. current += c;
  227. literal = internals.literals[c];
  228. }
  229. else if (c === ',' &&
  230. !parenthesis) {
  231. flush();
  232. }
  233. else {
  234. current += c;
  235. if (c === '(') {
  236. ++parenthesis;
  237. }
  238. else if (c === ')') {
  239. --parenthesis;
  240. }
  241. }
  242. }
  243. flush();
  244. }
  245. args = args.map((arg) => new exports.Parser(arg, this.settings));
  246. return function (context) {
  247. const innerValues = [];
  248. for (const arg of args) {
  249. innerValues.push(arg.evaluate(context));
  250. }
  251. return method.call(context, ...innerValues);
  252. };
  253. }
  254. evaluate(context) {
  255. const parts = this._parts.slice();
  256. // Prefix operators
  257. for (let i = parts.length - 2; i >= 0; --i) {
  258. const part = parts[i];
  259. if (part &&
  260. part.type === 'operator') {
  261. const current = parts[i + 1];
  262. parts.splice(i + 1, 1);
  263. const value = internals.evaluate(current, context);
  264. parts[i] = internals.single(part.value, value);
  265. }
  266. }
  267. // Left-right operators
  268. internals.operatorsOrder.forEach((set) => {
  269. for (let i = 1; i < parts.length - 1;) {
  270. if (set.includes(parts[i])) {
  271. const operator = parts[i];
  272. const left = internals.evaluate(parts[i - 1], context);
  273. const right = internals.evaluate(parts[i + 1], context);
  274. parts.splice(i, 2);
  275. const result = internals.calculate(operator, left, right);
  276. parts[i - 1] = result === 0 ? 0 : result; // Convert -0
  277. }
  278. else {
  279. i += 2;
  280. }
  281. }
  282. });
  283. return internals.evaluate(parts[0], context);
  284. }
  285. };
  286. exports.Parser.prototype[internals.symbol] = true;
  287. internals.reference = function (name) {
  288. return function (context) {
  289. return context && context[name] !== undefined ? context[name] : null;
  290. };
  291. };
  292. internals.evaluate = function (part, context) {
  293. if (part === null) {
  294. return null;
  295. }
  296. if (typeof part === 'function') {
  297. return part(context);
  298. }
  299. if (part[internals.symbol]) {
  300. return part.evaluate(context);
  301. }
  302. return part;
  303. };
  304. internals.single = function (operator, value) {
  305. if (operator === '!') {
  306. return value ? false : true;
  307. }
  308. // operator === 'n'
  309. const negative = -value;
  310. if (negative === 0) { // Override -0
  311. return 0;
  312. }
  313. return negative;
  314. };
  315. internals.calculate = function (operator, left, right) {
  316. if (operator === '??') {
  317. return internals.exists(left) ? left : right;
  318. }
  319. if (typeof left === 'string' ||
  320. typeof right === 'string') {
  321. if (operator === '+') {
  322. left = internals.exists(left) ? left : '';
  323. right = internals.exists(right) ? right : '';
  324. return left + right;
  325. }
  326. }
  327. else {
  328. switch (operator) {
  329. case '^': return Math.pow(left, right);
  330. case '*': return left * right;
  331. case '/': return left / right;
  332. case '%': return left % right;
  333. case '+': return left + right;
  334. case '-': return left - right;
  335. }
  336. }
  337. switch (operator) {
  338. case '<': return left < right;
  339. case '<=': return left <= right;
  340. case '>': return left > right;
  341. case '>=': return left >= right;
  342. case '==': return left === right;
  343. case '!=': return left !== right;
  344. case '&&': return left && right;
  345. case '||': return left || right;
  346. }
  347. return null;
  348. };
  349. internals.exists = function (value) {
  350. return value !== null && value !== undefined;
  351. };