@infitx/match
v1.4.4
Published
Object pattern matching utility
Readme
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');
// Exact match
match({ a: 1 }, { a: 1 }); // true
match({ a: 1 }, { a: 2 }); // false
// Partial object match
match({ a: 1, b: 2 }, { a: 1 }); // trueReferencing 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.
// Reference another property in the fact
match(
{ expectedStatus: 'active', order: { status: 'active' } },
{ order: { status: { $ref: '#/expectedStatus' } } }
); // true - order.status matches expectedStatus
// Use $ref in min/max range conditions
match(
{ offer: { dateCreated: '2024-01-01' }, order: { dateCreated: '2024-02-01' } },
{ order: { dateCreated: { min: { $ref: '#/offer/dateCreated' } } } }
); // true - order date is after offer date
// Multiple $ref in same rule
match(
{ source: 'Alice', destination: 'Bob', transfer: { from: 'Alice', to: 'Bob' } },
{ transfer: { from: { $ref: '#/source' }, to: { $ref: '#/destination' } } }
); // true - transfer from/to matches source/destination
// $ref with nested paths
match(
{ config: { pricing: { minPrice: 100 } }, order: { price: 150 } },
{ order: { price: { min: { $ref: '#/config/pricing/minPrice' } } } }
); // true - order price is above configured minimum
// Missing properties resolve to undefined
match(
{ order: { dateCreated: '2024-02-01' } },
{ order: { dateCreated: { min: { $ref: '#/offer/dateCreated' } } } }
); // true - missing reference is treated as no constraint
// $ref with type coercion
match(
{ targetAmount: '500', order: { amount: 500 } },
{ order: { amount: { $ref: '#/targetAmount' } } }
); // true - string '500' is coerced to number 500
// $ref with arrays (any-of semantics apply)
match(
{ allowedTag: 'vip', order: { tags: ['vip', 'premium'] } },
{ order: { tags: { $ref: '#/allowedTag' } } }
); // true - at least one tag matches the referenceJSON 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 ignoredArray 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:
// Scalar value against array condition
match('hello', ['hello', 'world']); // true - matches first element
match('world', ['hello', 'world']); // true - matches second element
match('goodbye', ['hello', 'world']); // false - matches none
// With numbers
match(5, [1, 5, 10]); // true
match(7, [1, 5, 10]); // false
// With booleans
match(true, [true, false]); // true
match(false, [true]); // false
// In nested structures - scalar value against array condition
match(
{ status: 'active' },
{ status: ['active', 'pending', 'processing'] }
); // true - status matches one of the allowed values
match(
{ status: 'inactive' },
{ status: ['active', 'pending'] }
); // false - status doesn't match any allowed valueArray in Value
When the value is an array, at least one element must match the condition:
// Array value against scalar condition
match(['apple', 'banana'], 'apple'); // true - first element matches
match(['apple', 'banana'], 'banana'); // true - second element matches
match(['apple', 'banana'], 'orange'); // false - no element matches
// With numbers
match([1, 2, 3], 2); // true
match([1, 2, 3], 5); // false
// In nested structures - array value against scalar condition
match(
{ tags: ['javascript', 'node', 'async'] },
{ tags: 'javascript' }
); // true - at least one tag matches
match(
{ tags: ['python', 'django'] },
{ tags: 'javascript' }
); // false - no tag matches
// Array value against function condition
match(
{ scores: [85, 90, 78] },
{ scores: (score) => score >= 80 }
); // true - at least one score is >= 80
match(
{ scores: [65, 70, 75] },
{ scores: (score) => score >= 80 }
); // false - no score is >= 80Array in Both Value and Condition
When both are arrays, a match occurs if any value element matches any condition element:
// Both arrays - cartesian "any of any" matching
match(['red', 'blue'], ['blue', 'green']); // true - 'blue' appears in both
match(['red', 'yellow'], ['blue', 'green']); // false - no common elements
match([1, 2, 3], [3, 4, 5]); // true - '3' is in both
match([1, 2], [3, 4]); // false - no overlap
// In nested structures
match(
{ tags: ['javascript', 'node'] },
{ tags: ['node', 'async', 'backend'] }
); // true - 'node' is in both arrays
match(
{ tags: ['python', 'django'] },
{ tags: ['javascript', 'react'] }
); // false - no common tags
// Complex: array value against array of conditions (including objects)
match(
{ priority: [1, 2, 3] },
{ priority: [{ min: 2, max: 5 }, 10] }
); // true - elements 2 and 3 match the range { min: 2, max: 5 }
match(
{ priority: [1] },
{ priority: [{ min: 2, max: 5 }, 10] }
); // false - 1 doesn't match range or 10Array 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:
// Array value against range condition
match(
[1, 5, 10],
{ min: 3, max: 7 }
); // true - element '5' falls within the range
match(
[1, 2],
{ min: 3, max: 7 }
); // false - no element falls within the range
// In nested structures
match(
{ scores: [45, 67, 89, 92] },
{ scores: { min: 80 } }
); // true - elements 89 and 92 are >= 80
match(
{ scores: [45, 67, 75] },
{ scores: { min: 80 } }
); // false - no score is >= 80
// Array value against regex condition
match(
{ emails: ['[email protected]', 'invalid', '[email protected]'] },
{ emails: /@test\.com$/ }
); // true - at least one email matches the pattern
match(
{ emails: ['invalid1', 'invalid2'] },
{ emails: /@test\.com$/ }
); // false - no email matchesCombining Arrays with Null
Arrays can include null to make conditions optional:
// Array with null - matches if property is missing OR matches other values
match(
{ status: 'active' },
{ status: [null, 'active'] }
); // true - matches 'active'
match(
{ name: 'Alice' },
{ status: [null, 'active'] }
); // true - 'status' is missing, matches null
match(
{ status: false },
{ status: [null, 'active'] }
); // false - false is neither null nor 'active'
match(
{ status: undefined },
{ status: [null, 'active'] }
); // true - undefined treated as null
// Nested with arrays and null
match(
{ user: { role: 'admin' } },
{ user: { role: [null, 'admin', 'moderator'] } }
); // true - role matches 'admin'
match(
{ user: { name: 'Alice' } },
{ user: { role: [null, 'admin'], name: 'Alice' } }
); // true - role is missing (matches null), name matchesArray Behavior Summary
| Value Type | Condition Type | Behavior | | ---------- | -------------------------- | --------------------------------------------- | | 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:
// null matches undefined/missing properties
match({ b: 0 }, { a: null }); // true - 'a' is missing
match({ a: null }, { a: null }); // true - both null
match({ a: undefined }, { a: null }); // true - undefined treated as null
match({ a: false }, { a: null }); // false - false is not null
match({ a: 0 }, { a: null }); // false - 0 is not null
match({ a: '' }, { a: null }); // false - empty string is not null
// Nested non-existing properties
match(
{ a: {} },
{ a: { b: null } }
); // true - 'b' doesn't exist in nested object
match(
{ a: { b: false } },
{ a: { b: null } }
); // false - 'b' exists and is false
// Array with null for optional properties
match(
{ status: 'active' },
{ status: [null, 'active'] }
); // true - matches 'active'
match(
{ name: 'Alice' },
{ status: [null, 'active'] }
); // true - 'status' is missing, matches null
match(
{ status: false },
{ status: [null, 'active'] }
); // false - false doesn't match null or 'active'Type Coercion
The function coerces values to match the type of the rule:
// Boolean coercion
match('hello', true); // true - truthy string
match('', false); // true - falsy empty string
match(0, false); // true - falsy zero
match(1, true); // true - truthy number
// String coercion
match(123, '123'); // true
match(true, 'true'); // true
// Number coercion
match('42', 42); // true
match('3.14', 3.14); // trueRange Matching
Use min and max for numeric and date range matching:
// Numeric ranges
match(5, { min: 1, max: 10 }); // true
match(15, { min: 1, max: 10 }); // false
match(1, { min: 1 }); // true - only minimum
match(10, { max: 10 }); // true - only maximum
// Date ranges
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 }); // true
match(new Date('2026-01-01'), { min: start, max: end }); // false
// Nested range matching
match(
{ user: { age: 25 } },
{ user: { age: { min: 18, max: 65 } } }
); // trueGrafana-Style Time Intervals
The min and max properties support Grafana-style relative time intervals
for convenient time-based matching:
// Current time
match(new Date(), { min: 'now-1h', max: 'now' });
// true - current time is within the last hour
// Future time check
match(new Date(), { max: 'now+1d' });
// true - current time is before tomorrow
// Past time check
match(new Date(), { min: 'now-1w' });
// true - current time is within the last week
// Time range
match(
new Date(),
{ min: 'now-1d', max: 'now+1d' }
); // true - current time is within ±1 day
// In nested structures
match(
{ event: { timestamp: new Date() } },
{ event: { timestamp: { min: 'now-5m' } } }
); // true - event occurred within the last 5 minutes
match(
{ event: { timestamp: new Date(Date.now() - 10 * 60 * 1000) } },
{ event: { timestamp: { min: 'now-5m' } } }
); // false - event occurred more than 5 minutes agoSupported Time Units
| Unit | Description | Example |
| ---- | -------------- | ----------- |
| 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:
| Unit | Rounds To | Example |
| ---- | -------------------- | -------------------------------- |
| 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 timenow-<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:
// Round to start of current day (midnight)
match(new Date('2025-06-15T14:30:00'), { min: 'now/d', max: 'now' });
// Compares against start of current day
// Round to start of current hour
match(new Date(), { min: 'now/h' });
// Matches times from the start of the current hour
// Combine offset and rounding
// Example: 5 days ago, rounded to midnight of that day
match(new Date(), { min: 'now-5d/d' });
// Week rounding (rounds to Monday)
match(new Date(), { min: 'now/w' });
// Matches times from the start of the current week
// Month rounding
match(new Date(), { min: 'now-1M/M' });
// Matches times from the start of last month
// Rounding units: s (second), m (minute), h (hour), d (day),
// w (week), M (month), y (year)// Examples with different units
match(new Date(), { min: 'now-500ms' }); // Last 500 milliseconds
match(new Date(), { min: 'now-30s' }); // Last 30 seconds
match(new Date(), { min: 'now-5m' }); // Last 5 minutes
match(new Date(), { min: 'now-2h' }); // Last 2 hours
match(new Date(), { min: 'now-7d' }); // Last 7 days
match(new Date(), { min: 'now-2w' }); // Last 2 weeks
match(new Date(), { min: 'now-3M' }); // Last 3 months (approx)
match(new Date(), { min: 'now-1y' }); // Last year (approx)
// Future times
match(new Date(), { max: 'now+1h' }); // Within next hour
match(new Date(), { max: 'now+7d' }); // Within next 7 days
// Examples with rounding
match(new Date(), { min: 'now/d' }); // Since midnight today
match(new Date(), { min: 'now-7d/d', max: 'now/d' }); // Last 7 full days
match(new Date(), { min: 'now/M' }); // Since start of current month
match(new Date(), { min: 'now-1y/y', max: 'now/y' }); // Last full year
// Practical examples
// Check if log entry is recent (last 15 minutes)
match(
{ log: { timestamp: new Date() } },
{ log: { timestamp: { min: 'now-15m' } } }
); // true
// Check if event occurred today (since midnight)
match(
{ event: { timestamp: new Date() } },
{ event: { timestamp: { min: 'now/d' } } }
); // true if event is from today
// Check if data is from the current month
match(
{ report: { date: new Date() } },
{ report: { date: { min: 'now/M', max: 'now' } } }
); // true if report is from current month
// Check if scheduled event is upcoming (next 24 hours)
match(
{ event: { scheduledAt: new Date(Date.now() + 12 * 60 * 60 * 1000) } },
{ event: { scheduledAt: { min: 'now', max: 'now+1d' } } }
); // true
// Check if user session is still valid (created within last 30 minutes)
match(
{ session: { createdAt: new Date(Date.now() - 10 * 60 * 1000) } },
{ session: { createdAt: { min: 'now-30m' } } }
); // true
// Daily reports: match events from start of day until now
match(
{ log: { timestamp: new Date() } },
{ log: { timestamp: { min: 'now/d', max: 'now' } } }
); // Matches anything that happened today
// Weekly reports: last 7 complete days
match(
{ metric: { recorded: new Date() } },
{ metric: { recorded: { min: 'now-7d/d', max: 'now/d' } } }
); // Matches data from last 7 full days (midnight to midnight)
// Business hours check: events during current hour
match(
{ transaction: { timestamp: new Date() } },
{ transaction: { timestamp: { min: 'now/h', max: 'now' } } }
); // Matches transactions from start of current hourFunction Predicates
Use functions for custom matching logic:
// Function as rule
match(10, (value) => value > 5); // true
match(3, (value) => value > 5); // false
// Complex predicate
match(
'[email protected]',
(value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
); // true - valid email
// Function in nested structure
match(
{ user: { age: 25 } },
{ user: { age: (age) => age >= 18 } }
); // trueNegation with not
Use the not property in an object condition to negate any match:
// Basic negation
match(3, { not: 5 }); // true - 3 is not 5
match(5, { not: 5 }); // false - 5 is 5
// Negate string match
match('goodbye', { not: 'hello' }); // true
match('hello', { not: 'hello' }); // false
// Negate boolean
match(false, { not: true }); // true
match(true, { not: true }); // false
match(0, { not: true }); // true - 0 is falsy, not true
match(1, { not: true }); // false - 1 coerces to true
// Negate null
match(0, { not: null }); // true - 0 is not null
match(null, { not: null }); // false
match(undefined, { not: null }); // false - undefined treated as null
// In nested structures
match(
{ status: 'inactive' },
{ status: { not: 'active' } }
); // true
match(
{ status: 'active' },
{ status: { not: 'active' } }
); // falseNegating Complex Conditions
The not property works with all match types including arrays, ranges,
functions, and regex:
// Negate regex
match('goodbye', { not: /hello/ }); // true
match('hello world', { not: /hello/ }); // false
match(
{ email: '[email protected]' },
{ email: { not: /@example\.com$/ } }
); // true - doesn't end with @example.com
// Negate array (none of)
match(5, { not: [1, 2, 3] }); // true - 5 is not in the list
match(2, { not: [1, 2, 3] }); // false - 2 is in the list
match(
{ role: 'guest' },
{ role: { not: ['admin', 'moderator'] } }
); // true - guest is neither admin nor moderator
match(
{ role: 'admin' },
{ role: { not: ['admin', 'moderator'] } }
); // false - admin is in the list
// Negate range
match(3, { not: { min: 5, max: 10 } }); // true - 3 is outside the range
match(7, { not: { min: 5, max: 10 } }); // false - 7 is in the range
match(15, { not: { min: 5, max: 10 } }); // true - 15 is outside the range
match(
{ age: 15 },
{ age: { not: { min: 18, max: 65 } } }
); // true - age is below minimum
match(
{ age: 25 },
{ age: { not: { min: 18, max: 65 } } }
); // false - age is in range
// Negate function predicate
match(3, { not: (v) => v > 5 }); // true - 3 is not > 5
match(10, { not: (v) => v > 5 }); // false - 10 is > 5
match(
{ price: 25 },
{ price: { not: (p) => p >= 100 } }
); // true - price is not >= 100
// Negate object match
match(
{ user: { role: 'guest' } },
{ user: { not: { role: 'admin' } } }
); // true - role is not admin
match(
{ user: { role: 'admin' } },
{ user: { not: { role: 'admin' } } }
); // false - role is admin
// Complex negation in nested structures
match(
{
user: {
email: '[email protected]',
role: 'user'
}
},
{
user: {
email: { not: /@example\.com$/ },
role: { not: ['admin', 'moderator'] }
}
}
); // true - email doesn't end with @example.com and role is not admin/moderatorCombining not with Other Conditions
You can combine not with other conditions in complex matching scenarios:
// Exclude certain values while checking other properties
match(
{ status: 'pending', priority: 2 },
{
status: { not: ['cancelled', 'completed'] },
priority: { min: 1, max: 3 }
}
); // true - status is not cancelled/completed and priority is in range
// Array values with negation
match(
{ tags: ['javascript', 'backend'] },
{ tags: { not: 'frontend' } }
); // true - none of the tags are 'frontend'
match(
{ tags: ['javascript', 'frontend'] },
{ tags: { not: 'frontend' } }
); // false - 'frontend' is in the tagsRegular Expression Matching
// Regex patterns
match('hello world', /hello/); // true
match('goodbye world', /hello/); // false
// Case-insensitive matching
match('Hello World', /hello/i); // true
// Nested regex
match(
{ email: '[email protected]' },
{ email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/ }
); // trueComplex Examples
Combining multiple matching strategies:
// Complex nested structure with arrays and nulls
match(
{
user: {
name: 'Alice',
email: '[email protected]',
roles: ['admin', 'user']
},
status: 'active'
},
{
user: {
email: /@example\.com$/,
roles: 'admin'
},
status: ['active', 'pending'],
lastLogin: null // optional field
}
); // true
// Multiple conditions
match(
{ price: 50, category: 'electronics', inStock: true },
{
price: { min: 0, max: 100 },
category: ['electronics', 'computers'],
inStock: true,
discount: null // discount is optional
}
); // trueAPI
match(factValue, ruleValue)
Compares a fact value against a rule value with flexible matching semantics.
Parameters:
factValue(any): The actual value to testruleValue(any): The pattern/rule to match against
Returns:
boolean:trueif the fact matches the rule,falseotherwise
Matching Rules:
- Exact equality: Returns
trueif values are strictly equal - Null handling:
nullin rule matchesnullorundefinedin 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/maxproperties enable range matching - Negation: Objects with
notproperty 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.
