You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
307 lines
6.4 KiB
307 lines
6.4 KiB
'use strict'; |
|
|
|
const Assert = require('./assert'); |
|
const DeepEqual = require('./deepEqual'); |
|
const EscapeRegex = require('./escapeRegex'); |
|
const Utils = require('./utils'); |
|
|
|
|
|
const internals = {}; |
|
|
|
|
|
module.exports = function (ref, values, options = {}) { // options: { deep, once, only, part, symbols } |
|
|
|
/* |
|
string -> string(s) |
|
array -> item(s) |
|
object -> key(s) |
|
object -> object (key:value) |
|
*/ |
|
|
|
if (typeof values !== 'object') { |
|
values = [values]; |
|
} |
|
|
|
Assert(!Array.isArray(values) || values.length, 'Values array cannot be empty'); |
|
|
|
// String |
|
|
|
if (typeof ref === 'string') { |
|
return internals.string(ref, values, options); |
|
} |
|
|
|
// Array |
|
|
|
if (Array.isArray(ref)) { |
|
return internals.array(ref, values, options); |
|
} |
|
|
|
// Object |
|
|
|
Assert(typeof ref === 'object', 'Reference must be string or an object'); |
|
return internals.object(ref, values, options); |
|
}; |
|
|
|
|
|
internals.array = function (ref, values, options) { |
|
|
|
if (!Array.isArray(values)) { |
|
values = [values]; |
|
} |
|
|
|
if (!ref.length) { |
|
return false; |
|
} |
|
|
|
if (options.only && |
|
options.once && |
|
ref.length !== values.length) { |
|
|
|
return false; |
|
} |
|
|
|
let compare; |
|
|
|
// Map values |
|
|
|
const map = new Map(); |
|
for (const value of values) { |
|
if (!options.deep || |
|
!value || |
|
typeof value !== 'object') { |
|
|
|
const existing = map.get(value); |
|
if (existing) { |
|
++existing.allowed; |
|
} |
|
else { |
|
map.set(value, { allowed: 1, hits: 0 }); |
|
} |
|
} |
|
else { |
|
compare = compare || internals.compare(options); |
|
|
|
let found = false; |
|
for (const [key, existing] of map.entries()) { |
|
if (compare(key, value)) { |
|
++existing.allowed; |
|
found = true; |
|
break; |
|
} |
|
} |
|
|
|
if (!found) { |
|
map.set(value, { allowed: 1, hits: 0 }); |
|
} |
|
} |
|
} |
|
|
|
// Lookup values |
|
|
|
let hits = 0; |
|
for (const item of ref) { |
|
let match; |
|
if (!options.deep || |
|
!item || |
|
typeof item !== 'object') { |
|
|
|
match = map.get(item); |
|
} |
|
else { |
|
compare = compare || internals.compare(options); |
|
|
|
for (const [key, existing] of map.entries()) { |
|
if (compare(key, item)) { |
|
match = existing; |
|
break; |
|
} |
|
} |
|
} |
|
|
|
if (match) { |
|
++match.hits; |
|
++hits; |
|
|
|
if (options.once && |
|
match.hits > match.allowed) { |
|
|
|
return false; |
|
} |
|
} |
|
} |
|
|
|
// Validate results |
|
|
|
if (options.only && |
|
hits !== ref.length) { |
|
|
|
return false; |
|
} |
|
|
|
for (const match of map.values()) { |
|
if (match.hits === match.allowed) { |
|
continue; |
|
} |
|
|
|
if (match.hits < match.allowed && |
|
!options.part) { |
|
|
|
return false; |
|
} |
|
} |
|
|
|
return !!hits; |
|
}; |
|
|
|
|
|
internals.object = function (ref, values, options) { |
|
|
|
Assert(options.once === undefined, 'Cannot use option once with object'); |
|
|
|
const keys = Utils.keys(ref, options); |
|
if (!keys.length) { |
|
return false; |
|
} |
|
|
|
// Keys list |
|
|
|
if (Array.isArray(values)) { |
|
return internals.array(keys, values, options); |
|
} |
|
|
|
// Key value pairs |
|
|
|
const symbols = Object.getOwnPropertySymbols(values).filter((sym) => values.propertyIsEnumerable(sym)); |
|
const targets = [...Object.keys(values), ...symbols]; |
|
|
|
const compare = internals.compare(options); |
|
const set = new Set(targets); |
|
|
|
for (const key of keys) { |
|
if (!set.has(key)) { |
|
if (options.only) { |
|
return false; |
|
} |
|
|
|
continue; |
|
} |
|
|
|
if (!compare(values[key], ref[key])) { |
|
return false; |
|
} |
|
|
|
set.delete(key); |
|
} |
|
|
|
if (set.size) { |
|
return options.part ? set.size < targets.length : false; |
|
} |
|
|
|
return true; |
|
}; |
|
|
|
|
|
internals.string = function (ref, values, options) { |
|
|
|
// Empty string |
|
|
|
if (ref === '') { |
|
return values.length === 1 && values[0] === '' || // '' contains '' |
|
!options.once && !values.some((v) => v !== ''); // '' contains multiple '' if !once |
|
} |
|
|
|
// Map values |
|
|
|
const map = new Map(); |
|
const patterns = []; |
|
|
|
for (const value of values) { |
|
Assert(typeof value === 'string', 'Cannot compare string reference to non-string value'); |
|
|
|
if (value) { |
|
const existing = map.get(value); |
|
if (existing) { |
|
++existing.allowed; |
|
} |
|
else { |
|
map.set(value, { allowed: 1, hits: 0 }); |
|
patterns.push(EscapeRegex(value)); |
|
} |
|
} |
|
else if (options.once || |
|
options.only) { |
|
|
|
return false; |
|
} |
|
} |
|
|
|
if (!patterns.length) { // Non-empty string contains unlimited empty string |
|
return true; |
|
} |
|
|
|
// Match patterns |
|
|
|
const regex = new RegExp(`(${patterns.join('|')})`, 'g'); |
|
const leftovers = ref.replace(regex, ($0, $1) => { |
|
|
|
++map.get($1).hits; |
|
return ''; // Remove from string |
|
}); |
|
|
|
// Validate results |
|
|
|
if (options.only && |
|
leftovers) { |
|
|
|
return false; |
|
} |
|
|
|
let any = false; |
|
for (const match of map.values()) { |
|
if (match.hits) { |
|
any = true; |
|
} |
|
|
|
if (match.hits === match.allowed) { |
|
continue; |
|
} |
|
|
|
if (match.hits < match.allowed && |
|
!options.part) { |
|
|
|
return false; |
|
} |
|
|
|
// match.hits > match.allowed |
|
|
|
if (options.once) { |
|
return false; |
|
} |
|
} |
|
|
|
return !!any; |
|
}; |
|
|
|
|
|
internals.compare = function (options) { |
|
|
|
if (!options.deep) { |
|
return internals.shallow; |
|
} |
|
|
|
const hasOnly = options.only !== undefined; |
|
const hasPart = options.part !== undefined; |
|
|
|
const flags = { |
|
prototype: hasOnly ? options.only : hasPart ? !options.part : false, |
|
part: hasOnly ? !options.only : hasPart ? options.part : false |
|
}; |
|
|
|
return (a, b) => DeepEqual(a, b, flags); |
|
}; |
|
|
|
|
|
internals.shallow = function (a, b) { |
|
|
|
return a === b; |
|
};
|
|
|