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.
 
 
 

305 lines
6.3 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 {
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;
};