@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 +1 -0
- package/lib/compile.js +9 -3
- package/lib/operators.js +23 -1
- package/package.json +1 -1
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
|
|
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 (
|
|
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.
|
|
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.
|
|
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": [
|