Spaces:
Sleeping
Sleeping
| ; | |
| 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; | |
| }; | |