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.
308 lines
6.4 KiB
308 lines
6.4 KiB
4 weeks ago
|
'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;
|
||
|
};
|