Spaces:
Sleeping
Sleeping
; | |
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 { | |
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; | |
}; | |