@jbrowse/jexl 4.0.1 → 5.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 +163 -20
- package/dist/Expression.d.ts +16 -0
- package/dist/Expression.js +28 -4
- package/dist/Expression.js.map +1 -1
- package/dist/Jexl.d.ts +20 -8
- package/dist/Jexl.js +34 -15
- package/dist/Jexl.js.map +1 -1
- package/dist/Lexer.d.ts +4 -3
- package/dist/Lexer.js +29 -21
- package/dist/Lexer.js.map +1 -1
- package/dist/analyze.d.ts +84 -0
- package/dist/analyze.js +259 -0
- package/dist/analyze.js.map +1 -0
- package/dist/check.d.ts +109 -0
- package/dist/check.js +1267 -0
- package/dist/check.js.map +1 -0
- package/dist/collections.d.ts +3 -0
- package/dist/collections.js +140 -0
- package/dist/collections.js.map +1 -0
- package/dist/conditions.d.ts +62 -0
- package/dist/conditions.js +207 -0
- package/dist/conditions.js.map +1 -0
- package/dist/errors.d.ts +5 -0
- package/dist/errors.js +18 -0
- package/dist/errors.js.map +1 -0
- package/dist/evaluator/compile.d.ts +10 -7
- package/dist/evaluator/compile.js +292 -99
- package/dist/evaluator/compile.js.map +1 -1
- package/dist/grammar.d.ts +22 -9
- package/dist/grammar.js +83 -55
- package/dist/grammar.js.map +1 -1
- package/dist/index.d.ts +9 -0
- package/dist/index.js +16 -1
- package/dist/index.js.map +1 -1
- package/dist/operators.d.ts +34 -0
- package/dist/operators.js +131 -0
- package/dist/operators.js.map +1 -0
- package/dist/parser/Parser.d.ts +48 -91
- package/dist/parser/Parser.js +347 -221
- package/dist/parser/Parser.js.map +1 -1
- package/dist/types.d.ts +11 -5
- package/esm/Expression.d.ts +16 -0
- package/esm/Expression.js +28 -4
- package/esm/Expression.js.map +1 -1
- package/esm/Jexl.d.ts +20 -8
- package/esm/Jexl.js +34 -15
- package/esm/Jexl.js.map +1 -1
- package/esm/Lexer.d.ts +4 -3
- package/esm/Lexer.js +29 -21
- package/esm/Lexer.js.map +1 -1
- package/esm/analyze.d.ts +84 -0
- package/esm/analyze.js +256 -0
- package/esm/analyze.js.map +1 -0
- package/esm/check.d.ts +109 -0
- package/esm/check.js +1260 -0
- package/esm/check.js.map +1 -0
- package/esm/collections.d.ts +3 -0
- package/esm/collections.js +137 -0
- package/esm/collections.js.map +1 -0
- package/esm/conditions.d.ts +62 -0
- package/esm/conditions.js +200 -0
- package/esm/conditions.js.map +1 -0
- package/esm/errors.d.ts +5 -0
- package/esm/errors.js +14 -0
- package/esm/errors.js.map +1 -0
- package/esm/evaluator/compile.d.ts +10 -7
- package/esm/evaluator/compile.js +292 -99
- package/esm/evaluator/compile.js.map +1 -1
- package/esm/grammar.d.ts +22 -9
- package/esm/grammar.js +79 -53
- package/esm/grammar.js.map +1 -1
- package/esm/index.d.ts +9 -0
- package/esm/index.js +4 -0
- package/esm/index.js.map +1 -1
- package/esm/operators.d.ts +34 -0
- package/esm/operators.js +123 -0
- package/esm/operators.js.map +1 -0
- package/esm/parser/Parser.d.ts +48 -91
- package/esm/parser/Parser.js +349 -192
- package/esm/parser/Parser.js.map +1 -1
- package/esm/types.d.ts +11 -5
- package/package.json +1 -1
- package/src/Expression.ts +34 -4
- package/src/Jexl.ts +52 -16
- package/src/Lexer.ts +32 -21
- package/src/analyze.ts +378 -0
- package/src/check.ts +1682 -0
- package/src/collections.ts +148 -0
- package/src/conditions.ts +262 -0
- package/src/errors.ts +15 -0
- package/src/evaluator/compile.ts +351 -102
- package/src/grammar.ts +137 -55
- package/src/index.ts +15 -0
- package/src/operators.ts +151 -0
- package/src/parser/Parser.ts +389 -199
- package/src/types.ts +13 -3
- package/dist/parser/handlers.d.ts +0 -115
- package/dist/parser/handlers.js +0 -352
- package/dist/parser/handlers.js.map +0 -1
- package/dist/parser/states.d.ts +0 -47
- package/dist/parser/states.js +0 -176
- package/dist/parser/states.js.map +0 -1
- package/esm/parser/handlers.d.ts +0 -115
- package/esm/parser/handlers.js +0 -328
- package/esm/parser/handlers.js.map +0 -1
- package/esm/parser/states.d.ts +0 -47
- package/esm/parser/states.js +0 -140
- package/esm/parser/states.js.map +0 -1
- package/src/parser/handlers.ts +0 -372
- package/src/parser/states.ts +0 -159
package/src/grammar.ts
CHANGED
|
@@ -5,6 +5,16 @@
|
|
|
5
5
|
|
|
6
6
|
/* eslint eqeqeq:0 */
|
|
7
7
|
|
|
8
|
+
import { collectionFunctions } from './collections.ts'
|
|
9
|
+
import {
|
|
10
|
+
addsPairwise,
|
|
11
|
+
anyPair,
|
|
12
|
+
isIn,
|
|
13
|
+
isList,
|
|
14
|
+
matches,
|
|
15
|
+
pairwise
|
|
16
|
+
} from './operators.ts'
|
|
17
|
+
|
|
8
18
|
import type { JexlValue } from './types.ts'
|
|
9
19
|
|
|
10
20
|
export type BinaryOpEval = (left: JexlValue, right: JexlValue) => JexlValue
|
|
@@ -40,6 +50,8 @@ export interface BinaryOp {
|
|
|
40
50
|
* which is how `-x` is distinguished from `a - x`.
|
|
41
51
|
*/
|
|
42
52
|
unaryEval?: (right: JexlValue) => JexlValue
|
|
53
|
+
/** Groups a chain from the right, so `a ^ b ^ c` is `a ^ (b ^ c)`. */
|
|
54
|
+
rightAssociative?: boolean
|
|
43
55
|
}
|
|
44
56
|
|
|
45
57
|
export type UnaryOpEval = (right: JexlValue) => JexlValue
|
|
@@ -52,9 +64,9 @@ export interface UnaryOp {
|
|
|
52
64
|
|
|
53
65
|
/**
|
|
54
66
|
* The punctuation elements of the grammar. Unlike operators these carry no
|
|
55
|
-
* behavior of their own; the Parser
|
|
56
|
-
*
|
|
57
|
-
*
|
|
67
|
+
* behavior of their own; the Parser gives them meaning. The type is a literal
|
|
68
|
+
* union rather than `string` so that `GrammarElement` is a discriminated union,
|
|
69
|
+
* letting operator properties be accessed without casts.
|
|
58
70
|
*/
|
|
59
71
|
export interface SimpleElement {
|
|
60
72
|
type:
|
|
@@ -69,6 +81,7 @@ export interface SimpleElement {
|
|
|
69
81
|
| 'closeParen'
|
|
70
82
|
| 'question'
|
|
71
83
|
| 'semicolon'
|
|
84
|
+
| 'arrow'
|
|
72
85
|
}
|
|
73
86
|
|
|
74
87
|
export type GrammarElement = BinaryOp | UnaryOp | SimpleElement
|
|
@@ -76,20 +89,71 @@ export type GrammarElement = BinaryOp | UnaryOp | SimpleElement
|
|
|
76
89
|
/** A registered function, as jexl calls it once the operands are evaluated. */
|
|
77
90
|
export type GrammarFn = (...args: JexlValue[]) => JexlValue
|
|
78
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Reads `key` off `subject` for `subject.key` and `subject[key]`, in place of
|
|
94
|
+
* a plain property read. Like a registered function, it is handed host values
|
|
95
|
+
* jexl has no type for, so the subject is only promised not to be nullish.
|
|
96
|
+
*/
|
|
97
|
+
export type GetMember = (
|
|
98
|
+
subject: NonNullable<unknown>,
|
|
99
|
+
key: string | number
|
|
100
|
+
) => unknown
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Called once per bare variable name as an expression compiles. A reader it
|
|
104
|
+
* returns replaces `context[name]` for that name; `undefined` keeps it.
|
|
105
|
+
*/
|
|
106
|
+
export type VariableReader = (
|
|
107
|
+
name: string
|
|
108
|
+
) => ((context: Record<string, unknown>) => unknown) | undefined
|
|
109
|
+
|
|
79
110
|
export interface Grammar {
|
|
80
111
|
elements: Record<string, GrammarElement>
|
|
81
112
|
functions: Record<string, GrammarFn>
|
|
113
|
+
getMember?: GetMember
|
|
114
|
+
variableReader?: VariableReader
|
|
82
115
|
}
|
|
83
116
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
117
|
+
const plus = (left: JexlValue, right: JexlValue) =>
|
|
118
|
+
(left as number) + (right as number)
|
|
119
|
+
const minus = (left: JexlValue, right: JexlValue) =>
|
|
120
|
+
(left as number) - (right as number)
|
|
121
|
+
const times = (left: JexlValue, right: JexlValue) =>
|
|
122
|
+
(left as number) * (right as number)
|
|
123
|
+
const divide = (left: JexlValue, right: JexlValue) =>
|
|
124
|
+
(left as number) / (right as number)
|
|
125
|
+
const floorDivide = (left: JexlValue, right: JexlValue) =>
|
|
126
|
+
Math.floor((left as number) / (right as number))
|
|
127
|
+
const modulo = (left: JexlValue, right: JexlValue) =>
|
|
128
|
+
(left as number) % (right as number)
|
|
129
|
+
const power = (left: JexlValue, right: JexlValue) =>
|
|
130
|
+
(left as number) ** (right as number)
|
|
131
|
+
const loose = (left: JexlValue, right: JexlValue) => left == right
|
|
132
|
+
// a missing value orders against nothing, as bcftools skips one; JavaScript
|
|
133
|
+
// alone would read null as 0, making [null, 0.2] < 0.05 true
|
|
134
|
+
const greater = (left: JexlValue, right: JexlValue) =>
|
|
135
|
+
left != null && right != null && (left as number) > (right as number)
|
|
136
|
+
const atLeast = (left: JexlValue, right: JexlValue) =>
|
|
137
|
+
left != null && right != null && (left as number) >= (right as number)
|
|
138
|
+
const less = (left: JexlValue, right: JexlValue) =>
|
|
139
|
+
left != null && right != null && (left as number) < (right as number)
|
|
140
|
+
const atMost = (left: JexlValue, right: JexlValue) =>
|
|
141
|
+
left != null && right != null && (left as number) <= (right as number)
|
|
142
|
+
// named so the compiler can recognize the built-in forms and short-circuit
|
|
143
|
+
// without the thunks a host's evalOnDemand operator is handed
|
|
144
|
+
export const and: BinaryOpEvalOnDemand = (left, right) => {
|
|
145
|
+
const leftVal = left.eval()
|
|
146
|
+
return leftVal ? right.eval() : leftVal
|
|
92
147
|
}
|
|
148
|
+
export const or: BinaryOpEvalOnDemand = (left, right) => {
|
|
149
|
+
const leftVal = left.eval()
|
|
150
|
+
return leftVal || right.eval()
|
|
151
|
+
}
|
|
152
|
+
export const nullish: BinaryOpEvalOnDemand = (left, right) =>
|
|
153
|
+
left.eval() ?? right.eval()
|
|
154
|
+
|
|
155
|
+
const equals = (left: JexlValue, right: JexlValue) =>
|
|
156
|
+
isList(left, right) ? anyPair(loose, left, right) : left == right
|
|
93
157
|
|
|
94
158
|
export const getGrammar = (): Grammar => ({
|
|
95
159
|
/**
|
|
@@ -109,111 +173,129 @@ export const getGrammar = (): Grammar => ({
|
|
|
109
173
|
')': { type: 'closeParen' },
|
|
110
174
|
'?': { type: 'question' },
|
|
111
175
|
';': { type: 'semicolon' },
|
|
176
|
+
'=>': { type: 'arrow' },
|
|
112
177
|
'+': {
|
|
113
178
|
type: 'binaryOp',
|
|
114
179
|
precedence: 30,
|
|
115
|
-
eval: (left, right) =>
|
|
180
|
+
eval: (left, right) =>
|
|
181
|
+
addsPairwise(left, right)
|
|
182
|
+
? pairwise(plus, left, right)
|
|
183
|
+
: plus(left, right)
|
|
116
184
|
},
|
|
117
185
|
'-': {
|
|
118
186
|
type: 'binaryOp',
|
|
119
187
|
precedence: 30,
|
|
120
|
-
eval: (left, right) =>
|
|
121
|
-
|
|
188
|
+
eval: (left, right) =>
|
|
189
|
+
isList(left, right) ? pairwise(minus, left, right) : minus(left, right),
|
|
190
|
+
unaryEval: (right) =>
|
|
191
|
+
Array.isArray(right)
|
|
192
|
+
? right.map((value) => -(value as number))
|
|
193
|
+
: -(right as number)
|
|
122
194
|
},
|
|
123
195
|
'*': {
|
|
124
196
|
type: 'binaryOp',
|
|
125
197
|
precedence: 40,
|
|
126
|
-
eval: (left, right) =>
|
|
198
|
+
eval: (left, right) =>
|
|
199
|
+
isList(left, right) ? pairwise(times, left, right) : times(left, right)
|
|
127
200
|
},
|
|
128
201
|
'/': {
|
|
129
202
|
type: 'binaryOp',
|
|
130
203
|
precedence: 40,
|
|
131
|
-
eval: (left, right) =>
|
|
204
|
+
eval: (left, right) =>
|
|
205
|
+
isList(left, right)
|
|
206
|
+
? pairwise(divide, left, right)
|
|
207
|
+
: divide(left, right)
|
|
132
208
|
},
|
|
133
209
|
'//': {
|
|
134
210
|
type: 'binaryOp',
|
|
135
211
|
precedence: 40,
|
|
136
|
-
eval: (left, right) =>
|
|
212
|
+
eval: (left, right) =>
|
|
213
|
+
isList(left, right)
|
|
214
|
+
? pairwise(floorDivide, left, right)
|
|
215
|
+
: floorDivide(left, right)
|
|
137
216
|
},
|
|
138
217
|
'%': {
|
|
139
218
|
type: 'binaryOp',
|
|
140
219
|
precedence: 50,
|
|
141
|
-
eval: (left, right) =>
|
|
220
|
+
eval: (left, right) =>
|
|
221
|
+
isList(left, right)
|
|
222
|
+
? pairwise(modulo, left, right)
|
|
223
|
+
: modulo(left, right)
|
|
142
224
|
},
|
|
143
225
|
'^': {
|
|
144
226
|
type: 'binaryOp',
|
|
145
227
|
precedence: 50,
|
|
146
|
-
|
|
228
|
+
rightAssociative: true,
|
|
229
|
+
eval: (left, right) =>
|
|
230
|
+
isList(left, right) ? pairwise(power, left, right) : power(left, right)
|
|
147
231
|
},
|
|
148
232
|
'==': {
|
|
149
233
|
type: 'binaryOp',
|
|
150
234
|
precedence: 20,
|
|
151
|
-
eval:
|
|
235
|
+
eval: equals
|
|
152
236
|
},
|
|
153
237
|
'!=': {
|
|
154
238
|
type: 'binaryOp',
|
|
155
239
|
precedence: 20,
|
|
156
|
-
eval: (left, right) => left
|
|
240
|
+
eval: (left, right) => !equals(left, right)
|
|
241
|
+
},
|
|
242
|
+
'~': {
|
|
243
|
+
type: 'binaryOp',
|
|
244
|
+
precedence: 20,
|
|
245
|
+
eval: matches
|
|
246
|
+
},
|
|
247
|
+
'!~': {
|
|
248
|
+
type: 'binaryOp',
|
|
249
|
+
precedence: 20,
|
|
250
|
+
eval: (left, right) => !matches(left, right)
|
|
157
251
|
},
|
|
158
252
|
'>': {
|
|
159
253
|
type: 'binaryOp',
|
|
160
254
|
precedence: 20,
|
|
161
|
-
eval: (left, right) =>
|
|
255
|
+
eval: (left, right) =>
|
|
256
|
+
isList(left, right)
|
|
257
|
+
? anyPair(greater, left, right)
|
|
258
|
+
: greater(left, right)
|
|
162
259
|
},
|
|
163
260
|
'>=': {
|
|
164
261
|
type: 'binaryOp',
|
|
165
262
|
precedence: 20,
|
|
166
|
-
eval: (left, right) =>
|
|
263
|
+
eval: (left, right) =>
|
|
264
|
+
isList(left, right)
|
|
265
|
+
? anyPair(atLeast, left, right)
|
|
266
|
+
: atLeast(left, right)
|
|
167
267
|
},
|
|
168
268
|
'<': {
|
|
169
269
|
type: 'binaryOp',
|
|
170
270
|
precedence: 20,
|
|
171
|
-
eval: (left, right) =>
|
|
271
|
+
eval: (left, right) =>
|
|
272
|
+
isList(left, right) ? anyPair(less, left, right) : less(left, right)
|
|
172
273
|
},
|
|
173
274
|
'<=': {
|
|
174
275
|
type: 'binaryOp',
|
|
175
276
|
precedence: 20,
|
|
176
|
-
eval: (left, right) =>
|
|
277
|
+
eval: (left, right) =>
|
|
278
|
+
isList(left, right) ? anyPair(atMost, left, right) : atMost(left, right)
|
|
177
279
|
},
|
|
178
280
|
'&&': {
|
|
179
281
|
type: 'binaryOp',
|
|
180
|
-
precedence:
|
|
181
|
-
evalOnDemand:
|
|
182
|
-
const leftVal = left.eval()
|
|
183
|
-
if (!leftVal) {
|
|
184
|
-
return leftVal
|
|
185
|
-
}
|
|
186
|
-
return right.eval()
|
|
187
|
-
}
|
|
282
|
+
precedence: 11,
|
|
283
|
+
evalOnDemand: and
|
|
188
284
|
},
|
|
189
285
|
'||': {
|
|
190
286
|
type: 'binaryOp',
|
|
191
287
|
precedence: 10,
|
|
192
|
-
evalOnDemand:
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}
|
|
288
|
+
evalOnDemand: or
|
|
289
|
+
},
|
|
290
|
+
'??': {
|
|
291
|
+
type: 'binaryOp',
|
|
292
|
+
precedence: 10,
|
|
293
|
+
evalOnDemand: nullish
|
|
199
294
|
},
|
|
200
295
|
in: {
|
|
201
296
|
type: 'binaryOp',
|
|
202
297
|
precedence: 20,
|
|
203
|
-
eval:
|
|
204
|
-
if (typeof right === 'string') {
|
|
205
|
-
// only a primitive has a meaningful substring form. An absent or
|
|
206
|
-
// structured left operand is not "in" a string, and must not be
|
|
207
|
-
// coerced to '' — every string contains the empty string, so that
|
|
208
|
-
// made `feature.missingAttr in "someString"` true for every feature.
|
|
209
|
-
return typeof left === 'string' ||
|
|
210
|
-
typeof left === 'number' ||
|
|
211
|
-
typeof left === 'boolean'
|
|
212
|
-
? right.includes(String(left))
|
|
213
|
-
: false
|
|
214
|
-
}
|
|
215
|
-
return Array.isArray(right) ? right.includes(left) : false
|
|
216
|
-
}
|
|
298
|
+
eval: isIn
|
|
217
299
|
},
|
|
218
300
|
'!': {
|
|
219
301
|
type: 'unaryOp',
|
|
@@ -243,5 +325,5 @@ export const getGrammar = (): Grammar => ({
|
|
|
243
325
|
* than throw. An error is only appropriate when the function would normally
|
|
244
326
|
* return a value, but cannot due to some other failure.
|
|
245
327
|
*/
|
|
246
|
-
functions: {}
|
|
328
|
+
functions: { ...collectionFunctions }
|
|
247
329
|
})
|
package/src/index.ts
CHANGED
|
@@ -4,7 +4,22 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
export { Jexl, default } from './Jexl.ts'
|
|
7
|
+
export type { JexlOptions } from './Jexl.ts'
|
|
8
|
+
export type { GetMember, VariableReader } from './grammar.ts'
|
|
7
9
|
export { default as Expression } from './Expression.ts'
|
|
10
|
+
export { analyze } from './analyze.ts'
|
|
11
|
+
export { check, print, recordOf, union } from './check.ts'
|
|
12
|
+
export {
|
|
13
|
+
callSubject,
|
|
14
|
+
conditions,
|
|
15
|
+
fromConditions,
|
|
16
|
+
pathSubject,
|
|
17
|
+
printCondition
|
|
18
|
+
} from './conditions.ts'
|
|
8
19
|
export { default as Lexer } from './Lexer.ts'
|
|
20
|
+
export { JexlSyntaxError } from './errors.ts'
|
|
9
21
|
export { getGrammar } from './grammar.ts'
|
|
22
|
+
export type * from './analyze.ts'
|
|
23
|
+
export type * from './check.ts'
|
|
24
|
+
export type * from './conditions.ts'
|
|
10
25
|
export type * from './types.ts'
|
package/src/operators.ts
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Jexl
|
|
3
|
+
* Copyright 2020 Tom Shawver
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { JexlValue } from './types.ts'
|
|
7
|
+
|
|
8
|
+
type Test = (left: JexlValue, right: JexlValue) => boolean
|
|
9
|
+
type Arithmetic = (left: JexlValue, right: JexlValue) => JexlValue
|
|
10
|
+
|
|
11
|
+
export function isList(left: JexlValue, right: JexlValue) {
|
|
12
|
+
return Array.isArray(left) || Array.isArray(right)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A comparison over list operands: it holds when it holds for any value, as
|
|
17
|
+
* bcftools compares a multi-valued tag, so `AF > 0.05` is true when any
|
|
18
|
+
* allele's frequency is and `CLNSIG == 'Pathogenic'` when any significance is.
|
|
19
|
+
* Each operator calls this only once {@link isList} says it must, so the
|
|
20
|
+
* comparison of two single values stays a direct call V8 can inline.
|
|
21
|
+
*/
|
|
22
|
+
export function anyPair(
|
|
23
|
+
test: Test,
|
|
24
|
+
left: JexlValue,
|
|
25
|
+
right: JexlValue
|
|
26
|
+
): boolean {
|
|
27
|
+
return Array.isArray(left)
|
|
28
|
+
? left.some((l) => anyPair(test, l, right))
|
|
29
|
+
: Array.isArray(right)
|
|
30
|
+
? right.some((r) => anyPair(test, left, r))
|
|
31
|
+
: test(left, right)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function hasText(value: JexlValue) {
|
|
35
|
+
return (
|
|
36
|
+
typeof value === 'string' ||
|
|
37
|
+
(Array.isArray(value) && value.some((item) => typeof item === 'string'))
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Whether `+` pairs its operands value by value: only when a list is involved
|
|
43
|
+
* and neither side holds text, so `REF + '>' + ALT` still joins as JavaScript
|
|
44
|
+
* joins, `'A>T,C'`, while `DP + 1` adds.
|
|
45
|
+
*/
|
|
46
|
+
export function addsPairwise(left: JexlValue, right: JexlValue) {
|
|
47
|
+
return isList(left, right) && !hasText(left) && !hasText(right)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Arithmetic over list operands, value by value, as bcftools and R apply it:
|
|
52
|
+
* `AC / AN` divides each count, and a list of one, or a lone value, pairs with
|
|
53
|
+
* every value of the other side. Lists of two other lengths have no pairing
|
|
54
|
+
* and yield undefined.
|
|
55
|
+
*/
|
|
56
|
+
export function pairwise(
|
|
57
|
+
fn: Arithmetic,
|
|
58
|
+
left: JexlValue,
|
|
59
|
+
right: JexlValue
|
|
60
|
+
): JexlValue {
|
|
61
|
+
const ls = Array.isArray(left) ? left : [left]
|
|
62
|
+
const rs = Array.isArray(right) ? right : [right]
|
|
63
|
+
if (ls.length !== rs.length && ls.length !== 1 && rs.length !== 1) {
|
|
64
|
+
return undefined
|
|
65
|
+
}
|
|
66
|
+
const length = ls.length === 1 ? rs.length : ls.length
|
|
67
|
+
const out: JexlValue[] = new Array(length)
|
|
68
|
+
for (let i = 0; i < length; i++) {
|
|
69
|
+
out[i] = fn(
|
|
70
|
+
ls.length === 1 ? ls[0] : ls[i],
|
|
71
|
+
rs.length === 1 ? rs[0] : rs[i]
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
return out
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const patterns = new Map<string, RegExp>()
|
|
78
|
+
const MAX_PATTERNS = 1000
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The regular expression a pattern string stands for. A leading `(?i)`, the
|
|
82
|
+
* inline flag PCRE, Python and Go read, makes it case-insensitive.
|
|
83
|
+
*/
|
|
84
|
+
function regex(pattern: string) {
|
|
85
|
+
let re = patterns.get(pattern)
|
|
86
|
+
if (!re) {
|
|
87
|
+
try {
|
|
88
|
+
re = pattern.startsWith('(?i)')
|
|
89
|
+
? new RegExp(pattern.slice(4), 'i')
|
|
90
|
+
: new RegExp(pattern)
|
|
91
|
+
} catch {
|
|
92
|
+
throw new Error(`Invalid regular expression: ${pattern}`)
|
|
93
|
+
}
|
|
94
|
+
if (patterns.size >= MAX_PATTERNS) {
|
|
95
|
+
patterns.clear()
|
|
96
|
+
}
|
|
97
|
+
patterns.set(pattern, re)
|
|
98
|
+
}
|
|
99
|
+
return re
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Whether a value, or any value of a list, matches a pattern string. */
|
|
103
|
+
export function matches(value: JexlValue, pattern: JexlValue): boolean {
|
|
104
|
+
if (typeof pattern !== 'string') {
|
|
105
|
+
return false
|
|
106
|
+
}
|
|
107
|
+
const re = regex(pattern)
|
|
108
|
+
const test = (v: JexlValue) =>
|
|
109
|
+
(typeof v === 'string' ||
|
|
110
|
+
typeof v === 'number' ||
|
|
111
|
+
typeof v === 'boolean') &&
|
|
112
|
+
re.test(String(v))
|
|
113
|
+
return Array.isArray(value) ? value.some(test) : test(value)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Whether a value is in a container: a substring of a string, a member of a
|
|
118
|
+
* list or Set, or a key of a Map or plain object. A list on the left is in
|
|
119
|
+
* the container when any of its values is.
|
|
120
|
+
*/
|
|
121
|
+
export function isIn(left: JexlValue, right: JexlValue): boolean {
|
|
122
|
+
if (Array.isArray(left)) {
|
|
123
|
+
return left.some((l) => isIn(l, right))
|
|
124
|
+
}
|
|
125
|
+
if (typeof right === 'string') {
|
|
126
|
+
// only a primitive has a meaningful substring form. An absent or
|
|
127
|
+
// structured left operand is not "in" a string, and must not be coerced
|
|
128
|
+
// to '' — every string contains the empty string
|
|
129
|
+
return typeof left === 'string' ||
|
|
130
|
+
typeof left === 'number' ||
|
|
131
|
+
typeof left === 'boolean'
|
|
132
|
+
? right.includes(String(left))
|
|
133
|
+
: false
|
|
134
|
+
}
|
|
135
|
+
if (Array.isArray(right)) {
|
|
136
|
+
return right.includes(left)
|
|
137
|
+
}
|
|
138
|
+
if (right instanceof Set || right instanceof Map) {
|
|
139
|
+
return right.has(left)
|
|
140
|
+
}
|
|
141
|
+
if (
|
|
142
|
+
typeof right === 'object' &&
|
|
143
|
+
right !== null &&
|
|
144
|
+
(typeof left === 'string' ||
|
|
145
|
+
typeof left === 'number' ||
|
|
146
|
+
typeof left === 'boolean')
|
|
147
|
+
) {
|
|
148
|
+
return Object.hasOwn(right, String(left))
|
|
149
|
+
}
|
|
150
|
+
return false
|
|
151
|
+
}
|