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