@xeplr/expression-handler 1.0.1

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 ADDED
@@ -0,0 +1,103 @@
1
+ # @xeplr/expression-handler
2
+
3
+ Level-1 structured expression evaluator. JSON expression in, boolean out — apply
4
+ field **functions** (`upper`, `month`, …) and **operators** (`eq`, `contains`,
5
+ `gt`, `between`, `isNull`, …) to a data row. No parser, no nesting, no `AND`/`OR`
6
+ combinators (by design — level 1 only). Zero dependencies.
7
+
8
+ ## Usage
9
+
10
+ ```js
11
+ const xf = require('@xeplr/expression-handler');
12
+
13
+ // upper(name) = upper(nickName)
14
+ xf.evaluate(
15
+ { left: { fn: 'upper', field: 'name' }, op: 'eq', right: { fn: 'upper', field: 'nickName' } },
16
+ { name: 'ada', nickName: 'ADA' }
17
+ ); // → true
18
+
19
+ // month(createdAt) = 'January'
20
+ xf.evaluate(
21
+ { left: { fn: 'month', field: 'createdAt' }, op: 'eq', right: { value: 'January' } },
22
+ { createdAt: '2026-01-15T00:00:00Z' }
23
+ ); // → true
24
+
25
+ // Compile once, filter many (drops into a streaming .filter too)
26
+ const isAdult = xf.toPredicate({ left: { field: 'age' }, op: 'gte', right: { value: 18 } });
27
+ rows.filter(isAdult);
28
+ ```
29
+
30
+ ## Expression shape
31
+
32
+ ```
33
+ { left: <operand>, op: <string>, right?: <operand> }
34
+ ```
35
+
36
+ **Operand** — one of:
37
+
38
+ | Form | Meaning |
39
+ |---|---|
40
+ | `{ field: 'name' }` | `row.name` |
41
+ | `{ field: 'a.b' }` | `row.a.b` (dot path) |
42
+ | `{ field: 'name', fn: 'upper' }` | `upper(row.name)` |
43
+ | `{ value: 42 }` | literal (string / number / boolean / null / array) |
44
+
45
+ `right` is omitted for unary operators (`isNull`, `isNotNull`).
46
+
47
+ ## Operators
48
+
49
+ `eq` `neq` `isNull` `isNotNull` · `gt` `gte` `lt` `lte` `between` (`[lo,hi]`, inclusive) ·
50
+ `contains` `notContains` `startsWith` `endsWith` · `in` `notIn`
51
+ Aliases: `equals`, `notEquals`, `doesNotContain`.
52
+
53
+ `eq`/`neq` against `{ value: null }` also work as null checks. Null-ish values make
54
+ numeric/string comparisons return `false` (never throw).
55
+
56
+ ## Functions
57
+
58
+ String: `upper` `lower` `trim` `length` · Date (UTC): `month` (`'January'`) `monthNum`
59
+ `year` `day` `quarter` `weekday` (`'Monday'`).
60
+
61
+ ## Extending
62
+
63
+ ```js
64
+ xf.registerFunction('reverse', v => String(v).split('').reverse().join(''));
65
+ xf.registerOperator('regex', (a, b) => new RegExp(b).test(String(a)));
66
+ ```
67
+
68
+ ## Formula wrapper (Excel-like)
69
+
70
+ Write formulas as strings; they compile to the same structured JSON.
71
+
72
+ ```js
73
+ xf.formula.parse('if(upper(someColumn)=anotherColumn, "Yes", "No")');
74
+ // → { if: { left:{fn:'upper',field:'someColumn'}, op:'eq', right:{field:'anotherColumn'} },
75
+ // then: { value:'Yes' },
76
+ // else: { value:'No' } }
77
+
78
+ const f = xf.formula.compile('if(score>=90,"A",if(score>=80,"B","C"))'); // nested ifs
79
+ f({ score: 85 }); // → 'B'
80
+ ```
81
+
82
+ Rules: `"double"`/`'single'` quoted → string literal (doubled quote escapes); bare
83
+ word → field ref; `number` / `null` / `true` / `false` → literals; `name(...)` →
84
+ function (value fn `upper`/`month`…, boolean fn `contains`/`between`/`isnull`…, or
85
+ `if`); comparisons `= == != <> > >= < <=`. Function names are case-insensitive;
86
+ `if(...)` nests. An IF evaluates to its then/else value; a bare comparison
87
+ (`upper(name)="ADA"`) compiles to a boolean predicate.
88
+
89
+ `xf.formula.parse(str)` · `xf.formula.compile(str) → (row)=>value` · `xf.formula.evaluate(node, row)`
90
+
91
+ ## NLP wrapper (stub)
92
+
93
+ `xf.nlp` — natural language → expression JSON. **Not implemented yet**
94
+ (`xf.nlp.implemented === false`; `xf.nlp.parse()` throws). Placeholder surface for
95
+ a future LLM/grammar backend.
96
+
97
+ ## API
98
+
99
+ `evaluate(expr, row)` · `toPredicate(expr)` · `validate(expr) → { valid, errors }` ·
100
+ `formula.{parse,compile,evaluate}` · `nlp` (stub) ·
101
+ `registerFunction` · `registerOperator` · `operators()` · `functions()`
102
+
103
+ Test: `npm test` (node --test, no DB). 29 tests.
package/index.js ADDED
@@ -0,0 +1,50 @@
1
+ // @xeplr/expression-handler — level-1 structured expression evaluator.
2
+ //
3
+ // const xf = require('@xeplr/expression-handler');
4
+ //
5
+ // xf.evaluate(
6
+ // { left: { fn: 'upper', field: 'name' }, op: 'eq', right: { fn: 'upper', field: 'nickName' } },
7
+ // { name: 'ada', nickName: 'ADA' }
8
+ // ); // → true
9
+ //
10
+ // const isAdult = xf.toPredicate({ left: { field: 'age' }, op: 'gte', right: { value: 18 } });
11
+ // rows.filter(isAdult);
12
+ //
13
+ // Extend with your own functions / operators:
14
+ // xf.registerFunction('reverse', v => String(v).split('').reverse().join(''));
15
+ // xf.registerOperator('regex', (a, b) => new RegExp(b).test(a));
16
+
17
+ var { evaluate, toPredicate, validate, resolveOperand, getPath } = require('./lib/evaluate');
18
+ var { OPERATORS, registerOperator } = require('./lib/operators');
19
+ var { FUNCTIONS, registerFunction } = require('./lib/functions');
20
+ var { parseFormula, evaluateFormula, compileFormula } = require('./lib/formula');
21
+ var nlp = require('./lib/nlp');
22
+
23
+ module.exports = {
24
+ evaluate: evaluate,
25
+ toPredicate: toPredicate,
26
+ validate: validate,
27
+ resolveOperand: resolveOperand,
28
+ getPath: getPath,
29
+
30
+ // Excel-like formula wrapper → structured JSON (+ evaluate it).
31
+ // xf.formula.parse('if(upper(a)=b,"Yes","No")') → JSON
32
+ // xf.formula.compile(str)(row) → value
33
+ formula: {
34
+ parse: parseFormula,
35
+ evaluate: evaluateFormula,
36
+ compile: compileFormula
37
+ },
38
+
39
+ // Natural-language wrapper — STUB (not implemented yet).
40
+ nlp: nlp,
41
+
42
+ registerOperator: registerOperator,
43
+ registerFunction: registerFunction,
44
+
45
+ // Introspection — the registered operator/function names.
46
+ OPERATORS: OPERATORS,
47
+ FUNCTIONS: FUNCTIONS,
48
+ operators: function () { return Object.keys(OPERATORS); },
49
+ functions: function () { return Object.keys(FUNCTIONS); }
50
+ };
@@ -0,0 +1,102 @@
1
+ // Evaluate a level-1 structured expression against a data row → boolean.
2
+ //
3
+ // Expression shape:
4
+ // { left: <operand>, op: <string>, right?: <operand> }
5
+ //
6
+ // Operand shape (one of):
7
+ // { field: 'name' } → row.name
8
+ // { field: 'a.b' } → row.a.b (dot path)
9
+ // { field: 'name', fn: 'upper' } → upper(row.name)
10
+ // { value: 42 } → literal (string | number | boolean | null | array)
11
+ // { value: [1, 10] } → literal array (for between / in)
12
+ //
13
+ // Unary operators (isNull / isNotNull) ignore `right`.
14
+ //
15
+ // evaluate({ left:{fn:'month',field:'createdAt'}, op:'eq', right:{value:'January'} }, row)
16
+
17
+ var { OPERATORS } = require('./operators');
18
+ var { FUNCTIONS, applyFunction } = require('./functions');
19
+
20
+ function getPath(obj, path) {
21
+ if (obj === null || obj === undefined) return undefined;
22
+ if (path.indexOf('.') === -1) return obj[path];
23
+ return path.split('.').reduce(function (o, k) {
24
+ return (o === null || o === undefined) ? undefined : o[k];
25
+ }, obj);
26
+ }
27
+
28
+ // Resolve an operand to a concrete value against the row.
29
+ function resolveOperand(operand, row) {
30
+ if (operand === null || operand === undefined) return undefined;
31
+ var v;
32
+ if (Object.prototype.hasOwnProperty.call(operand, 'value')) {
33
+ v = operand.value;
34
+ } else if (operand.field !== null && operand.field !== undefined) {
35
+ v = getPath(row, operand.field);
36
+ } else {
37
+ throw new Error('Invalid operand: needs "field" or "value"');
38
+ }
39
+ if (operand.fn) v = applyFunction(operand.fn, v);
40
+ return v;
41
+ }
42
+
43
+ function evaluate(expr, row) {
44
+ if (!expr || typeof expr !== 'object') throw new Error('expression must be an object');
45
+ var opDef = OPERATORS[expr.op];
46
+ if (!opDef) throw new Error('Unknown operator: "' + expr.op + '"');
47
+
48
+ var left = resolveOperand(expr.left, row);
49
+ if (opDef.arity === 1) return !!opDef.fn(left);
50
+
51
+ var right = resolveOperand(
52
+ (expr.right === null || expr.right === undefined) ? { value: undefined } : expr.right,
53
+ row
54
+ );
55
+ return !!opDef.fn(left, right);
56
+ }
57
+
58
+ // Compile an expression into a reusable row predicate (validated once up front).
59
+ // Drops straight into Array.filter / a streaming .filter(...).
60
+ function toPredicate(expr) {
61
+ var v = validate(expr);
62
+ if (!v.valid) throw new Error('Invalid expression: ' + v.errors.join('; '));
63
+ return function (row) { return evaluate(expr, row); };
64
+ }
65
+
66
+ function validateOperand(operand, side, errors) {
67
+ if (operand === null || operand === undefined || typeof operand !== 'object') {
68
+ errors.push(side + ' operand must be an object with "field" or "value"');
69
+ return;
70
+ }
71
+ var hasValue = Object.prototype.hasOwnProperty.call(operand, 'value');
72
+ if (!hasValue && (operand.field === null || operand.field === undefined)) {
73
+ errors.push(side + ' operand needs "field" or "value"');
74
+ }
75
+ if (operand.fn && !FUNCTIONS[operand.fn]) {
76
+ errors.push('unknown function: "' + operand.fn + '"');
77
+ }
78
+ }
79
+
80
+ // Static shape check — returns { valid, errors } without touching a row.
81
+ function validate(expr) {
82
+ var errors = [];
83
+ if (!expr || typeof expr !== 'object') return { valid: false, errors: ['expression must be an object'] };
84
+
85
+ var opDef = OPERATORS[expr.op];
86
+ if (!opDef) errors.push('unknown operator: "' + expr.op + '"');
87
+
88
+ if (expr.left === null || expr.left === undefined) errors.push('missing left operand');
89
+ else validateOperand(expr.left, 'left', errors);
90
+
91
+ if (opDef && opDef.arity === 2) {
92
+ if (expr.right === null || expr.right === undefined) {
93
+ errors.push('operator "' + expr.op + '" requires a right operand');
94
+ } else {
95
+ validateOperand(expr.right, 'right', errors);
96
+ }
97
+ }
98
+
99
+ return { valid: errors.length === 0, errors: errors };
100
+ }
101
+
102
+ module.exports = { evaluate, toPredicate, validate, resolveOperand, getPath };
package/lib/formula.js ADDED
@@ -0,0 +1,196 @@
1
+ // Formula wrapper — parse an Excel-like formula string into the structured
2
+ // JSON the evaluator understands, and evaluate it against a row.
3
+ //
4
+ // parseFormula('if(upper(someColumn)=anotherColumn, "Yes", "No")')
5
+ // →
6
+ // { if: { left: { fn:'upper', field:'someColumn' }, op:'eq',
7
+ // right: { field:'anotherColumn' } },
8
+ // then: { value: 'Yes' },
9
+ // else: { value: 'No' } }
10
+ //
11
+ // Rules (level 1 — no arithmetic, no AND/OR, single function nesting):
12
+ // • "double quoted" (or 'single quoted') → string literal { value }
13
+ // • bare word → field reference { field }
14
+ // • number / null / true / false → literal { value }
15
+ // • name(...) → function call (value fn like
16
+ // upper/month, boolean fn like contains/between/isnull, or IF)
17
+ // • comparisons: = == != <> > >= < <=
18
+ //
19
+ // An IF node evaluates to its then/else VALUE; a bare comparison evaluates to
20
+ // a boolean. So compileFormula() yields a value-producer or a predicate alike.
21
+
22
+ var { evaluate, resolveOperand } = require('./evaluate');
23
+ var { FUNCTIONS } = require('./functions');
24
+
25
+ // Boolean functions → operator names (arity fixed). Keys are lowercased.
26
+ var BOOL_FUNCS = {
27
+ contains: { op: 'contains', arity: 2 },
28
+ notcontains: { op: 'notContains', arity: 2 },
29
+ doesnotcontain: { op: 'notContains', arity: 2 },
30
+ startswith: { op: 'startsWith', arity: 2 },
31
+ endswith: { op: 'endsWith', arity: 2 },
32
+ isnull: { op: 'isNull', arity: 1 },
33
+ isnotnull: { op: 'isNotNull', arity: 1 },
34
+ between: { op: 'between', arity: 3 }
35
+ };
36
+
37
+ var OP_SYMBOLS = { '=': 'eq', '==': 'eq', '!=': 'neq', '<>': 'neq', '>': 'gt', '>=': 'gte', '<': 'lt', '<=': 'lte' };
38
+
39
+ // ── tokenizer ────────────────────────────────────────────────────────────
40
+ function tokenize(input) {
41
+ var tokens = [];
42
+ var i = 0, n = input.length;
43
+ while (i < n) {
44
+ var c = input[i];
45
+ if (/\s/.test(c)) { i++; continue; }
46
+
47
+ if (c === '"' || c === "'") { // string literal (doubled quote = escape)
48
+ var q = c; i++; var s = '';
49
+ while (i < n) {
50
+ if (input[i] === q) {
51
+ if (input[i + 1] === q) { s += q; i += 2; continue; }
52
+ i++; break;
53
+ }
54
+ s += input[i++];
55
+ }
56
+ tokens.push({ t: 'string', v: s });
57
+ continue;
58
+ }
59
+ if (c === '(') { tokens.push({ t: 'lparen' }); i++; continue; }
60
+ if (c === ')') { tokens.push({ t: 'rparen' }); i++; continue; }
61
+ if (c === ',') { tokens.push({ t: 'comma' }); i++; continue; }
62
+
63
+ var two = input.substr(i, 2);
64
+ if (two === '>=' || two === '<=' || two === '<>' || two === '!=' || two === '==') { tokens.push({ t: 'op', v: two }); i += 2; continue; }
65
+ if (c === '=' || c === '>' || c === '<') { tokens.push({ t: 'op', v: c }); i++; continue; }
66
+
67
+ if (/[0-9]/.test(c) || (c === '-' && /[0-9]/.test(input[i + 1] || ''))) {
68
+ var num = c; i++;
69
+ while (i < n && /[0-9.]/.test(input[i])) num += input[i++];
70
+ tokens.push({ t: 'number', v: parseFloat(num) });
71
+ continue;
72
+ }
73
+ if (/[A-Za-z_]/.test(c)) {
74
+ var id = c; i++;
75
+ while (i < n && /[A-Za-z0-9_.]/.test(input[i])) id += input[i++];
76
+ tokens.push({ t: 'ident', v: id });
77
+ continue;
78
+ }
79
+ throw new Error('Unexpected character "' + c + '" at position ' + i);
80
+ }
81
+ return tokens;
82
+ }
83
+
84
+ // Resolve a function name to the registry's canonical (case-insensitive) key.
85
+ function canonicalFn(low) {
86
+ if (FUNCTIONS[low]) return low;
87
+ var keys = Object.keys(FUNCTIONS);
88
+ for (var i = 0; i < keys.length; i++) if (keys[i].toLowerCase() === low) return keys[i];
89
+ return low; // unknown — evaluate() will throw a clear error at run time
90
+ }
91
+
92
+ function literalOf(node, ctx) {
93
+ if (node && Object.prototype.hasOwnProperty.call(node, 'value')) return node.value;
94
+ throw new Error(ctx + ' must be a literal value');
95
+ }
96
+
97
+ // ── parser (recursive descent) ─────────────────────────────────────────────
98
+ function parseFormula(input) {
99
+ if (typeof input !== 'string') throw new Error('parseFormula: input must be a string');
100
+ var toks = tokenize(input);
101
+ var pos = 0;
102
+ function peek() { return toks[pos]; }
103
+ function expect(t) {
104
+ var tk = toks[pos];
105
+ if (!tk || tk.t !== t) throw new Error('Expected ' + t + ' but got ' + (tk ? (tk.t + (tk.v != null ? ' "' + tk.v + '"' : '')) : 'end of formula'));
106
+ return toks[pos++];
107
+ }
108
+
109
+ function parseExpression() {
110
+ var left = parsePrimary();
111
+ var tk = peek();
112
+ if (tk && tk.t === 'op') {
113
+ pos++;
114
+ var right = parsePrimary();
115
+ return { left: left, op: OP_SYMBOLS[tk.v], right: right };
116
+ }
117
+ return left;
118
+ }
119
+
120
+ function parsePrimary() {
121
+ var tk = peek();
122
+ if (!tk) throw new Error('Unexpected end of formula');
123
+
124
+ if (tk.t === 'string') { pos++; return { value: tk.v }; }
125
+ if (tk.t === 'number') { pos++; return { value: tk.v }; }
126
+ if (tk.t === 'lparen') { pos++; var e = parseExpression(); expect('rparen'); return e; }
127
+
128
+ if (tk.t === 'ident') {
129
+ var name = tk.v;
130
+ var low = name.toLowerCase();
131
+ var isCall = toks[pos + 1] && toks[pos + 1].t === 'lparen';
132
+ if (isCall) {
133
+ pos += 2; // consume ident + '('
134
+ if (low === 'if') return parseIf();
135
+ if (BOOL_FUNCS[low]) return parseBoolFunc(low);
136
+ return parseValueFunc(low);
137
+ }
138
+ pos++;
139
+ if (low === 'null') return { value: null };
140
+ if (low === 'true') return { value: true };
141
+ if (low === 'false') return { value: false };
142
+ return { field: name }; // field refs keep their case
143
+ }
144
+ throw new Error('Unexpected token: ' + tk.t);
145
+ }
146
+
147
+ function parseIf() { // 'if' + '(' already consumed
148
+ var cond = parseExpression();
149
+ expect('comma');
150
+ var thenNode = parseExpression();
151
+ expect('comma');
152
+ var elseNode = parseExpression();
153
+ expect('rparen');
154
+ return { if: cond, then: thenNode, else: elseNode };
155
+ }
156
+
157
+ function parseValueFunc(low) { // e.g. upper(x), month(d)
158
+ var arg = parseExpression();
159
+ expect('rparen');
160
+ if (arg && (arg.fn || arg.if || arg.op)) throw new Error('level-1: function "' + low + '" argument must be a field or literal');
161
+ return Object.assign({ fn: canonicalFn(low) }, arg);
162
+ }
163
+
164
+ function parseBoolFunc(low) { // e.g. contains(a,b), isnull(a), between(a,lo,hi)
165
+ var def = BOOL_FUNCS[low];
166
+ var args = [parseExpression()];
167
+ while (peek() && peek().t === 'comma') { pos++; args.push(parseExpression()); }
168
+ expect('rparen');
169
+ if (args.length !== def.arity) throw new Error(low + '() expects ' + def.arity + ' argument(s), got ' + args.length);
170
+ if (def.op === 'between') return { left: args[0], op: 'between', right: { value: [literalOf(args[1], 'between low bound'), literalOf(args[2], 'between high bound')] } };
171
+ if (def.arity === 1) return { left: args[0], op: def.op };
172
+ return { left: args[0], op: def.op, right: args[1] };
173
+ }
174
+
175
+ var result = parseExpression();
176
+ if (pos < toks.length) throw new Error('Unexpected trailing tokens in formula (near token ' + (pos + 1) + ')');
177
+ return result;
178
+ }
179
+
180
+ // ── evaluation ─────────────────────────────────────────────────────────────
181
+ // Returns a VALUE (IF → then/else value), or a boolean (bare comparison).
182
+ function evaluateFormula(node, row) {
183
+ if (node === null || node === undefined) return null;
184
+ if (Object.prototype.hasOwnProperty.call(node, 'if')) {
185
+ return evaluate(node.if, row) ? evaluateFormula(node.then, row) : evaluateFormula(node.else, row);
186
+ }
187
+ if (node.op) return evaluate(node, row); // comparison → boolean
188
+ return resolveOperand(node, row); // operand → value
189
+ }
190
+
191
+ function compileFormula(input) {
192
+ var node = parseFormula(input);
193
+ return function (row) { return evaluateFormula(node, row); };
194
+ }
195
+
196
+ module.exports = { parseFormula, evaluateFormula, compileFormula };
@@ -0,0 +1,50 @@
1
+ // Field functions — applied to an operand's resolved value before the operator
2
+ // runs. Level 1: a function takes one value and returns one value.
3
+ //
4
+ // Dates use UTC getters (xeplr stores datetimes as UTC), and month/weekday
5
+ // return English names so `month(dateField) eq 'January'` reads naturally.
6
+ // null/invalid input → null (so the downstream comparison simply fails rather
7
+ // than throwing).
8
+
9
+ var MONTHS = [
10
+ 'January', 'February', 'March', 'April', 'May', 'June',
11
+ 'July', 'August', 'September', 'October', 'November', 'December'
12
+ ];
13
+ var WEEKDAYS = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
14
+
15
+ function toDate(v) {
16
+ if (v === null || v === undefined) return null;
17
+ var d = v instanceof Date ? v : new Date(v);
18
+ return isNaN(d.getTime()) ? null : d;
19
+ }
20
+
21
+ function nil(v) { return v === null || v === undefined; }
22
+
23
+ var FUNCTIONS = {
24
+ // ── string ──
25
+ upper: function (v) { return nil(v) ? null : String(v).toUpperCase(); },
26
+ lower: function (v) { return nil(v) ? null : String(v).toLowerCase(); },
27
+ trim: function (v) { return nil(v) ? null : String(v).trim(); },
28
+ length: function (v) { return nil(v) ? null : String(v).length; },
29
+
30
+ // ── date (UTC) ──
31
+ month: function (v) { var d = toDate(v); return d ? MONTHS[d.getUTCMonth()] : null; }, // 'January'
32
+ monthNum:function (v) { var d = toDate(v); return d ? d.getUTCMonth() + 1 : null; }, // 1–12
33
+ year: function (v) { var d = toDate(v); return d ? d.getUTCFullYear() : null; },
34
+ day: function (v) { var d = toDate(v); return d ? d.getUTCDate() : null; }, // 1–31
35
+ quarter: function (v) { var d = toDate(v); return d ? Math.floor(d.getUTCMonth() / 3) + 1 : null; }, // 1–4
36
+ weekday: function (v) { var d = toDate(v); return d ? WEEKDAYS[d.getUTCDay()] : null; } // 'Monday'
37
+ };
38
+
39
+ function registerFunction(name, fn) {
40
+ if (typeof fn !== 'function') throw new Error('registerFunction: fn must be a function');
41
+ FUNCTIONS[name] = fn;
42
+ }
43
+
44
+ function applyFunction(name, value) {
45
+ var fn = FUNCTIONS[name];
46
+ if (!fn) throw new Error('Unknown function: "' + name + '"');
47
+ return fn(value);
48
+ }
49
+
50
+ module.exports = { FUNCTIONS, registerFunction, applyFunction };
package/lib/nlp.js ADDED
@@ -0,0 +1,16 @@
1
+ // NLP wrapper — STUB. Future: turn a natural-language phrase into the same
2
+ // structured expression/formula JSON the evaluator consumes, e.g.
3
+ // "rows where the created month is January"
4
+ // → { left: { fn:'month', field:'created' }, op:'eq', right:{ value:'January' } }
5
+ //
6
+ // Intentionally not implemented yet. Shipped as a stub so the surface exists
7
+ // and callers can feature-detect. Wire an LLM/grammar backend here later.
8
+
9
+ function parse(_text, _options) {
10
+ throw new Error('nlp wrapper: not implemented yet (stub). Use formula.parseFormula or the structured JSON API.');
11
+ }
12
+
13
+ module.exports = {
14
+ parse: parse,
15
+ implemented: false
16
+ };
@@ -0,0 +1,69 @@
1
+ // Operators — each is a small handler with a fixed arity:
2
+ // arity 2 → fn(left, right) (eq, gt, contains, between, …)
3
+ // arity 1 → fn(left) (isNull, isNotNull)
4
+ //
5
+ // Semantics chosen for filter/rules use, documented per operator:
6
+ // • null-ish (null/undefined) on a numeric/string comparison → false
7
+ // (except isNull/isNotNull, and eq/neq which treat null explicitly).
8
+ // • numeric operators coerce via Number(); non-numeric → NaN → false.
9
+ // • string operators coerce via String().
10
+
11
+ function isNil(v) { return v === null || v === undefined; }
12
+ function num(v) { return typeof v === 'number' ? v : Number(v); }
13
+ function str(v) { return isNil(v) ? '' : String(v); }
14
+
15
+ // Equality that treats null explicitly and compares numbers as numbers,
16
+ // everything else as strings (so 18 == '18', 'January' == 'January').
17
+ function looseEq(a, b) {
18
+ if (isNil(a) && isNil(b)) return true;
19
+ if (isNil(a) || isNil(b)) return false;
20
+ if (typeof a === 'number' && typeof b === 'number') return a === b;
21
+ return String(a) === String(b);
22
+ }
23
+
24
+ var OPERATORS = {
25
+ // equality / null — eq/neq against a null value ALSO work as is-null checks
26
+ eq: { arity: 2, fn: function (a, b) { return looseEq(a, b); } },
27
+ neq: { arity: 2, fn: function (a, b) { return !looseEq(a, b); } },
28
+ isNull: { arity: 1, fn: function (a) { return isNil(a); } },
29
+ isNotNull: { arity: 1, fn: function (a) { return !isNil(a); } },
30
+
31
+ // numeric
32
+ gt: { arity: 2, fn: function (a, b) { return num(a) > num(b); } },
33
+ gte: { arity: 2, fn: function (a, b) { return num(a) >= num(b); } },
34
+ lt: { arity: 2, fn: function (a, b) { return num(a) < num(b); } },
35
+ lte: { arity: 2, fn: function (a, b) { return num(a) <= num(b); } },
36
+ between: {
37
+ arity: 2,
38
+ // right operand is a [lo, hi] pair; inclusive.
39
+ fn: function (a, b) {
40
+ if (!Array.isArray(b) || b.length < 2) return false;
41
+ var x = num(a), lo = num(b[0]), hi = num(b[1]);
42
+ return x >= lo && x <= hi;
43
+ }
44
+ },
45
+
46
+ // string
47
+ contains: { arity: 2, fn: function (a, b) { return str(a).indexOf(str(b)) !== -1; } },
48
+ notContains: { arity: 2, fn: function (a, b) { return str(a).indexOf(str(b)) === -1; } },
49
+ startsWith: { arity: 2, fn: function (a, b) { return str(a).indexOf(str(b)) === 0; } },
50
+ endsWith: { arity: 2, fn: function (a, b) { var s = str(a), t = str(b); return t === '' || s.slice(-t.length) === t; } },
51
+
52
+ // set membership — right operand is an array
53
+ in: { arity: 2, fn: function (a, b) { return Array.isArray(b) && b.some(function (x) { return looseEq(a, x); }); } },
54
+ notIn: { arity: 2, fn: function (a, b) { return !(Array.isArray(b) && b.some(function (x) { return looseEq(a, x); })); } }
55
+ };
56
+
57
+ // Friendly aliases for the phrasings a UI/user might send.
58
+ OPERATORS.equals = OPERATORS.eq;
59
+ OPERATORS.notEquals = OPERATORS.neq;
60
+ OPERATORS.doesNotContain = OPERATORS.notContains;
61
+
62
+ function registerOperator(name, def) {
63
+ if (typeof def === 'function') def = { arity: 2, fn: def };
64
+ if (!def || typeof def.fn !== 'function') throw new Error('registerOperator: needs a function or { arity, fn }');
65
+ if (def.arity !== 1 && def.arity !== 2) def.arity = 2;
66
+ OPERATORS[name] = def;
67
+ }
68
+
69
+ module.exports = { OPERATORS, registerOperator, looseEq, num, str, isNil };
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@xeplr/expression-handler",
3
+ "version": "1.0.1",
4
+ "description": "Level-1 structured expression evaluator — apply field functions (upper/month/…) and operators (eq/contains/gt/between/isNull/…) to a data row. No parser; JSON expressions in, boolean out.",
5
+ "main": "index.js",
6
+ "files": [
7
+ "index.js",
8
+ "lib/"
9
+ ],
10
+ "scripts": {
11
+ "test": "node --test"
12
+ },
13
+ "keywords": [
14
+ "expression",
15
+ "formula",
16
+ "filter",
17
+ "predicate",
18
+ "rules",
19
+ "evaluator",
20
+ "xeplr"
21
+ ],
22
+ "author": "xeplr",
23
+ "license": "MIT",
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "https://github.com/Xeplr/xeplr-expression-handler"
27
+ },
28
+ "publishConfig": {
29
+ "access": "public"
30
+ }
31
+ }