308 lines
		
	
	
		
			6.4 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			308 lines
		
	
	
		
			6.4 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| '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;
 | |
| };
 |