|
|
'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; };
|