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