@xeplr/expression-handler 2.0.0 → 2.1.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.
package/README.md CHANGED
@@ -171,6 +171,7 @@ const f = xf.compile('sumif(fytd(order_date), revenue)', {
171
171
  | `fiscalYearStart` | `4` (April) | `fytd` and any financial-year function |
172
172
  | `today` | now | `ytd`, `fytd` — pin it for tests and scheduled runs |
173
173
  | `missing` | `'null'` | `'error'` makes a name the row lacks an error |
174
+ | `ignoreCase` | `false` | `true` makes `=`, `!=`, `in` and the text operators (`contains`, `startsWith`, `endsWith`, `notIn`, …) ignore case. Numbers and null compare exactly as before. BI's report formulas set it, so a formula and the SQL it is translated into agree whatever the database's collation; Workflow's conditions leave it off |
174
175
  | `maxListItems` · `maxDistinct` · `maxSteps` · `maxLevels` | see [Limits](#limits) | |
175
176
  | `aggregate(name, args, { compile, level })` | none | supplies the rows for the group form of a total (BI) |
176
177
 
package/lib/compile.js CHANGED
@@ -31,7 +31,8 @@ var DEFAULTS = {
31
31
  maxListItems: 10000, // items in one list a total runs over
32
32
  maxDistinct: 1000000, // distinct values one countifunique may hold
33
33
  maxSteps: 10000000, // list items visited in one evaluation
34
- maxLevels: 2 // totals inside totals
34
+ maxLevels: 2, // totals inside totals
35
+ ignoreCase: false // true: =, !=, in and the text operators ignore case
35
36
  };
36
37
 
37
38
  function konst(v) {
@@ -50,6 +51,9 @@ function compileTree(tree, options) {
50
51
  if (!(fy >= 1 && fy <= 12 && Math.floor(fy) === fy)) fail('fiscalYearStart must be a month, 1–12');
51
52
 
52
53
  var ctx = { now: dates.todayFor(opts), fiscalYearStart: fy };
54
+ // Equality for `=`, `in` and the word operators: exact, or folded when the
55
+ // caller asked (opts.ignoreCase) — see operators.IGNORE_CASE.
56
+ var eq = opts.ignoreCase ? operators.looseEqIgnoreCase : operators.looseEq;
53
57
  // Shared by every closure of this formula: how many list items one
54
58
  // evaluation has visited. Reset per top-level call.
55
59
  var state = { steps: 0 };
@@ -172,7 +176,7 @@ function compileTree(tree, options) {
172
176
  // Membership compares like the 1.x `in` (18 matches "18").
173
177
  'in': function (a, b) {
174
178
  if (!Array.isArray(b)) return false;
175
- for (var i = 0; i < b.length; i++) if (operators.looseEq(a, b[i])) return true;
179
+ for (var i = 0; i < b.length; i++) if (eq(a, b[i])) return true;
176
180
  return false;
177
181
  }
178
182
  };
@@ -184,7 +188,9 @@ function compileTree(tree, options) {
184
188
  return binary(c(node.left, level), c(node.right, level), sym);
185
189
  }
186
190
  // 1.x word operators: { left, op: 'eq' | 'contains' | 'isNull' | …, right }
187
- var def = Object.prototype.hasOwnProperty.call(operators.OPERATORS, node.op) ? operators.OPERATORS[node.op] : null;
191
+ var def = opts.ignoreCase && Object.prototype.hasOwnProperty.call(operators.IGNORE_CASE, node.op)
192
+ ? operators.IGNORE_CASE[node.op]
193
+ : Object.prototype.hasOwnProperty.call(operators.OPERATORS, node.op) ? operators.OPERATORS[node.op] : null;
188
194
  if (!def) fail('Unknown operator: "' + node.op + '"');
189
195
  if (node.left === undefined) fail('missing left operand');
190
196
  var left = c(node.left, level);
package/lib/operators.js CHANGED
@@ -59,6 +59,28 @@ OPERATORS.equals = OPERATORS.eq;
59
59
  OPERATORS.notEquals = OPERATORS.neq;
60
60
  OPERATORS.doesNotContain = OPERATORS.notContains;
61
61
 
62
+ // THE SAME OPERATORS, IGNORING CASE — chosen per formula (compile option
63
+ // `ignoreCase`), never by changing the ones above: Workflow's conditions and
64
+ // every 1.x caller keep exact matching. Only text is folded; numbers and
65
+ // null compare exactly as before.
66
+ function fold(v) { return typeof v === 'string' ? v.toLowerCase() : v; }
67
+ function looseEqIgnoreCase(a, b) { return looseEq(fold(a), fold(b)); }
68
+ function lower(v) { return str(v).toLowerCase(); }
69
+
70
+ var IGNORE_CASE = {
71
+ eq: { arity: 2, fn: function (a, b) { return looseEqIgnoreCase(a, b); } },
72
+ neq: { arity: 2, fn: function (a, b) { return !looseEqIgnoreCase(a, b); } },
73
+ contains: { arity: 2, fn: function (a, b) { return lower(a).indexOf(lower(b)) !== -1; } },
74
+ notContains: { arity: 2, fn: function (a, b) { return lower(a).indexOf(lower(b)) === -1; } },
75
+ startsWith: { arity: 2, fn: function (a, b) { return lower(a).indexOf(lower(b)) === 0; } },
76
+ endsWith: { arity: 2, fn: function (a, b) { var s = lower(a), t = lower(b); return t === '' || s.slice(-t.length) === t; } },
77
+ in: { arity: 2, fn: function (a, b) { return Array.isArray(b) && b.some(function (x) { return looseEqIgnoreCase(a, x); }); } },
78
+ notIn: { arity: 2, fn: function (a, b) { return !(Array.isArray(b) && b.some(function (x) { return looseEqIgnoreCase(a, x); })); } }
79
+ };
80
+ IGNORE_CASE.equals = IGNORE_CASE.eq;
81
+ IGNORE_CASE.notEquals = IGNORE_CASE.neq;
82
+ IGNORE_CASE.doesNotContain = IGNORE_CASE.notContains;
83
+
62
84
  function registerOperator(name, def) {
63
85
  if (typeof def === 'function') def = { arity: 2, fn: def };
64
86
  if (!def || typeof def.fn !== 'function') throw new Error('registerOperator: needs a function or { arity, fn }');
@@ -66,4 +88,4 @@ function registerOperator(name, def) {
66
88
  OPERATORS[name] = def;
67
89
  }
68
90
 
69
- module.exports = { OPERATORS, registerOperator, looseEq, num, str, isNil };
91
+ module.exports = { OPERATORS, IGNORE_CASE, registerOperator, looseEq, looseEqIgnoreCase, num, str, isNil };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xeplr/expression-handler",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "The one formula engine for xeplr: its own parser (no eval, no dependencies), one function list, compiled-once formulas, totals over lists, AND/OR conditions. Runs in Node and the browser.",
5
5
  "main": "index.js",
6
6
  "files": [