Match Function
A flexible matching utility that compares values with advanced semantic rules,
supporting nested structures, arrays, type coercion, and various matching strategies.
Installation
const match = require('./match');
Basic Usage
const match = require('./match');
match({ a: 1 }, { a: 1 });
match({ a: 1 }, { a: 2 });
match({ a: 1, b: 2 }, { a: 1 });
Referencing Fact Values with $ref
The $ref feature allows rules to reference values from other parts of the fact using JSON Pointer syntax. This is useful for dynamic comparisons where the expected value comes from the fact itself.
match(
{ expectedStatus: 'active', order: { status: 'active' } },
{ order: { status: { $ref: '#/expectedStatus' } } }
);
match(
{ offer: { dateCreated: '2024-01-01' }, order: { dateCreated: '2024-02-01' } },
{ order: { dateCreated: { min: { $ref: '#/offer/dateCreated' } } } }
);
match(
{ source: 'Alice', destination: 'Bob', transfer: { from: 'Alice', to: 'Bob' } },
{ transfer: { from: { $ref: '#/source' }, to: { $ref: '#/destination' } } }
);
match(
{ config: { pricing: { minPrice: 100 } }, order: { price: 150 } },
{ order: { price: { min: { $ref: '#/config/pricing/minPrice' } } } }
);
match(
{ order: { dateCreated: '2024-02-01' } },
{ order: { dateCreated: { min: { $ref: '#/offer/dateCreated' } } } }
);
match(
{ targetAmount: '500', order: { amount: 500 } },
{ order: { amount: { $ref: '#/targetAmount' } } }
);
match(
{ allowedTag: 'vip', order: { tags: ['vip', 'premium'] } },
{ order: { tags: { $ref: '#/allowedTag' } } }
);
JSON Pointer Syntax
The $ref value uses JSON Pointer syntax (RFC 6901):
- Always starts with
#/ to indicate the root of the fact object
- Properties are separated by
/
- Examples:
#/offer/dateCreated → fact.offer.dateCreated
#/config/pricing/minPrice → fact.config.pricing.minPrice
#/expectedStatus → fact.expectedStatus
When to Use $ref
Use $ref when you need to:
- Compare related values within the same fact
- Implement dynamic constraints based on other properties
- Reference configuration values from the fact
- Create rules that adapt to the data being matched
## Matching Against Nested Structures
The match function recursively compares nested objects:
```javascript
// Nested object matching
match(
{ user: { name: 'Alice', age: 30 } },
{ user: { name: 'Alice' } }
); // true - partial match on nested object
match(
{ user: { name: 'Alice', age: 30 } },
{ user: { name: 'Bob' } }
); // false - name doesn't match
// Deep nesting
match(
{ a: { b: { c: { d: 'value' } } } },
{ a: { b: { c: { d: 'value' } } } }
); // true
match(
{ a: { b: { c: { d: 'value', e: 'extra' } } } },
{ a: { b: { c: { d: 'value' } } } }
); // true - extra properties are ignored
Array Matching (Any-Of Semantics)
Arrays implement "any of" semantics - at least one element must match. The
behavior varies depending on whether arrays appear in the value, the condition,
or both.
Array in Condition (Rule)
When the condition is an array, the value must match at least one element
in the array:
match('hello', ['hello', 'world']);
match('world', ['hello', 'world']);
match('goodbye', ['hello', 'world']);
match(5, [1, 5, 10]);
match(7, [1, 5, 10]);
match(true, [true, false]);
match(false, [true]);
match(
{ status: 'active' },
{ status: ['active', 'pending', 'processing'] }
);
match(
{ status: 'inactive' },
{ status: ['active', 'pending'] }
);
Array in Value
When the value is an array, at least one element must match the condition:
match(['apple', 'banana'], 'apple');
match(['apple', 'banana'], 'banana');
match(['apple', 'banana'], 'orange');
match([1, 2, 3], 2);
match([1, 2, 3], 5);
match(
{ tags: ['javascript', 'node', 'async'] },
{ tags: 'javascript' }
);
match(
{ tags: ['python', 'django'] },
{ tags: 'javascript' }
);
match(
{ scores: [85, 90, 78] },
{ scores: (score) => score >= 80 }
);
match(
{ scores: [65, 70, 75] },
{ scores: (score) => score >= 80 }
);
Array in Both Value and Condition
When both are arrays, a match occurs if any value element matches any
condition element:
match(['red', 'blue'], ['blue', 'green']);
match(['red', 'yellow'], ['blue', 'green']);
match([1, 2, 3], [3, 4, 5]);
match([1, 2], [3, 4]);
match(
{ tags: ['javascript', 'node'] },
{ tags: ['node', 'async', 'backend'] }
);
match(
{ tags: ['python', 'django'] },
{ tags: ['javascript', 'react'] }
);
match(
{ priority: [1, 2, 3] },
{ priority: [{ min: 2, max: 5 }, 10] }
);
match(
{ priority: [1] },
{ priority: [{ min: 2, max: 5 }, 10] }
);
Array Value Against Object Condition
When an array value is matched against an object condition (like a range or
complex object), each element is tested against the condition:
match(
[1, 5, 10],
{ min: 3, max: 7 }
);
match(
[1, 2],
{ min: 3, max: 7 }
);
match(
{ scores: [45, 67, 89, 92] },
{ scores: { min: 80 } }
);
match(
{ scores: [45, 67, 75] },
{ scores: { min: 80 } }
);
match(
{ emails: ['admin@test.com', 'invalid', 'user@test.com'] },
{ emails: /@test\.com$/ }
);
match(
{ emails: ['invalid1', 'invalid2'] },
{ emails: /@test\.com$/ }
);
Combining Arrays with Null
Arrays can include null to make conditions optional:
match(
{ status: 'active' },
{ status: [null, 'active'] }
);
match(
{ name: 'Alice' },
{ status: [null, 'active'] }
);
match(
{ status: false },
{ status: [null, 'active'] }
);
match(
{ status: undefined },
{ status: [null, 'active'] }
);
match(
{ user: { role: 'admin' } },
{ user: { role: [null, 'admin', 'moderator'] } }
);
match(
{ user: { name: 'Alice' } },
{ user: { role: [null, 'admin'], name: 'Alice' } }
);
Array Behavior Summary
| Scalar | Array | Value must match at least one array element |
| Array | Scalar | At least one value must match the scalar |
| Array | Array | At least one value must match one condition |
| Array | Object (range/complex) | At least one value must match the condition |
| Array | Function | At least one value must satisfy the function |
Matching Against Non-Existing Properties
The match function has special handling for null values to match against
non-existing properties:
match({ b: 0 }, { a: null });
match({ a: null }, { a: null });
match({ a: undefined }, { a: null });
match({ a: false }, { a: null });
match({ a: 0 }, { a: null });
match({ a: '' }, { a: null });
match(
{ a: {} },
{ a: { b: null } }
);
match(
{ a: { b: false } },
{ a: { b: null } }
);
match(
{ status: 'active' },
{ status: [null, 'active'] }
);
match(
{ name: 'Alice' },
{ status: [null, 'active'] }
);
match(
{ status: false },
{ status: [null, 'active'] }
);
Type Coercion
The function coerces values to match the type of the rule:
match('hello', true);
match('', false);
match(0, false);
match(1, true);
match(123, '123');
match(true, 'true');
match('42', 42);
match('3.14', 3.14);
Range Matching
Use min and max for numeric and date range matching:
match(5, { min: 1, max: 10 });
match(15, { min: 1, max: 10 });
match(1, { min: 1 });
match(10, { max: 10 });
const now = new Date('2025-06-15');
const start = new Date('2025-01-01');
const end = new Date('2025-12-31');
match(now, { min: start, max: end });
match(new Date('2026-01-01'), { min: start, max: end });
match(
{ user: { age: 25 } },
{ user: { age: { min: 18, max: 65 } } }
);
Grafana-Style Time Intervals
The min and max properties support Grafana-style relative time intervals
for convenient time-based matching:
match(new Date(), { min: 'now-1h', max: 'now' });
match(new Date(), { max: 'now+1d' });
match(new Date(), { min: 'now-1w' });
match(
new Date(),
{ min: 'now-1d', max: 'now+1d' }
);
match(
{ event: { timestamp: new Date() } },
{ event: { timestamp: { min: 'now-5m' } } }
);
match(
{ event: { timestamp: new Date(Date.now() - 10 * 60 * 1000) } },
{ event: { timestamp: { min: 'now-5m' } } }
);
Supported Time Units
ms | Milliseconds | now-500ms |
s | Seconds | now-30s |
m | Minutes | now-5m |
h | Hours | now-2h |
d | Days | now-7d |
w | Weeks | now-2w |
M | Months (30d) | now-3M |
y | Years (365d) | now-1y |
Rounding Units
When using the / operator, you can round to these time units:
s | Start of second | now/s = current second at .000 |
m | Start of minute | now/m = current minute at :00 |
h | Start of hour | now/h = current hour at :00:00 |
d | Start of day | now/d = today at 00:00:00 |
w | Start of week | now/w = Monday at 00:00:00 |
M | Start of month | now/M = 1st of month 00:00:00 |
y | Start of year | now/y = Jan 1st at 00:00:00 |
Note: Week rounding always rounds to Monday as the first day of the week.
Time Interval Format
now: Current time
now-<amount><unit>: Time in the past (e.g., now-5m = 5 minutes ago)
now+<amount><unit>: Time in the future (e.g., now+1h = 1 hour from now)
now/<unit>: Current time rounded to start of unit (e.g., now/d = start of today)
now[+-]<amount><unit>/<roundUnit>: Time with offset, rounded (e.g., now-5d/d = 5 days ago at midnight)
Time Rounding
The / operator rounds timestamps to the start of the specified time unit, following Grafana's approach:
match(new Date('2025-06-15T14:30:00'), { min: 'now/d', max: 'now' });
match(new Date(), { min: 'now/h' });
match(new Date(), { min: 'now-5d/d' });
match(new Date(), { min: 'now/w' });
match(new Date(), { min: 'now-1M/M' });
match(new Date(), { min: 'now-500ms' });
match(new Date(), { min: 'now-30s' });
match(new Date(), { min: 'now-5m' });
match(new Date(), { min: 'now-2h' });
match(new Date(), { min: 'now-7d' });
match(new Date(), { min: 'now-2w' });
match(new Date(), { min: 'now-3M' });
match(new Date(), { min: 'now-1y' });
match(new Date(), { max: 'now+1h' });
match(new Date(), { max: 'now+7d' });
match(new Date(), { min: 'now/d' });
match(new Date(), { min: 'now-7d/d', max: 'now/d' });
match(new Date(), { min: 'now/M' });
match(new Date(), { min: 'now-1y/y', max: 'now/y' });
match(
{ log: { timestamp: new Date() } },
{ log: { timestamp: { min: 'now-15m' } } }
);
match(
{ event: { timestamp: new Date() } },
{ event: { timestamp: { min: 'now/d' } } }
);
match(
{ report: { date: new Date() } },
{ report: { date: { min: 'now/M', max: 'now' } } }
);
match(
{ event: { scheduledAt: new Date(Date.now() + 12 * 60 * 60 * 1000) } },
{ event: { scheduledAt: { min: 'now', max: 'now+1d' } } }
);
match(
{ session: { createdAt: new Date(Date.now() - 10 * 60 * 1000) } },
{ session: { createdAt: { min: 'now-30m' } } }
);
match(
{ log: { timestamp: new Date() } },
{ log: { timestamp: { min: 'now/d', max: 'now' } } }
);
match(
{ metric: { recorded: new Date() } },
{ metric: { recorded: { min: 'now-7d/d', max: 'now/d' } } }
);
match(
{ transaction: { timestamp: new Date() } },
{ transaction: { timestamp: { min: 'now/h', max: 'now' } } }
);
Function Predicates
Use functions for custom matching logic:
match(10, (value) => value > 5);
match(3, (value) => value > 5);
match(
'hello@example.com',
(value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
);
match(
{ user: { age: 25 } },
{ user: { age: (age) => age >= 18 } }
);
Negation with not
Use the not property in an object condition to negate any match:
match(3, { not: 5 });
match(5, { not: 5 });
match('goodbye', { not: 'hello' });
match('hello', { not: 'hello' });
match(false, { not: true });
match(true, { not: true });
match(0, { not: true });
match(1, { not: true });
match(0, { not: null });
match(null, { not: null });
match(undefined, { not: null });
match(
{ status: 'inactive' },
{ status: { not: 'active' } }
);
match(
{ status: 'active' },
{ status: { not: 'active' } }
);
Negating Complex Conditions
The not property works with all match types including arrays, ranges,
functions, and regex:
match('goodbye', { not: /hello/ });
match('hello world', { not: /hello/ });
match(
{ email: 'user@domain.org' },
{ email: { not: /@example\.com$/ } }
);
match(5, { not: [1, 2, 3] });
match(2, { not: [1, 2, 3] });
match(
{ role: 'guest' },
{ role: { not: ['admin', 'moderator'] } }
);
match(
{ role: 'admin' },
{ role: { not: ['admin', 'moderator'] } }
);
match(3, { not: { min: 5, max: 10 } });
match(7, { not: { min: 5, max: 10 } });
match(15, { not: { min: 5, max: 10 } });
match(
{ age: 15 },
{ age: { not: { min: 18, max: 65 } } }
);
match(
{ age: 25 },
{ age: { not: { min: 18, max: 65 } } }
);
match(3, { not: (v) => v > 5 });
match(10, { not: (v) => v > 5 });
match(
{ price: 25 },
{ price: { not: (p) => p >= 100 } }
);
match(
{ user: { role: 'guest' } },
{ user: { not: { role: 'admin' } } }
);
match(
{ user: { role: 'admin' } },
{ user: { not: { role: 'admin' } } }
);
match(
{
user: {
email: 'user@custom.com',
role: 'user'
}
},
{
user: {
email: { not: /@example\.com$/ },
role: { not: ['admin', 'moderator'] }
}
}
);
Combining not with Other Conditions
You can combine not with other conditions in complex matching scenarios:
match(
{ status: 'pending', priority: 2 },
{
status: { not: ['cancelled', 'completed'] },
priority: { min: 1, max: 3 }
}
);
match(
{ tags: ['javascript', 'backend'] },
{ tags: { not: 'frontend' } }
);
match(
{ tags: ['javascript', 'frontend'] },
{ tags: { not: 'frontend' } }
);
Regular Expression Matching
match('hello world', /hello/);
match('goodbye world', /hello/);
match('Hello World', /hello/i);
match(
{ email: 'user@example.com' },
{ email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/ }
);
Complex Examples
Combining multiple matching strategies:
match(
{
user: {
name: 'Alice',
email: 'alice@example.com',
roles: ['admin', 'user']
},
status: 'active'
},
{
user: {
email: /@example\.com$/,
roles: 'admin'
},
status: ['active', 'pending'],
lastLogin: null
}
);
match(
{ price: 50, category: 'electronics', inStock: true },
{
price: { min: 0, max: 100 },
category: ['electronics', 'computers'],
inStock: true,
discount: null
}
);
API
match(factValue, ruleValue)
Compares a fact value against a rule value with flexible matching semantics.
Parameters:
factValue (any): The actual value to test
ruleValue (any): The pattern/rule to match against
Returns:
boolean: true if the fact matches the rule, false otherwise
Matching Rules:
- Exact equality: Returns
true if values are strictly equal
- Null handling:
null in rule matches null or undefined in fact
- Arrays: "Any of" semantics - at least one element must match
- Objects: Recursively matches properties (partial matching allowed)
- Type coercion: Values are coerced to match rule type
- Ranges: Objects with
min/max properties enable range matching
- Negation: Objects with
not property negate any match condition
- Functions: Rule functions are called with fact value as predicate
- RegExp: Tests string values against regex patterns
License
See the main project license.