@infitx/match 0.0.1 → 1.3.2
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/CHANGELOG.md +52 -0
- package/README.md +765 -0
- package/index.js +167 -0
- package/package.json +21 -8
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [1.3.2](https://github.com/infitx-org/release-cd/compare/match-v1.3.1...match-v1.3.2) (2026-02-09)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* update package versions and permissions in workflows ([c582d9f](https://github.com/infitx-org/release-cd/commit/c582d9faff24e6a0549e529608f0ef6a8b362bcf))
|
|
9
|
+
|
|
10
|
+
## [1.3.1](https://github.com/infitx-org/release-cd/compare/match-v1.3.0...match-v1.3.1) (2026-02-09)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* deploy ([9aa2bd6](https://github.com/infitx-org/release-cd/commit/9aa2bd69093d4374f86dd8554a527b627032c326))
|
|
16
|
+
|
|
17
|
+
## [1.3.0](https://github.com/infitx-org/release-cd/compare/match-v1.2.1...match-v1.3.0) (2026-02-09)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### Features
|
|
21
|
+
|
|
22
|
+
* add .npmignore files and ci-publish script for decision and match libraries ([c518708](https://github.com/infitx-org/release-cd/commit/c5187080c63a215a670fcbe37a11989d6ec4c37f))
|
|
23
|
+
|
|
24
|
+
## [1.2.1](https://github.com/infitx-org/release-cd/compare/match-v1.2.0...match-v1.2.1) (2026-02-04)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
### Bug Fixes
|
|
28
|
+
|
|
29
|
+
* enhance match function to support reference time and update test cases ([deb24d0](https://github.com/infitx-org/release-cd/commit/deb24d078f82fdc358b43bf2aa174d537b169c1d))
|
|
30
|
+
|
|
31
|
+
## [1.2.0](https://github.com/infitx-org/release-cd/compare/match-v1.1.1...match-v1.2.0) (2026-01-15)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
### Features
|
|
35
|
+
|
|
36
|
+
* add negation support with 'not' property in match function ([31fddc6](https://github.com/infitx-org/release-cd/commit/31fddc6ae4e7661bf71e37e40490e6baf0ce38aa))
|
|
37
|
+
* add support for Grafana-style time intervals in match function ([ef622bb](https://github.com/infitx-org/release-cd/commit/ef622bba120978b3cf5950a08d34dd5e47dbbea5))
|
|
38
|
+
|
|
39
|
+
## [1.1.1](https://github.com/infitx-org/release-cd/compare/match-v1.1.0...match-v1.1.1) (2025-12-26)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
### Bug Fixes
|
|
43
|
+
|
|
44
|
+
* improve match function for nullish values ([2c5a5c0](https://github.com/infitx-org/release-cd/commit/2c5a5c05ef34ddb013a610ba4357f34185e60eed))
|
|
45
|
+
|
|
46
|
+
## [1.1.0](https://github.com/infitx-org/release-cd/compare/match-v1.0.0...match-v1.1.0) (2025-12-23)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
### Features
|
|
50
|
+
|
|
51
|
+
* add build script to package.json for decision and match libraries ([f4da999](https://github.com/infitx-org/release-cd/commit/f4da9993feec346cc94f573fa103dffa8c4a9eed))
|
|
52
|
+
* match and decision libraries ([db62341](https://github.com/infitx-org/release-cd/commit/db623419a179b3e0ec0cbda05b2f135e01375552))
|
package/README.md
ADDED
|
@@ -0,0 +1,765 @@
|
|
|
1
|
+
# Match Function
|
|
2
|
+
|
|
3
|
+
A flexible matching utility that compares values with advanced semantic rules,
|
|
4
|
+
supporting nested structures, arrays, type coercion, and various matching strategies.
|
|
5
|
+
|
|
6
|
+
## Installation
|
|
7
|
+
|
|
8
|
+
```javascript
|
|
9
|
+
const match = require('./match');
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Basic Usage
|
|
13
|
+
|
|
14
|
+
```javascript
|
|
15
|
+
const match = require('./match');
|
|
16
|
+
|
|
17
|
+
// Exact match
|
|
18
|
+
match({ a: 1 }, { a: 1 }); // true
|
|
19
|
+
match({ a: 1 }, { a: 2 }); // false
|
|
20
|
+
|
|
21
|
+
// Partial object match
|
|
22
|
+
match({ a: 1, b: 2 }, { a: 1 }); // true
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Matching Against Nested Structures
|
|
26
|
+
|
|
27
|
+
The match function recursively compares nested objects:
|
|
28
|
+
|
|
29
|
+
```javascript
|
|
30
|
+
// Nested object matching
|
|
31
|
+
match(
|
|
32
|
+
{ user: { name: 'Alice', age: 30 } },
|
|
33
|
+
{ user: { name: 'Alice' } }
|
|
34
|
+
); // true - partial match on nested object
|
|
35
|
+
|
|
36
|
+
match(
|
|
37
|
+
{ user: { name: 'Alice', age: 30 } },
|
|
38
|
+
{ user: { name: 'Bob' } }
|
|
39
|
+
); // false - name doesn't match
|
|
40
|
+
|
|
41
|
+
// Deep nesting
|
|
42
|
+
match(
|
|
43
|
+
{ a: { b: { c: { d: 'value' } } } },
|
|
44
|
+
{ a: { b: { c: { d: 'value' } } } }
|
|
45
|
+
); // true
|
|
46
|
+
|
|
47
|
+
match(
|
|
48
|
+
{ a: { b: { c: { d: 'value', e: 'extra' } } } },
|
|
49
|
+
{ a: { b: { c: { d: 'value' } } } }
|
|
50
|
+
); // true - extra properties are ignored
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Array Matching (Any-Of Semantics)
|
|
54
|
+
|
|
55
|
+
Arrays implement "any of" semantics - at least one element must match. The
|
|
56
|
+
behavior varies depending on whether arrays appear in the value, the condition,
|
|
57
|
+
or both.
|
|
58
|
+
|
|
59
|
+
### Array in Condition (Rule)
|
|
60
|
+
|
|
61
|
+
When the condition is an array, the value must match **at least one** element
|
|
62
|
+
in the array:
|
|
63
|
+
|
|
64
|
+
```javascript
|
|
65
|
+
// Scalar value against array condition
|
|
66
|
+
match('hello', ['hello', 'world']); // true - matches first element
|
|
67
|
+
match('world', ['hello', 'world']); // true - matches second element
|
|
68
|
+
match('goodbye', ['hello', 'world']); // false - matches none
|
|
69
|
+
|
|
70
|
+
// With numbers
|
|
71
|
+
match(5, [1, 5, 10]); // true
|
|
72
|
+
match(7, [1, 5, 10]); // false
|
|
73
|
+
|
|
74
|
+
// With booleans
|
|
75
|
+
match(true, [true, false]); // true
|
|
76
|
+
match(false, [true]); // false
|
|
77
|
+
|
|
78
|
+
// In nested structures - scalar value against array condition
|
|
79
|
+
match(
|
|
80
|
+
{ status: 'active' },
|
|
81
|
+
{ status: ['active', 'pending', 'processing'] }
|
|
82
|
+
); // true - status matches one of the allowed values
|
|
83
|
+
|
|
84
|
+
match(
|
|
85
|
+
{ status: 'inactive' },
|
|
86
|
+
{ status: ['active', 'pending'] }
|
|
87
|
+
); // false - status doesn't match any allowed value
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Array in Value
|
|
91
|
+
|
|
92
|
+
When the value is an array, **at least one element** must match the condition:
|
|
93
|
+
|
|
94
|
+
```javascript
|
|
95
|
+
// Array value against scalar condition
|
|
96
|
+
match(['apple', 'banana'], 'apple'); // true - first element matches
|
|
97
|
+
match(['apple', 'banana'], 'banana'); // true - second element matches
|
|
98
|
+
match(['apple', 'banana'], 'orange'); // false - no element matches
|
|
99
|
+
|
|
100
|
+
// With numbers
|
|
101
|
+
match([1, 2, 3], 2); // true
|
|
102
|
+
match([1, 2, 3], 5); // false
|
|
103
|
+
|
|
104
|
+
// In nested structures - array value against scalar condition
|
|
105
|
+
match(
|
|
106
|
+
{ tags: ['javascript', 'node', 'async'] },
|
|
107
|
+
{ tags: 'javascript' }
|
|
108
|
+
); // true - at least one tag matches
|
|
109
|
+
|
|
110
|
+
match(
|
|
111
|
+
{ tags: ['python', 'django'] },
|
|
112
|
+
{ tags: 'javascript' }
|
|
113
|
+
); // false - no tag matches
|
|
114
|
+
|
|
115
|
+
// Array value against function condition
|
|
116
|
+
match(
|
|
117
|
+
{ scores: [85, 90, 78] },
|
|
118
|
+
{ scores: (score) => score >= 80 }
|
|
119
|
+
); // true - at least one score is >= 80
|
|
120
|
+
|
|
121
|
+
match(
|
|
122
|
+
{ scores: [65, 70, 75] },
|
|
123
|
+
{ scores: (score) => score >= 80 }
|
|
124
|
+
); // false - no score is >= 80
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Array in Both Value and Condition
|
|
128
|
+
|
|
129
|
+
When both are arrays, a match occurs if **any value element matches any
|
|
130
|
+
condition element**:
|
|
131
|
+
|
|
132
|
+
```javascript
|
|
133
|
+
// Both arrays - cartesian "any of any" matching
|
|
134
|
+
match(['red', 'blue'], ['blue', 'green']); // true - 'blue' appears in both
|
|
135
|
+
match(['red', 'yellow'], ['blue', 'green']); // false - no common elements
|
|
136
|
+
|
|
137
|
+
match([1, 2, 3], [3, 4, 5]); // true - '3' is in both
|
|
138
|
+
match([1, 2], [3, 4]); // false - no overlap
|
|
139
|
+
|
|
140
|
+
// In nested structures
|
|
141
|
+
match(
|
|
142
|
+
{ tags: ['javascript', 'node'] },
|
|
143
|
+
{ tags: ['node', 'async', 'backend'] }
|
|
144
|
+
); // true - 'node' is in both arrays
|
|
145
|
+
|
|
146
|
+
match(
|
|
147
|
+
{ tags: ['python', 'django'] },
|
|
148
|
+
{ tags: ['javascript', 'react'] }
|
|
149
|
+
); // false - no common tags
|
|
150
|
+
|
|
151
|
+
// Complex: array value against array of conditions (including objects)
|
|
152
|
+
match(
|
|
153
|
+
{ priority: [1, 2, 3] },
|
|
154
|
+
{ priority: [{ min: 2, max: 5 }, 10] }
|
|
155
|
+
); // true - elements 2 and 3 match the range { min: 2, max: 5 }
|
|
156
|
+
|
|
157
|
+
match(
|
|
158
|
+
{ priority: [1] },
|
|
159
|
+
{ priority: [{ min: 2, max: 5 }, 10] }
|
|
160
|
+
); // false - 1 doesn't match range or 10
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Array Value Against Object Condition
|
|
164
|
+
|
|
165
|
+
When an array value is matched against an object condition (like a range or
|
|
166
|
+
complex object), each element is tested against the condition:
|
|
167
|
+
|
|
168
|
+
```javascript
|
|
169
|
+
// Array value against range condition
|
|
170
|
+
match(
|
|
171
|
+
[1, 5, 10],
|
|
172
|
+
{ min: 3, max: 7 }
|
|
173
|
+
); // true - element '5' falls within the range
|
|
174
|
+
|
|
175
|
+
match(
|
|
176
|
+
[1, 2],
|
|
177
|
+
{ min: 3, max: 7 }
|
|
178
|
+
); // false - no element falls within the range
|
|
179
|
+
|
|
180
|
+
// In nested structures
|
|
181
|
+
match(
|
|
182
|
+
{ scores: [45, 67, 89, 92] },
|
|
183
|
+
{ scores: { min: 80 } }
|
|
184
|
+
); // true - elements 89 and 92 are >= 80
|
|
185
|
+
|
|
186
|
+
match(
|
|
187
|
+
{ scores: [45, 67, 75] },
|
|
188
|
+
{ scores: { min: 80 } }
|
|
189
|
+
); // false - no score is >= 80
|
|
190
|
+
|
|
191
|
+
// Array value against regex condition
|
|
192
|
+
match(
|
|
193
|
+
{ emails: ['admin@test.com', 'invalid', 'user@test.com'] },
|
|
194
|
+
{ emails: /@test\.com$/ }
|
|
195
|
+
); // true - at least one email matches the pattern
|
|
196
|
+
|
|
197
|
+
match(
|
|
198
|
+
{ emails: ['invalid1', 'invalid2'] },
|
|
199
|
+
{ emails: /@test\.com$/ }
|
|
200
|
+
); // false - no email matches
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Combining Arrays with Null
|
|
204
|
+
|
|
205
|
+
Arrays can include `null` to make conditions optional:
|
|
206
|
+
|
|
207
|
+
```javascript
|
|
208
|
+
// Array with null - matches if property is missing OR matches other values
|
|
209
|
+
match(
|
|
210
|
+
{ status: 'active' },
|
|
211
|
+
{ status: [null, 'active'] }
|
|
212
|
+
); // true - matches 'active'
|
|
213
|
+
|
|
214
|
+
match(
|
|
215
|
+
{ name: 'Alice' },
|
|
216
|
+
{ status: [null, 'active'] }
|
|
217
|
+
); // true - 'status' is missing, matches null
|
|
218
|
+
|
|
219
|
+
match(
|
|
220
|
+
{ status: false },
|
|
221
|
+
{ status: [null, 'active'] }
|
|
222
|
+
); // false - false is neither null nor 'active'
|
|
223
|
+
|
|
224
|
+
match(
|
|
225
|
+
{ status: undefined },
|
|
226
|
+
{ status: [null, 'active'] }
|
|
227
|
+
); // true - undefined treated as null
|
|
228
|
+
|
|
229
|
+
// Nested with arrays and null
|
|
230
|
+
match(
|
|
231
|
+
{ user: { role: 'admin' } },
|
|
232
|
+
{ user: { role: [null, 'admin', 'moderator'] } }
|
|
233
|
+
); // true - role matches 'admin'
|
|
234
|
+
|
|
235
|
+
match(
|
|
236
|
+
{ user: { name: 'Alice' } },
|
|
237
|
+
{ user: { role: [null, 'admin'], name: 'Alice' } }
|
|
238
|
+
); // true - role is missing (matches null), name matches
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### Array Behavior Summary
|
|
242
|
+
|
|
243
|
+
| Value Type | Condition Type | Behavior |
|
|
244
|
+
| ---------- | -------------------------- | --------------------------------------------- |
|
|
245
|
+
| Scalar | Array | Value must match at least one array element |
|
|
246
|
+
| Array | Scalar | At least one value must match the scalar |
|
|
247
|
+
| Array | Array | At least one value must match one condition |
|
|
248
|
+
| Array | Object (range/complex) | At least one value must match the condition |
|
|
249
|
+
| Array | Function | At least one value must satisfy the function |
|
|
250
|
+
|
|
251
|
+
## Matching Against Non-Existing Properties
|
|
252
|
+
|
|
253
|
+
The match function has special handling for `null` values to match against
|
|
254
|
+
non-existing properties:
|
|
255
|
+
|
|
256
|
+
```javascript
|
|
257
|
+
// null matches undefined/missing properties
|
|
258
|
+
match({ b: 0 }, { a: null }); // true - 'a' is missing
|
|
259
|
+
|
|
260
|
+
match({ a: null }, { a: null }); // true - both null
|
|
261
|
+
|
|
262
|
+
match({ a: undefined }, { a: null }); // true - undefined treated as null
|
|
263
|
+
|
|
264
|
+
match({ a: false }, { a: null }); // false - false is not null
|
|
265
|
+
|
|
266
|
+
match({ a: 0 }, { a: null }); // false - 0 is not null
|
|
267
|
+
|
|
268
|
+
match({ a: '' }, { a: null }); // false - empty string is not null
|
|
269
|
+
|
|
270
|
+
// Nested non-existing properties
|
|
271
|
+
match(
|
|
272
|
+
{ a: {} },
|
|
273
|
+
{ a: { b: null } }
|
|
274
|
+
); // true - 'b' doesn't exist in nested object
|
|
275
|
+
|
|
276
|
+
match(
|
|
277
|
+
{ a: { b: false } },
|
|
278
|
+
{ a: { b: null } }
|
|
279
|
+
); // false - 'b' exists and is false
|
|
280
|
+
|
|
281
|
+
// Array with null for optional properties
|
|
282
|
+
match(
|
|
283
|
+
{ status: 'active' },
|
|
284
|
+
{ status: [null, 'active'] }
|
|
285
|
+
); // true - matches 'active'
|
|
286
|
+
|
|
287
|
+
match(
|
|
288
|
+
{ name: 'Alice' },
|
|
289
|
+
{ status: [null, 'active'] }
|
|
290
|
+
); // true - 'status' is missing, matches null
|
|
291
|
+
|
|
292
|
+
match(
|
|
293
|
+
{ status: false },
|
|
294
|
+
{ status: [null, 'active'] }
|
|
295
|
+
); // false - false doesn't match null or 'active'
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
## Type Coercion
|
|
299
|
+
|
|
300
|
+
The function coerces values to match the type of the rule:
|
|
301
|
+
|
|
302
|
+
```javascript
|
|
303
|
+
// Boolean coercion
|
|
304
|
+
match('hello', true); // true - truthy string
|
|
305
|
+
match('', false); // true - falsy empty string
|
|
306
|
+
match(0, false); // true - falsy zero
|
|
307
|
+
match(1, true); // true - truthy number
|
|
308
|
+
|
|
309
|
+
// String coercion
|
|
310
|
+
match(123, '123'); // true
|
|
311
|
+
match(true, 'true'); // true
|
|
312
|
+
|
|
313
|
+
// Number coercion
|
|
314
|
+
match('42', 42); // true
|
|
315
|
+
match('3.14', 3.14); // true
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
## Range Matching
|
|
319
|
+
|
|
320
|
+
Use `min` and `max` for numeric and date range matching:
|
|
321
|
+
|
|
322
|
+
```javascript
|
|
323
|
+
// Numeric ranges
|
|
324
|
+
match(5, { min: 1, max: 10 }); // true
|
|
325
|
+
match(15, { min: 1, max: 10 }); // false
|
|
326
|
+
match(1, { min: 1 }); // true - only minimum
|
|
327
|
+
match(10, { max: 10 }); // true - only maximum
|
|
328
|
+
|
|
329
|
+
// Date ranges
|
|
330
|
+
const now = new Date('2025-06-15');
|
|
331
|
+
const start = new Date('2025-01-01');
|
|
332
|
+
const end = new Date('2025-12-31');
|
|
333
|
+
|
|
334
|
+
match(now, { min: start, max: end }); // true
|
|
335
|
+
match(new Date('2026-01-01'), { min: start, max: end }); // false
|
|
336
|
+
|
|
337
|
+
// Nested range matching
|
|
338
|
+
match(
|
|
339
|
+
{ user: { age: 25 } },
|
|
340
|
+
{ user: { age: { min: 18, max: 65 } } }
|
|
341
|
+
); // true
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
### Grafana-Style Time Intervals
|
|
345
|
+
|
|
346
|
+
The `min` and `max` properties support Grafana-style relative time intervals
|
|
347
|
+
for convenient time-based matching:
|
|
348
|
+
|
|
349
|
+
```javascript
|
|
350
|
+
// Current time
|
|
351
|
+
match(new Date(), { min: 'now-1h', max: 'now' });
|
|
352
|
+
// true - current time is within the last hour
|
|
353
|
+
|
|
354
|
+
// Future time check
|
|
355
|
+
match(new Date(), { max: 'now+1d' });
|
|
356
|
+
// true - current time is before tomorrow
|
|
357
|
+
|
|
358
|
+
// Past time check
|
|
359
|
+
match(new Date(), { min: 'now-1w' });
|
|
360
|
+
// true - current time is within the last week
|
|
361
|
+
|
|
362
|
+
// Time range
|
|
363
|
+
match(
|
|
364
|
+
new Date(),
|
|
365
|
+
{ min: 'now-1d', max: 'now+1d' }
|
|
366
|
+
); // true - current time is within ±1 day
|
|
367
|
+
|
|
368
|
+
// In nested structures
|
|
369
|
+
match(
|
|
370
|
+
{ event: { timestamp: new Date() } },
|
|
371
|
+
{ event: { timestamp: { min: 'now-5m' } } }
|
|
372
|
+
); // true - event occurred within the last 5 minutes
|
|
373
|
+
|
|
374
|
+
match(
|
|
375
|
+
{ event: { timestamp: new Date(Date.now() - 10 * 60 * 1000) } },
|
|
376
|
+
{ event: { timestamp: { min: 'now-5m' } } }
|
|
377
|
+
); // false - event occurred more than 5 minutes ago
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
#### Supported Time Units
|
|
381
|
+
|
|
382
|
+
| Unit | Description | Example |
|
|
383
|
+
| ---- | -------------- | ----------- |
|
|
384
|
+
| `ms` | Milliseconds | `now-500ms` |
|
|
385
|
+
| `s` | Seconds | `now-30s` |
|
|
386
|
+
| `m` | Minutes | `now-5m` |
|
|
387
|
+
| `h` | Hours | `now-2h` |
|
|
388
|
+
| `d` | Days | `now-7d` |
|
|
389
|
+
| `w` | Weeks | `now-2w` |
|
|
390
|
+
| `M` | Months (30d) | `now-3M` |
|
|
391
|
+
| `y` | Years (365d) | `now-1y` |
|
|
392
|
+
|
|
393
|
+
#### Rounding Units
|
|
394
|
+
|
|
395
|
+
When using the `/` operator, you can round to these time units:
|
|
396
|
+
|
|
397
|
+
| Unit | Rounds To | Example |
|
|
398
|
+
| ---- | -------------------- | -------------------------------- |
|
|
399
|
+
| `s` | Start of second | `now/s` = current second at .000 |
|
|
400
|
+
| `m` | Start of minute | `now/m` = current minute at :00 |
|
|
401
|
+
| `h` | Start of hour | `now/h` = current hour at :00:00 |
|
|
402
|
+
| `d` | Start of day | `now/d` = today at 00:00:00 |
|
|
403
|
+
| `w` | Start of week | `now/w` = Monday at 00:00:00 |
|
|
404
|
+
| `M` | Start of month | `now/M` = 1st of month 00:00:00 |
|
|
405
|
+
| `y` | Start of year | `now/y` = Jan 1st at 00:00:00 |
|
|
406
|
+
|
|
407
|
+
**Note**: Week rounding always rounds to Monday as the first day of the week.
|
|
408
|
+
|
|
409
|
+
#### Time Interval Format
|
|
410
|
+
|
|
411
|
+
- **`now`**: Current time
|
|
412
|
+
- **`now-<amount><unit>`**: Time in the past (e.g., `now-5m` = 5 minutes ago)
|
|
413
|
+
- **`now+<amount><unit>`**: Time in the future (e.g., `now+1h` = 1 hour from now)
|
|
414
|
+
- **`now/<unit>`**: Current time rounded to start of unit (e.g., `now/d` = start of today)
|
|
415
|
+
- **`now[+-]<amount><unit>/<roundUnit>`**: Time with offset, rounded (e.g., `now-5d/d` = 5 days ago at midnight)
|
|
416
|
+
|
|
417
|
+
#### Time Rounding
|
|
418
|
+
|
|
419
|
+
The `/` operator rounds timestamps to the start of the specified time unit, following Grafana's approach:
|
|
420
|
+
|
|
421
|
+
```javascript
|
|
422
|
+
// Round to start of current day (midnight)
|
|
423
|
+
match(new Date('2025-06-15T14:30:00'), { min: 'now/d', max: 'now' });
|
|
424
|
+
// Compares against start of current day
|
|
425
|
+
|
|
426
|
+
// Round to start of current hour
|
|
427
|
+
match(new Date(), { min: 'now/h' });
|
|
428
|
+
// Matches times from the start of the current hour
|
|
429
|
+
|
|
430
|
+
// Combine offset and rounding
|
|
431
|
+
// Example: 5 days ago, rounded to midnight of that day
|
|
432
|
+
match(new Date(), { min: 'now-5d/d' });
|
|
433
|
+
|
|
434
|
+
// Week rounding (rounds to Monday)
|
|
435
|
+
match(new Date(), { min: 'now/w' });
|
|
436
|
+
// Matches times from the start of the current week
|
|
437
|
+
|
|
438
|
+
// Month rounding
|
|
439
|
+
match(new Date(), { min: 'now-1M/M' });
|
|
440
|
+
// Matches times from the start of last month
|
|
441
|
+
|
|
442
|
+
// Rounding units: s (second), m (minute), h (hour), d (day),
|
|
443
|
+
// w (week), M (month), y (year)
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
```javascript
|
|
447
|
+
// Examples with different units
|
|
448
|
+
match(new Date(), { min: 'now-500ms' }); // Last 500 milliseconds
|
|
449
|
+
match(new Date(), { min: 'now-30s' }); // Last 30 seconds
|
|
450
|
+
match(new Date(), { min: 'now-5m' }); // Last 5 minutes
|
|
451
|
+
match(new Date(), { min: 'now-2h' }); // Last 2 hours
|
|
452
|
+
match(new Date(), { min: 'now-7d' }); // Last 7 days
|
|
453
|
+
match(new Date(), { min: 'now-2w' }); // Last 2 weeks
|
|
454
|
+
match(new Date(), { min: 'now-3M' }); // Last 3 months (approx)
|
|
455
|
+
match(new Date(), { min: 'now-1y' }); // Last year (approx)
|
|
456
|
+
|
|
457
|
+
// Future times
|
|
458
|
+
match(new Date(), { max: 'now+1h' }); // Within next hour
|
|
459
|
+
match(new Date(), { max: 'now+7d' }); // Within next 7 days
|
|
460
|
+
|
|
461
|
+
// Examples with rounding
|
|
462
|
+
match(new Date(), { min: 'now/d' }); // Since midnight today
|
|
463
|
+
match(new Date(), { min: 'now-7d/d', max: 'now/d' }); // Last 7 full days
|
|
464
|
+
match(new Date(), { min: 'now/M' }); // Since start of current month
|
|
465
|
+
match(new Date(), { min: 'now-1y/y', max: 'now/y' }); // Last full year
|
|
466
|
+
|
|
467
|
+
// Practical examples
|
|
468
|
+
// Check if log entry is recent (last 15 minutes)
|
|
469
|
+
match(
|
|
470
|
+
{ log: { timestamp: new Date() } },
|
|
471
|
+
{ log: { timestamp: { min: 'now-15m' } } }
|
|
472
|
+
); // true
|
|
473
|
+
|
|
474
|
+
// Check if event occurred today (since midnight)
|
|
475
|
+
match(
|
|
476
|
+
{ event: { timestamp: new Date() } },
|
|
477
|
+
{ event: { timestamp: { min: 'now/d' } } }
|
|
478
|
+
); // true if event is from today
|
|
479
|
+
|
|
480
|
+
// Check if data is from the current month
|
|
481
|
+
match(
|
|
482
|
+
{ report: { date: new Date() } },
|
|
483
|
+
{ report: { date: { min: 'now/M', max: 'now' } } }
|
|
484
|
+
); // true if report is from current month
|
|
485
|
+
|
|
486
|
+
// Check if scheduled event is upcoming (next 24 hours)
|
|
487
|
+
match(
|
|
488
|
+
{ event: { scheduledAt: new Date(Date.now() + 12 * 60 * 60 * 1000) } },
|
|
489
|
+
{ event: { scheduledAt: { min: 'now', max: 'now+1d' } } }
|
|
490
|
+
); // true
|
|
491
|
+
|
|
492
|
+
// Check if user session is still valid (created within last 30 minutes)
|
|
493
|
+
match(
|
|
494
|
+
{ session: { createdAt: new Date(Date.now() - 10 * 60 * 1000) } },
|
|
495
|
+
{ session: { createdAt: { min: 'now-30m' } } }
|
|
496
|
+
); // true
|
|
497
|
+
|
|
498
|
+
// Daily reports: match events from start of day until now
|
|
499
|
+
match(
|
|
500
|
+
{ log: { timestamp: new Date() } },
|
|
501
|
+
{ log: { timestamp: { min: 'now/d', max: 'now' } } }
|
|
502
|
+
); // Matches anything that happened today
|
|
503
|
+
|
|
504
|
+
// Weekly reports: last 7 complete days
|
|
505
|
+
match(
|
|
506
|
+
{ metric: { recorded: new Date() } },
|
|
507
|
+
{ metric: { recorded: { min: 'now-7d/d', max: 'now/d' } } }
|
|
508
|
+
); // Matches data from last 7 full days (midnight to midnight)
|
|
509
|
+
|
|
510
|
+
// Business hours check: events during current hour
|
|
511
|
+
match(
|
|
512
|
+
{ transaction: { timestamp: new Date() } },
|
|
513
|
+
{ transaction: { timestamp: { min: 'now/h', max: 'now' } } }
|
|
514
|
+
); // Matches transactions from start of current hour
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
## Function Predicates
|
|
518
|
+
|
|
519
|
+
Use functions for custom matching logic:
|
|
520
|
+
|
|
521
|
+
```javascript
|
|
522
|
+
// Function as rule
|
|
523
|
+
match(10, (value) => value > 5); // true
|
|
524
|
+
match(3, (value) => value > 5); // false
|
|
525
|
+
|
|
526
|
+
// Complex predicate
|
|
527
|
+
match(
|
|
528
|
+
'hello@example.com',
|
|
529
|
+
(value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
|
|
530
|
+
); // true - valid email
|
|
531
|
+
|
|
532
|
+
// Function in nested structure
|
|
533
|
+
match(
|
|
534
|
+
{ user: { age: 25 } },
|
|
535
|
+
{ user: { age: (age) => age >= 18 } }
|
|
536
|
+
); // true
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
## Negation with `not`
|
|
540
|
+
|
|
541
|
+
Use the `not` property in an object condition to negate any match:
|
|
542
|
+
|
|
543
|
+
```javascript
|
|
544
|
+
// Basic negation
|
|
545
|
+
match(3, { not: 5 }); // true - 3 is not 5
|
|
546
|
+
match(5, { not: 5 }); // false - 5 is 5
|
|
547
|
+
|
|
548
|
+
// Negate string match
|
|
549
|
+
match('goodbye', { not: 'hello' }); // true
|
|
550
|
+
match('hello', { not: 'hello' }); // false
|
|
551
|
+
|
|
552
|
+
// Negate boolean
|
|
553
|
+
match(false, { not: true }); // true
|
|
554
|
+
match(true, { not: true }); // false
|
|
555
|
+
match(0, { not: true }); // true - 0 is falsy, not true
|
|
556
|
+
match(1, { not: true }); // false - 1 coerces to true
|
|
557
|
+
|
|
558
|
+
// Negate null
|
|
559
|
+
match(0, { not: null }); // true - 0 is not null
|
|
560
|
+
match(null, { not: null }); // false
|
|
561
|
+
match(undefined, { not: null }); // false - undefined treated as null
|
|
562
|
+
|
|
563
|
+
// In nested structures
|
|
564
|
+
match(
|
|
565
|
+
{ status: 'inactive' },
|
|
566
|
+
{ status: { not: 'active' } }
|
|
567
|
+
); // true
|
|
568
|
+
|
|
569
|
+
match(
|
|
570
|
+
{ status: 'active' },
|
|
571
|
+
{ status: { not: 'active' } }
|
|
572
|
+
); // false
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
### Negating Complex Conditions
|
|
576
|
+
|
|
577
|
+
The `not` property works with all match types including arrays, ranges,
|
|
578
|
+
functions, and regex:
|
|
579
|
+
|
|
580
|
+
```javascript
|
|
581
|
+
// Negate regex
|
|
582
|
+
match('goodbye', { not: /hello/ }); // true
|
|
583
|
+
match('hello world', { not: /hello/ }); // false
|
|
584
|
+
|
|
585
|
+
match(
|
|
586
|
+
{ email: 'user@domain.org' },
|
|
587
|
+
{ email: { not: /@example\.com$/ } }
|
|
588
|
+
); // true - doesn't end with @example.com
|
|
589
|
+
|
|
590
|
+
// Negate array (none of)
|
|
591
|
+
match(5, { not: [1, 2, 3] }); // true - 5 is not in the list
|
|
592
|
+
match(2, { not: [1, 2, 3] }); // false - 2 is in the list
|
|
593
|
+
|
|
594
|
+
match(
|
|
595
|
+
{ role: 'guest' },
|
|
596
|
+
{ role: { not: ['admin', 'moderator'] } }
|
|
597
|
+
); // true - guest is neither admin nor moderator
|
|
598
|
+
|
|
599
|
+
match(
|
|
600
|
+
{ role: 'admin' },
|
|
601
|
+
{ role: { not: ['admin', 'moderator'] } }
|
|
602
|
+
); // false - admin is in the list
|
|
603
|
+
|
|
604
|
+
// Negate range
|
|
605
|
+
match(3, { not: { min: 5, max: 10 } }); // true - 3 is outside the range
|
|
606
|
+
match(7, { not: { min: 5, max: 10 } }); // false - 7 is in the range
|
|
607
|
+
match(15, { not: { min: 5, max: 10 } }); // true - 15 is outside the range
|
|
608
|
+
|
|
609
|
+
match(
|
|
610
|
+
{ age: 15 },
|
|
611
|
+
{ age: { not: { min: 18, max: 65 } } }
|
|
612
|
+
); // true - age is below minimum
|
|
613
|
+
|
|
614
|
+
match(
|
|
615
|
+
{ age: 25 },
|
|
616
|
+
{ age: { not: { min: 18, max: 65 } } }
|
|
617
|
+
); // false - age is in range
|
|
618
|
+
|
|
619
|
+
// Negate function predicate
|
|
620
|
+
match(3, { not: (v) => v > 5 }); // true - 3 is not > 5
|
|
621
|
+
match(10, { not: (v) => v > 5 }); // false - 10 is > 5
|
|
622
|
+
|
|
623
|
+
match(
|
|
624
|
+
{ price: 25 },
|
|
625
|
+
{ price: { not: (p) => p >= 100 } }
|
|
626
|
+
); // true - price is not >= 100
|
|
627
|
+
|
|
628
|
+
// Negate object match
|
|
629
|
+
match(
|
|
630
|
+
{ user: { role: 'guest' } },
|
|
631
|
+
{ user: { not: { role: 'admin' } } }
|
|
632
|
+
); // true - role is not admin
|
|
633
|
+
|
|
634
|
+
match(
|
|
635
|
+
{ user: { role: 'admin' } },
|
|
636
|
+
{ user: { not: { role: 'admin' } } }
|
|
637
|
+
); // false - role is admin
|
|
638
|
+
|
|
639
|
+
// Complex negation in nested structures
|
|
640
|
+
match(
|
|
641
|
+
{
|
|
642
|
+
user: {
|
|
643
|
+
email: 'user@custom.com',
|
|
644
|
+
role: 'user'
|
|
645
|
+
}
|
|
646
|
+
},
|
|
647
|
+
{
|
|
648
|
+
user: {
|
|
649
|
+
email: { not: /@example\.com$/ },
|
|
650
|
+
role: { not: ['admin', 'moderator'] }
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
); // true - email doesn't end with @example.com and role is not admin/moderator
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
### Combining `not` with Other Conditions
|
|
657
|
+
|
|
658
|
+
You can combine `not` with other conditions in complex matching scenarios:
|
|
659
|
+
|
|
660
|
+
```javascript
|
|
661
|
+
// Exclude certain values while checking other properties
|
|
662
|
+
match(
|
|
663
|
+
{ status: 'pending', priority: 2 },
|
|
664
|
+
{
|
|
665
|
+
status: { not: ['cancelled', 'completed'] },
|
|
666
|
+
priority: { min: 1, max: 3 }
|
|
667
|
+
}
|
|
668
|
+
); // true - status is not cancelled/completed and priority is in range
|
|
669
|
+
|
|
670
|
+
// Array values with negation
|
|
671
|
+
match(
|
|
672
|
+
{ tags: ['javascript', 'backend'] },
|
|
673
|
+
{ tags: { not: 'frontend' } }
|
|
674
|
+
); // true - none of the tags are 'frontend'
|
|
675
|
+
|
|
676
|
+
match(
|
|
677
|
+
{ tags: ['javascript', 'frontend'] },
|
|
678
|
+
{ tags: { not: 'frontend' } }
|
|
679
|
+
); // false - 'frontend' is in the tags
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
## Regular Expression Matching
|
|
683
|
+
|
|
684
|
+
```javascript
|
|
685
|
+
// Regex patterns
|
|
686
|
+
match('hello world', /hello/); // true
|
|
687
|
+
match('goodbye world', /hello/); // false
|
|
688
|
+
|
|
689
|
+
// Case-insensitive matching
|
|
690
|
+
match('Hello World', /hello/i); // true
|
|
691
|
+
|
|
692
|
+
// Nested regex
|
|
693
|
+
match(
|
|
694
|
+
{ email: 'user@example.com' },
|
|
695
|
+
{ email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/ }
|
|
696
|
+
); // true
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
## Complex Examples
|
|
700
|
+
|
|
701
|
+
Combining multiple matching strategies:
|
|
702
|
+
|
|
703
|
+
```javascript
|
|
704
|
+
// Complex nested structure with arrays and nulls
|
|
705
|
+
match(
|
|
706
|
+
{
|
|
707
|
+
user: {
|
|
708
|
+
name: 'Alice',
|
|
709
|
+
email: 'alice@example.com',
|
|
710
|
+
roles: ['admin', 'user']
|
|
711
|
+
},
|
|
712
|
+
status: 'active'
|
|
713
|
+
},
|
|
714
|
+
{
|
|
715
|
+
user: {
|
|
716
|
+
email: /@example\.com$/,
|
|
717
|
+
roles: 'admin'
|
|
718
|
+
},
|
|
719
|
+
status: ['active', 'pending'],
|
|
720
|
+
lastLogin: null // optional field
|
|
721
|
+
}
|
|
722
|
+
); // true
|
|
723
|
+
|
|
724
|
+
// Multiple conditions
|
|
725
|
+
match(
|
|
726
|
+
{ price: 50, category: 'electronics', inStock: true },
|
|
727
|
+
{
|
|
728
|
+
price: { min: 0, max: 100 },
|
|
729
|
+
category: ['electronics', 'computers'],
|
|
730
|
+
inStock: true,
|
|
731
|
+
discount: null // discount is optional
|
|
732
|
+
}
|
|
733
|
+
); // true
|
|
734
|
+
```
|
|
735
|
+
|
|
736
|
+
## API
|
|
737
|
+
|
|
738
|
+
### `match(factValue, ruleValue)`
|
|
739
|
+
|
|
740
|
+
Compares a fact value against a rule value with flexible matching semantics.
|
|
741
|
+
|
|
742
|
+
**Parameters:**
|
|
743
|
+
|
|
744
|
+
- `factValue` (any): The actual value to test
|
|
745
|
+
- `ruleValue` (any): The pattern/rule to match against
|
|
746
|
+
|
|
747
|
+
**Returns:**
|
|
748
|
+
|
|
749
|
+
- `boolean`: `true` if the fact matches the rule, `false` otherwise
|
|
750
|
+
|
|
751
|
+
**Matching Rules:**
|
|
752
|
+
|
|
753
|
+
- **Exact equality**: Returns `true` if values are strictly equal
|
|
754
|
+
- **Null handling**: `null` in rule matches `null` or `undefined` in fact
|
|
755
|
+
- **Arrays**: "Any of" semantics - at least one element must match
|
|
756
|
+
- **Objects**: Recursively matches properties (partial matching allowed)
|
|
757
|
+
- **Type coercion**: Values are coerced to match rule type
|
|
758
|
+
- **Ranges**: Objects with `min`/`max` properties enable range matching
|
|
759
|
+
- **Negation**: Objects with `not` property negate any match condition
|
|
760
|
+
- **Functions**: Rule functions are called with fact value as predicate
|
|
761
|
+
- **RegExp**: Tests string values against regex patterns
|
|
762
|
+
|
|
763
|
+
## License
|
|
764
|
+
|
|
765
|
+
See the main project license.
|
package/index.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
const isMatchWith = require('lodash/isMatchWith');
|
|
2
|
+
const isPlainObject = require('lodash/isPlainObject');
|
|
3
|
+
|
|
4
|
+
// Round a date to the start of a time unit
|
|
5
|
+
function roundToUnit(date, unit) {
|
|
6
|
+
const d = new Date(date);
|
|
7
|
+
|
|
8
|
+
switch (unit) {
|
|
9
|
+
case 's': // Round to start of second
|
|
10
|
+
d.setMilliseconds(0);
|
|
11
|
+
break;
|
|
12
|
+
case 'm': // Round to start of minute
|
|
13
|
+
d.setSeconds(0, 0);
|
|
14
|
+
break;
|
|
15
|
+
case 'h': // Round to start of hour
|
|
16
|
+
d.setMinutes(0, 0, 0);
|
|
17
|
+
break;
|
|
18
|
+
case 'd': // Round to start of day
|
|
19
|
+
d.setHours(0, 0, 0, 0);
|
|
20
|
+
break;
|
|
21
|
+
case 'w': // Round to start of week (Monday)
|
|
22
|
+
d.setHours(0, 0, 0, 0);
|
|
23
|
+
const day = d.getDay();
|
|
24
|
+
const diff = day === 0 ? 6 : day - 1; // Monday is 1, Sunday is 0
|
|
25
|
+
d.setDate(d.getDate() - diff);
|
|
26
|
+
break;
|
|
27
|
+
case 'M': // Round to start of month
|
|
28
|
+
d.setDate(1);
|
|
29
|
+
d.setHours(0, 0, 0, 0);
|
|
30
|
+
break;
|
|
31
|
+
case 'y': // Round to start of year
|
|
32
|
+
d.setMonth(0, 1);
|
|
33
|
+
d.setHours(0, 0, 0, 0);
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return d;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Parse Grafana-style time intervals (now, now-5m, now+1h, now/d, now-5d/d, etc.)
|
|
41
|
+
function parseTimeInterval(str, referenceTime) {
|
|
42
|
+
if (typeof str !== 'string') return null;
|
|
43
|
+
|
|
44
|
+
// Handle "now" without modifiers
|
|
45
|
+
if (str === 'now') return new Date(referenceTime);
|
|
46
|
+
|
|
47
|
+
// Handle "now/unit" (rounding without offset)
|
|
48
|
+
const roundOnlyMatch = /^now\/([smhdwMy])$/.exec(str);
|
|
49
|
+
if (roundOnlyMatch) {
|
|
50
|
+
const [, unit] = roundOnlyMatch;
|
|
51
|
+
return roundToUnit(new Date(referenceTime), unit);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Handle "now[+-]amount(unit)" or "now[+-]amount(unit)/roundUnit"
|
|
55
|
+
const match = /^now([+-])(\d+)(ms|s|m|h|d|w|M|y)(?:\/([smhdwMy]))?$/.exec(str);
|
|
56
|
+
if (!match) return null;
|
|
57
|
+
|
|
58
|
+
const [, sign, amount, unit, roundUnit] = match;
|
|
59
|
+
const value = parseInt(amount, 10);
|
|
60
|
+
const multiplier = sign === '+' ? 1 : -1;
|
|
61
|
+
|
|
62
|
+
// Convert to milliseconds
|
|
63
|
+
const unitMultipliers = {
|
|
64
|
+
ms: 1,
|
|
65
|
+
s: 1000,
|
|
66
|
+
m: 60 * 1000,
|
|
67
|
+
h: 60 * 60 * 1000,
|
|
68
|
+
d: 24 * 60 * 60 * 1000,
|
|
69
|
+
w: 7 * 24 * 60 * 60 * 1000,
|
|
70
|
+
M: 30 * 24 * 60 * 60 * 1000, // Approximate month
|
|
71
|
+
y: 365 * 24 * 60 * 60 * 1000 // Approximate year
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const offset = multiplier * value * unitMultipliers[unit];
|
|
75
|
+
console.log({ referenceTime, offset });
|
|
76
|
+
const resultTime = new Date(referenceTime + offset);
|
|
77
|
+
|
|
78
|
+
// Apply rounding if specified
|
|
79
|
+
if (roundUnit) {
|
|
80
|
+
return roundToUnit(resultTime, roundUnit);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return resultTime;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const match = (referenceTime) => (value, condition) => {
|
|
87
|
+
if (value == null && condition == null) {
|
|
88
|
+
return true
|
|
89
|
+
} else if (value === condition) {
|
|
90
|
+
return true;
|
|
91
|
+
} else if (Array.isArray(condition) || Array.isArray(value)) {
|
|
92
|
+
return Array.isArray(value)
|
|
93
|
+
? value.some(v => module.exports(v, condition, referenceTime))
|
|
94
|
+
: condition.some(v => module.exports(value, v, referenceTime));
|
|
95
|
+
} else if (value == null || condition == null) {
|
|
96
|
+
return false;
|
|
97
|
+
} else if (condition instanceof RegExp) {
|
|
98
|
+
if (typeof value === 'string') return condition.test(value);
|
|
99
|
+
} else if (condition instanceof Date) {
|
|
100
|
+
value = value instanceof Date ? value.getTime() : new Date(value).getTime();
|
|
101
|
+
condition = condition.getTime();
|
|
102
|
+
if (!Number.isFinite(value) || !Number.isFinite(condition)) return false;
|
|
103
|
+
return value === condition;
|
|
104
|
+
} else if (Number.isNaN(value) || Number.isNaN(condition)) return false;
|
|
105
|
+
switch (typeof condition) {
|
|
106
|
+
case 'boolean':
|
|
107
|
+
return Boolean(value) === condition;
|
|
108
|
+
case 'string':
|
|
109
|
+
return String(value) === condition;
|
|
110
|
+
case 'number':
|
|
111
|
+
return Number(value) === condition;
|
|
112
|
+
case 'function':
|
|
113
|
+
return condition(value);
|
|
114
|
+
case 'object': {
|
|
115
|
+
let { min, max, not } = condition;
|
|
116
|
+
if (not != null) return !module.exports(value, not, referenceTime);
|
|
117
|
+
|
|
118
|
+
// Parse Grafana-style time intervals for min and max
|
|
119
|
+
if (typeof min === 'string') {
|
|
120
|
+
const parsed = parseTimeInterval(min, referenceTime);
|
|
121
|
+
if (parsed) min = parsed;
|
|
122
|
+
}
|
|
123
|
+
if (typeof max === 'string') {
|
|
124
|
+
const parsed = parseTimeInterval(max, referenceTime);
|
|
125
|
+
if (parsed) max = parsed;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
console.log({ min, max, value });
|
|
129
|
+
if (value instanceof Date) {
|
|
130
|
+
value = value.getTime();
|
|
131
|
+
if (!Number.isFinite(value)) return false;
|
|
132
|
+
if (min != null) min = new Date(min).getTime();
|
|
133
|
+
if (max != null) max = new Date(max).getTime();
|
|
134
|
+
} else if (min instanceof Date || max instanceof Date) {
|
|
135
|
+
value = new Date(value).getTime();
|
|
136
|
+
if (!Number.isFinite(value)) return false;
|
|
137
|
+
}
|
|
138
|
+
if (min instanceof Date) min = min.getTime();
|
|
139
|
+
if (max instanceof Date) max = max.getTime();
|
|
140
|
+
if (Number.isNaN(min)) return false;
|
|
141
|
+
if (Number.isNaN(max)) return false;
|
|
142
|
+
if (min != null && (value < min || value === -Infinity || min === Infinity))
|
|
143
|
+
return false;
|
|
144
|
+
if (max != null && (value > max || value === Infinity || max === -Infinity))
|
|
145
|
+
return false;
|
|
146
|
+
if (min != null || max != null) return true;
|
|
147
|
+
if (typeof value === 'object' && value && condition) return module.exports(value, condition, referenceTime);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
module.exports = function (factValue, ruleValue, referenceTime = Date.now()) {
|
|
153
|
+
if (factValue === ruleValue) return true;
|
|
154
|
+
if (ruleValue && typeof ruleValue === 'object' && 'not' in ruleValue && Object.keys(ruleValue).length === 1) return !module.exports(factValue, ruleValue.not, referenceTime);
|
|
155
|
+
if (
|
|
156
|
+
Array.isArray(factValue) ||
|
|
157
|
+
Array.isArray(ruleValue) ||
|
|
158
|
+
!isPlainObject(factValue) ||
|
|
159
|
+
!isPlainObject(ruleValue)
|
|
160
|
+
)
|
|
161
|
+
return match(referenceTime)(factValue, ruleValue);
|
|
162
|
+
if (factValue && ruleValue && typeof factValue === 'object' && typeof ruleValue === 'object') {
|
|
163
|
+
const nullFilter = Object.entries(ruleValue).filter(([, value]) => value == null || Array.isArray(value));
|
|
164
|
+
if (nullFilter.length > 0) factValue = { ...Object.fromEntries(nullFilter), ...factValue };
|
|
165
|
+
};
|
|
166
|
+
return isMatchWith(factValue, ruleValue, match(referenceTime));
|
|
167
|
+
};
|
package/package.json
CHANGED
|
@@ -1,12 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@infitx/match",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "",
|
|
5
|
-
"license": "ISC",
|
|
6
|
-
"author": "",
|
|
7
|
-
"type": "commonjs",
|
|
3
|
+
"version": "1.3.2",
|
|
4
|
+
"description": "Object pattern matching utility",
|
|
8
5
|
"main": "index.js",
|
|
9
6
|
"scripts": {
|
|
10
|
-
"
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
"build": "true",
|
|
8
|
+
"ci-unit": "JEST_JUNIT_OUTPUT_DIR=coverage jest --ci --reporters=default --reporters=jest-junit --outputFile=./coverage/junit.xml",
|
|
9
|
+
"ci-publish": "npm publish --access public --provenance",
|
|
10
|
+
"watch": "jest --watchAll"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"lodash": "^4.17.21"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"yaml": "^2.8.2",
|
|
17
|
+
"jest": "^30.2.0",
|
|
18
|
+
"jest-junit": "^16.0.0"
|
|
19
|
+
},
|
|
20
|
+
"repository": {
|
|
21
|
+
"url": "git+https://github.com/infitx-org/release-cd.git"
|
|
22
|
+
},
|
|
23
|
+
"author": "Kalin Krustev",
|
|
24
|
+
"license": "Apache-2.0"
|
|
25
|
+
}
|