(function () { /** @module fuzzball */ 'use strict'; var SequenceMatcher = require('./lib/fbdifflib.js'); var Heap = require('heap'); var _intersect = require('./lib/lodash.custom.min.js').intersection; var _intersectWith = require('./lib/lodash.custom.min.js').intersectionWith; var _difference = require('./lib/lodash.custom.min.js').difference; var _differenceWith = require('./lib/lodash.custom.min.js').differenceWith; var _uniq = require('./lib/lodash.custom.min.js').uniq; var _uniqWith = require('./lib/lodash.custom.min.js').uniqWith; var _partialRight = require('./lib/lodash.custom.min.js').partialRight; var _forEach = require('./lib/lodash.custom.min.js').forEach; var _keys = require('./lib/lodash.custom.min.js').keys; var _isArray = require('./lib/lodash.custom.min.js').isArray; var _toArray = require('./lib/lodash.custom.min.js').toArray; var _orderBy = require('./lib/lodash.custom.min.js').orderBy; function orderByDesc (arr, cmp) { var mapped = arr.map(function (str) { return { key: str, value: cmp(str) }; }); mapped.sort(function (a, b) { return b.value - a.value; }); return mapped.map(function (item) { return item.key; }); } var iLeven = require('./lib/iLeven.js'); var wildleven = require('./lib/wildcardLeven.js'); var leven = require('./lib/leven.js'); if (typeof setImmediate !== 'function') { require('setimmediate'); } // didn't run in tiny-worker without extra check var utils = require('./lib/utils.js')(_uniq, _uniqWith, _partialRight); var validate = utils.validate; var process_and_sort = utils.process_and_sort; var tokenize = utils.tokenize; var full_process = utils.full_process; var clone_and_set_option_defaults = utils.clone_and_set_option_defaults; var isCustomFunc = utils.isCustomFunc; var processing = require('./lib/process.js')(clone_and_set_option_defaults, _isArray, QRatio, extract); var dedupe = processing.dedupe; /** Mostly follows after python fuzzywuzzy, https://github.com/seatgeek/fuzzywuzzy */ /** Public functions */ function distance(str1, str2, options_p) { /** * Calculate levenshtein distance of the two strings. * * @function distance * @param {string} str1 - the first string. * @param {string} str2 - the second string. * @param {Object} [options_p] - Additional options. * @param {boolean} [options_p.useCollator] - Use `Intl.Collator` for locale-sensitive string comparison. * @param {boolean} [options_p.full_process] - Apply basic cleanup, non-alphanumeric to whitespace etc. if true. default true * @param {boolean} [options_p.force_ascii] - Strip non-ascii in full_process if true (non-ascii will not become whtespace), only applied if full_process is true as well, default true * @param {boolean} [options_p.collapseWhitespace] - Collapse consecutive white space during full_process, default true * @param {string} [options_p.wildcards] - characters that will be used as wildcards if provided * @param {number} [options_p.astral] - Use astral aware calculation * @param {string} [options_p.normalize] - Normalize unicode representations * @returns {number} - the levenshtein distance (0 and above). */ var options = clone_and_set_option_defaults(options_p); str1 = options.full_process ? full_process(str1, options) : str1; str2 = options.full_process ? full_process(str2, options) : str2; if (typeof options.subcost === "undefined") options.subcost = 1; if (options.astral) return iLeven(str1, str2, options, _toArray); else return wildleven(str1, str2, options, leven); // falls back to leven if no wildcards } function QRatio(str1, str2, options_p) { /** * Calculate levenshtein ratio of the two strings. * * @function ratio * @param {string} str1 - the first string. * @param {string} str2 - the second string. * @param {Object} [options_p] - Additional options. * @param {boolean} [options_p.useCollator] - Use `Intl.Collator` for locale-sensitive string comparison. * @param {boolean} [options_p.full_process] - Apply basic cleanup, non-alphanumeric to whitespace etc. if true. default true * @param {boolean} [options_p.force_ascii] - Strip non-ascii in full_process if true (non-ascii will not become whtespace), only applied if full_process is true as well, default true * @param {boolean} [options_p.collapseWhitespace] - Collapse consecutive white space during full_process, default true * @param {string} [options_p.wildcards] - characters that will be used as wildcards if provided * @param {number} [options_p.astral] - Use astral aware calculation * @param {string} [options_p.normalize] - Normalize unicode representations * @returns {number} - the levenshtein ratio (0-100). */ var options = clone_and_set_option_defaults(options_p); str1 = options.full_process ? full_process(str1, options) : str1; str2 = options.full_process ? full_process(str2, options) : str2; if (!validate(str1)) return 0; if (!validate(str2)) return 0; return _ratio(str1, str2, options); } function partial_ratio(str1, str2, options_p) { /** * Calculate partial levenshtein ratio of the two strings. * * @function partial_ratio * @param {string} str1 - the first string. * @param {string} str2 - the second string. * @param {Object} [options_p] - Additional options. * @param {boolean} [options_p.useCollator] - Use `Intl.Collator` for locale-sensitive string comparison. * @param {boolean} [options_p.full_process] - Apply basic cleanup, non-alphanumeric to whitespace etc. if true. default true * @param {boolean} [options_p.force_ascii] - Strip non-ascii in full_process if true (non-ascii will not become whtespace), only applied if full_process is true as well, default true * @param {boolean} [options_p.collapseWhitespace] - Collapse consecutive white space during full_process, default true * @param {string} [options_p.wildcards] - characters that will be used as wildcards if provided * @param {number} [options_p.astral] - Use astral aware calculation * @param {string} [options_p.normalize] - Normalize unicode representations * @returns {number} - the levenshtein ratio (0-100). */ var options = clone_and_set_option_defaults(options_p); str1 = options.full_process ? full_process(str1, options) : str1; str2 = options.full_process ? full_process(str2, options) : str2; if (!validate(str1)) return 0; if (!validate(str2)) return 0; return _partial_ratio(str1, str2, options); } function token_set_ratio(str1, str2, options_p) { /** * Calculate token set ratio of the two strings. * * @function token_set_ratio * @param {string} str1 - the first string. * @param {string} str2 - the second string. * @param {Object} [options_p] - Additional options. * @param {boolean} [options_p.useCollator] - Use `Intl.Collator` for locale-sensitive string comparison. * @param {boolean} [options_p.full_process] - Apply basic cleanup, non-alphanumeric to whitespace etc. if true. default true * @param {boolean} [options_p.force_ascii] - Strip non-ascii in full_process if true (non-ascii will not become whtespace), only applied if full_process is true as well, default true * @param {boolean} [options_p.trySimple] - try simple/partial ratio as part of (parial_)token_set_ratio test suite * @param {boolean} [options_p.sortBySimilarity] - sort tokens by similarity to each other before combining instead of alphabetically * @param {string} [options_p.wildcards] - characters that will be used as wildcards if provided * @param {number} [options_p.astral] - Use astral aware calculation * @param {string} [options_p.normalize] - Normalize unicode representations * @returns {number} - the levenshtein ratio (0-100). */ var options = clone_and_set_option_defaults(options_p); str1 = options.full_process ? full_process(str1, options) : str1; str2 = options.full_process ? full_process(str2, options) : str2; if (!validate(str1)) return 0; if (!validate(str2)) return 0; return _token_set(str1, str2, options); } function partial_token_set_ratio(str1, str2, options_p) { /** * Calculate partial token ratio of the two strings. * * @function partial_token_set_ratio * @param {string} str1 - the first string. * @param {string} str2 - the second string. * @param {Object} [options_p] - Additional options. * @param {boolean} [options_p.useCollator] - Use `Intl.Collator` for locale-sensitive string comparison. * @param {boolean} [options_p.full_process] - Apply basic cleanup, non-alphanumeric to whitespace etc. if true. default true * @param {boolean} [options_p.force_ascii] - Strip non-ascii in full_process if true (non-ascii will not become whtespace), only applied if full_process is true as well, default true * @param {boolean} [options_p.trySimple] - try simple/partial ratio as part of (parial_)token_set_ratio test suite * @param {boolean} [options_p.sortBySimilarity] - sort tokens by similarity to each other before combining instead of alphabetically * @param {string} [options_p.wildcards] - characters that will be used as wildcards if provided * @param {number} [options_p.astral] - Use astral aware calculation * @param {string} [options_p.normalize] - Normalize unicode representations * @returns {number} - the levenshtein ratio (0-100). */ var options = clone_and_set_option_defaults(options_p); str1 = options.full_process ? full_process(str1, options) : str1; str2 = options.full_process ? full_process(str2, options) : str2; if (!validate(str1)) return 0; if (!validate(str2)) return 0; options.partial = true; return _token_set(str1, str2, options); } function token_sort_ratio(str1, str2, options_p) { /** * Calculate token sort ratio of the two strings. * * @function token_sort_ratio * @param {string} str1 - the first string. * @param {string} str2 - the second string. * @param {Object} [options_p] - Additional options. * @param {boolean} [options_p.useCollator] - Use `Intl.Collator` for locale-sensitive string comparison. * @param {boolean} [options_p.full_process] - Apply basic cleanup, non-alphanumeric to whitespace etc. if true. default true * @param {boolean} [options_p.force_ascii] - Strip non-ascii in full_process if true (non-ascii will not become whtespace), only applied if full_process is true as well, default true * @param {string} [options_p.wildcards] - characters that will be used as wildcards if provided * @param {number} [options_p.astral] - Use astral aware calculation * @param {string} [options_p.normalize] - Normalize unicode representations * @returns {number} - the levenshtein ratio (0-100). */ var options = clone_and_set_option_defaults(options_p); str1 = options.full_process ? full_process(str1, options) : str1; str2 = options.full_process ? full_process(str2, options) : str2; if (!validate(str1)) return 0; if (!validate(str2)) return 0; if (!options.proc_sorted) { str1 = process_and_sort(str1); str2 = process_and_sort(str2); } return _ratio(str1, str2, options); } function partial_token_sort_ratio(str1, str2, options_p) { /** * Calculate partial token sort ratio of the two strings. * * @function partial_token_sort_ratio * @param {string} str1 - the first string. * @param {string} str2 - the second string. * @param {Object} [options_p] - Additional options. * @param {boolean} [options_p.useCollator] - Use `Intl.Collator` for locale-sensitive string comparison. * @param {boolean} [options_p.full_process] - Apply basic cleanup, non-alphanumeric to whitespace etc. if true. default true * @param {boolean} [options_p.force_ascii] - Strip non-ascii in full_process if true (non-ascii will not become whtespace), only applied if full_process is true as well, default true * @param {string} [options_p.wildcards] - characters that will be used as wildcards if provided * @param {number} [options_p.astral] - Use astral aware calculation * @param {string} [options_p.normalize] - Normalize unicode representations * @returns {number} - the levenshtein ratio (0-100). */ var options = clone_and_set_option_defaults(options_p); str1 = options.full_process ? full_process(str1, options) : str1; str2 = options.full_process ? full_process(str2, options) : str2; if (!validate(str1)) return 0; if (!validate(str2)) return 0; options.partial = true; if (!options.proc_sorted) { str1 = process_and_sort(str1); str2 = process_and_sort(str2); } return _partial_ratio(str1, str2, options); } function token_similarity_sort_ratio(str1, str2, options_p) { /** * Calculate token sort ratio of the two strings. * * @function token_similarity_sort_ratio * @param {string} str1 - the first string. * @param {string} str2 - the second string. * @param {Object} [options_p] - Additional options. * @param {boolean} [options_p.useCollator] - Use `Intl.Collator` for locale-sensitive string comparison. * @param {boolean} [options_p.full_process] - Apply basic cleanup, non-alphanumeric to whitespace etc. if true. default true * @param {boolean} [options_p.force_ascii] - Strip non-ascii in full_process if true (non-ascii will not become whtespace), only applied if full_process is true as well, default true * @param {string} [options_p.wildcards] - characters that will be used as wildcards if provided * @param {number} [options_p.astral] - Use astral aware calculation * @param {string} [options_p.normalize] - Normalize unicode representations * @returns {number} - the levenshtein ratio (0-100). */ var options = clone_and_set_option_defaults(options_p); str1 = options.full_process ? full_process(str1, options) : str1; str2 = options.full_process ? full_process(str2, options) : str2; if (!validate(str1)) return 0; if (!validate(str2)) return 0; return _token_similarity_sort_ratio(str1, str2, options); } function partial_token_similarity_sort_ratio(str1, str2, options_p) { /** * Calculate token sort ratio of the two strings. * * @function partial_token_similarity_sort_ratio * @param {string} str1 - the first string. * @param {string} str2 - the second string. * @param {Object} [options_p] - Additional options. * @param {boolean} [options_p.useCollator] - Use `Intl.Collator` for locale-sensitive string comparison. * @param {boolean} [options_p.full_process] - Apply basic cleanup, non-alphanumeric to whitespace etc. if true. default true * @param {boolean} [options_p.force_ascii] - Strip non-ascii in full_process if true (non-ascii will not become whtespace), only applied if full_process is true as well, default true * @param {string} [options_p.wildcards] - characters that will be used as wildcards if provided * @param {number} [options_p.astral] - Use astral aware calculation * @param {string} [options_p.normalize] - Normalize unicode representations * @returns {number} - the levenshtein ratio (0-100). */ var options = clone_and_set_option_defaults(options_p); str1 = options.full_process ? full_process(str1, options) : str1; str2 = options.full_process ? full_process(str2, options) : str2; if (!validate(str1)) return 0; if (!validate(str2)) return 0; options.partial = true; return _token_similarity_sort_ratio(str1, str2, options); } function WRatio(str1, str2, options_p) { /** * Calculate weighted ratio of the two strings, taking best score of various methods. * * @function WRatio * @param {string} str1 - the first string. * @param {string} str2 - the second string. * @param {Object} [options_p] - Additional options. * @param {boolean} [options_p.useCollator] - Use `Intl.Collator` for locale-sensitive string comparison. * @param {boolean} [options_p.full_process] - Apply basic cleanup, non-alphanumeric to whitespace etc. if true. default true * @param {boolean} [options_p.force_ascii] - Strip non-ascii in full_process if true (non-ascii will not become whtespace), only applied if full_process is true as well, default true * @param {boolean} [options_p.collapseWhitespace] - Collapse consecutive white space during full_process, default true * @param {string} [options_p.wildcards] - characters that will be used as wildcards if provided * @param {number} [options_p.astral] - Use astral aware calculation * @param {string} [options_p.normalize] - Normalize unicode representations * @returns {number} - the levenshtein ratio (0-100). */ var options = clone_and_set_option_defaults(options_p); //str1 = full_process(str1, options); //fuzzywuzzy runs no matter what, reason? going by options.full_process //str2 = full_process(str2, options); str1 = options.full_process ? full_process(str1, options) : str1; str2 = options.full_process ? full_process(str2, options) : str2; options.full_process = false; if (!validate(str1)) return 0; if (!validate(str2)) return 0; var try_partial = true; var unbase_scale = .95; var partial_scale = .90; var base = _ratio(str1, str2, options); var len_ratio = Math.max(str1.length, str2.length)/Math.min(str1.length, str2.length); if (len_ratio < 1.5) try_partial = false; if (len_ratio > 8) partial_scale = .6; if (try_partial) { var partial = _partial_ratio(str1, str2, options) * partial_scale; var ptsor = partial_token_sort_ratio(str1, str2, options) * unbase_scale * partial_scale; var ptser = partial_token_set_ratio(str1, str2, options) * unbase_scale * partial_scale; return Math.round(Math.max(base, partial, ptsor, ptser)); } else { var tsor = token_sort_ratio(str1, str2, options) * unbase_scale; var tser = token_set_ratio(str1, str2, options) * unbase_scale; return Math.round(Math.max(base, tsor, tser)); } } function extract(query, choices, options_p) { /** * Return the top scoring items from an array (or assoc array) of choices * * @function extract * @param query - the search term. * @param {String[]|Object[]|Object} choices - array of strings, or array of choice objects if processor is supplied, or object of form {key: choice} * @param {Object} [options_p] - Additional options. * @param {function} [options_p.scorer] - takes two values and returns a score, will be passed options as 3rd argument * @param {function} [options_p.processor] - takes each choice and outputs a value to be used for Scoring * @param {number} [options_p.limit] - optional max number of results to return, returns all if not supplied * @param {number} [options_p.cutoff] - minimum score that will get returned 0-100 * @param {boolean} [options_p.useCollator] - Use `Intl.Collator` for locale-sensitive string comparison. * @param {number} [options_p.astral] - Use astral aware calculation * @param {string} [options_p.normalize] - Normalize unicode representations * @param {boolean} [options_p.full_process] - Apply basic cleanup, non-alphanumeric to whitespace etc. if true. default true * @param {boolean} [options_p.force_ascii] - Strip non-ascii in full_process if true (non-ascii will not become whtespace), only applied if full_process is true as well, default false * @param {boolean} [options_p.collapseWhitespace] - Collapse consecutive white space during full_process, default true * @param {boolean} [options_p.trySimple] - try simple/partial ratio as part of (parial_)token_set_ratio test suite * @param {boolean} [options_p.sortBySimilarity] - sort tokens by similarity to each other before combining instead of alphabetically * @param {string} [options_p.wildcards] - characters that will be used as wildcards if provided * @param {boolean} [options_p.returnObjects] - return array of object instead of array of tuples; default false * @returns {Array[] | Object[]} - array of choice results with their computed ratios (0-100). */ var options = clone_and_set_option_defaults(options_p); var numchoices; if (_isArray(choices)) { numchoices = choices.length; } else if (!(choices instanceof Object)) { throw new Error("Invalid choices"); } else numchoices = _keys(choices).length; if (!choices || numchoices === 0) { if (typeof console !== undefined) console.warn("No choices"); return []; } if (options.processor && typeof options.processor !== "function") { throw new Error("Invalid Processor"); } if (!options.processor) options.processor = function (x) { return x; } if (options.scorer && typeof options.scorer !== "function") { throw new Error("Invalid Scorer"); } if (!options.scorer) { options.scorer = QRatio; } var isCustom = isCustomFunc(options.scorer); // check if func name is one of fuzzball's, so don't use same names.. if (!options.cutoff || typeof options.cutoff !== "number") { options.cutoff = -1;} var pre_processor = function(choice, force_ascii) {return choice;} if (options.full_process) { pre_processor = full_process; if (!isCustom) options.processed = true; // to let wildcardLeven know and not run again after we set fp to false below } var normalize = false; if (!isCustom) { // if custom scorer func let scorer handle it query = pre_processor(query, options); options.full_process = false; if (options.astral && options.normalize) { options.normalize = false; // don't normalize again in ratio if doing here if (String.prototype.normalize) { normalize = true query = query.normalize(); } else { if (typeof console !== undefined) console.warn("Normalization not supported in your environment"); } } if (query.length === 0) if (typeof console !== undefined) console.warn("Processed query is empty string"); } var results = []; var anyblank = false; var tsort = false; var tset = false; if (options.scorer.name === "token_sort_ratio" || options.scorer.name === "partial_token_sort_ratio") { var proc_sorted_query = process_and_sort(query); tsort = true; } else if (options.scorer.name === "token_set_ratio" || options.scorer.name === "partial_token_set_ratio") { var query_tokens = tokenize(query, options); tset = true; } var result, mychoice, cmpHeap, cmpSort; if (options.returnObjects) { cmpHeap = function (a, b) { return a.score - b.score; }; cmpSort = function (a, b) { return b.score - a.score; }; } else { cmpHeap = function (a, b) { return a[1] - b[1]; }; cmpSort = function (a, b) { return b[1] - a[1]; }; } _forEach(choices, function (value, key) { options.tokens = undefined; options.proc_sorted = false; if (tsort) { options.proc_sorted = true; if (value && value.proc_sorted) mychoice = value.proc_sorted; else { mychoice = pre_processor(options.processor(value), options); mychoice = process_and_sort(normalize ? mychoice.normalize() : mychoice); } result = options.scorer(proc_sorted_query, mychoice, options); } else if (tset) { mychoice = "x"; //dummy string so it validates, if either tokens is [] all 3 tests will still be 0 if (value && value.tokens) { options.tokens = [query_tokens, value.tokens]; if (options.trySimple) mychoice = pre_processor(options.processor(value), options); } else { mychoice = pre_processor(options.processor(value), options); options.tokens = [query_tokens, tokenize((normalize ? mychoice.normalize() : mychoice), options)] } //query and mychoice only used for validation here unless trySimple = true result = options.scorer(query, mychoice, options); } else if (isCustom) { // options.full_process should be unmodified, don't pre-process here since mychoice maybe not string mychoice = options.processor(value); result = options.scorer(query, mychoice, options); } else { mychoice = pre_processor(options.processor(value), options); if (typeof mychoice !== "string" || mychoice.length === 0) anyblank = true; if (normalize && typeof mychoice === "string") mychoice = mychoice.normalize(); result = options.scorer(query, mychoice, options); } if (result > options.cutoff) { if (options.returnObjects) results.push({choice: value, score: result, key: key}); else results.push([value, result, key]); } }); if (anyblank) if (typeof console !== undefined) console.log("One or more choices were empty. (post-processing if applied)") if (options.limit && typeof options.limit === "number" && options.limit > 0 && options.limit < numchoices && !options.unsorted) { results = Heap.nlargest(results, options.limit, cmpHeap); } else if (!options.unsorted) { results = results.sort(cmpSort); } return results; } function extractAsync(query, choices, options_p, callback) { /** * Return the top scoring items from an array (or assoc array) of choices * * @function extractAsync * @param query - the search term. * @param {String[]|Object[]|Object} choices - array of strings, or array of choice objects if processor is supplied, or object of form {key: choice} * @param {Object} [options_p] - Additional options. * @param {function} [options_p.scorer] - takes two values and returns a score, will be passed options as 3rd argument * @param {function} [options_p.processor] - takes each choice and outputs a value to be used for Scoring * @param {number} [options_p.limit] - optional max number of results to return, returns all if not supplied * @param {number} [options_p.cutoff] - minimum score that will get returned 0-100 * @param {boolean} [options_p.useCollator] - Use `Intl.Collator` for locale-sensitive string comparison. * @param {number} [options_p.astral] - Use astral aware calculation * @param {string} [options_p.normalize] - Normalize unicode representations * @param {boolean} [options_p.full_process] - Apply basic cleanup, non-alphanumeric to whitespace etc. if true. default true * @param {boolean} [options_p.force_ascii] - Strip non-ascii in full_process if true (non-ascii will not become whtespace), only applied if full_process is true as well, default false * @param {boolean} [options_p.collapseWhitespace] - Collapse consecutive white space during full_process, default true * @param {boolean} [options_p.trySimple] - try simple/partial ratio as part of (parial_)token_set_ratio test suite * @param {boolean} [options_p.sortBySimilarity] - sort tokens by similarity to each other before combining instead of alphabetically * @param {string} [options_p.wildcards] - characters that will be used as wildcards if provided * @param {boolean} [options_p.returnObjects] - return array of object instead of array of tuples; default false * @param {Object} [options_p.abortController] - track abortion * @param {Object} [options_p.cancelToken] - track cancellation * @param {number} [options_p.asyncLoopOffset] - number of rows to run in between every async loop iteration, default 256 * @param {function} callback - node style callback (err, arrayOfResults) */ var options = clone_and_set_option_defaults(options_p); var abortController; if (typeof options_p.abortController === "object") { abortController = options_p.abortController; } var cancelToken; if (typeof options_p.cancelToken === "object") { cancelToken = options_p.cancelToken; } var loopOffset = 256; if (typeof options.asyncLoopOffset === 'number') { if (options.asyncLoopOffset < 1) loopOffset = 1; else loopOffset = options.asyncLoopOffset; } var isArray = false; var numchoices; if (choices && choices.length && _isArray(choices)) { numchoices = choices.length; isArray = true; //if array don't check hasOwnProperty every time below } else if (!(choices instanceof Object)) { callback(new Error("Invalid choices")); return; } else numchoices = Object.keys(choices).length; if (!choices || numchoices === 0) { if (typeof console !== undefined) console.warn("No choices"); callback(null, []); return; } if (options.processor && typeof options.processor !== "function") { callback(new Error("Invalid Processor")); return; } if (!options.processor) options.processor = function (x) { return x; } if (options.scorer && typeof options.scorer !== "function") { callback(new Error("Invalid Scorer")); return; } if (!options.scorer) { options.scorer = QRatio; } var isCustom = isCustomFunc(options.scorer); // check if func name is one of fuzzball's, so don't use same names.. if (!options.cutoff || typeof options.cutoff !== "number") { options.cutoff = -1; } var pre_processor = function (choice, force_ascii) { return choice; } if (options.full_process) { pre_processor = full_process; if (!isCustom) options.processed = true; // to let wildcardLeven know and not run again after we set fp to false below } var normalize = false; if (!isCustom) { // if custom scorer func let scorer handle it query = pre_processor(query, options); options.full_process = false; if (options.astral && options.normalize) { options.normalize = false; // don't normalize again in ratio if doing here if (String.prototype.normalize) { normalize = true query = query.normalize(); } else { if (typeof console !== undefined) console.warn("Normalization not supported in your environment"); } } if (query.length === 0) if (typeof console !== undefined) console.warn("Processed query is empty string"); } var results = []; var anyblank = false; var tsort = false; var tset = false; if (options.scorer.name === "token_sort_ratio" || options.scorer.name === "partial_token_sort_ratio") { var proc_sorted_query = process_and_sort(query); tsort = true; } else if (options.scorer.name === "token_set_ratio" || options.scorer.name === "partial_token_set_ratio") { var query_tokens = tokenize(query, options); tset = true; } var idx, mychoice, result, cmpHeap, cmpSort; if (options.returnObjects) { cmpHeap = function (a, b) { return a.score - b.score; }; cmpSort = function (a, b) { return b.score - a.score; }; } else { cmpHeap = function (a, b) { return a[1] - b[1]; }; cmpSort = function (a, b) { return b[1] - a[1]; }; } var keys = Object.keys(choices); isArray ? searchLoop(0) : searchLoop(keys[0], 0); function searchLoop(c, i) { if (isArray || choices.hasOwnProperty(c)) { options.tokens = undefined; options.proc_sorted = false; if (tsort) { options.proc_sorted = true; if (choices[c] && choices[c].proc_sorted) mychoice = choices[c].proc_sorted; else { mychoice = pre_processor(options.processor(choices[c]), options); mychoice = process_and_sort(normalize ? mychoice.normalize() : mychoice); } result = options.scorer(proc_sorted_query, mychoice, options); } else if (tset) { mychoice = "x"; //dummy string so it validates if (choices[c] && choices[c].tokens) { options.tokens = [query_tokens, choices[c].tokens]; if (options.trySimple) mychoice = pre_processor(options.processor(choices[c]), options); } else { mychoice = pre_processor(options.processor(choices[c]), options); options.tokens = [query_tokens, tokenize((normalize ? mychoice.normalize() : mychoice), options)] } //query and mychoice only used for validation here unless trySimple = true result = options.scorer(query, mychoice, options); } else if (isCustom) { // options.full_process should be unmodified, don't pre-process here since mychoice maybe not string mychoice = options.processor(choices[c]); result = options.scorer(query, mychoice, options); } else { mychoice = pre_processor(options.processor(choices[c]), options); if (typeof mychoice !== "string" || mychoice.length === 0) anyblank = true; if (normalize && typeof mychoice === "string") mychoice = mychoice.normalize(); result = options.scorer(query, mychoice, options); } if (isArray) idx = parseInt(c); else idx = c; if (result > options.cutoff) { if (options.returnObjects) results.push({ choice: choices[c], score: result, key: idx }); else results.push([choices[c], result, idx]); } } if (abortController && abortController.signal.aborted === true) { callback(new Error("aborted")); return; } if (cancelToken && cancelToken.canceled === true) { callback(new Error("canceled")); return; } if (isArray && c < choices.length - 1) { if (c % loopOffset === 0) { setImmediate(function () { searchLoop(c + 1) }); } else { searchLoop(c + 1); } } else if (i < keys.length - 1) { if (i % loopOffset === 0) {setImmediate(function () { searchLoop(keys[i + 1], i + 1); }); } else { searchLoop(keys[i + 1], i + 1); } } else { if (anyblank) if (typeof console !== undefined) console.log("One or more choices were empty. (post-processing if applied)") if (options.limit && typeof options.limit === "number" && options.limit > 0 && options.limit < numchoices && !options.unsorted) { results = Heap.nlargest(results, options.limit, cmpHeap); } else if (!options.unsorted) { results = results.sort(cmpSort); } callback(null, results); } } } /** Main Scoring Code */ function _cosineSim(v1, v2, options) { var keysV1 = Object.keys(v1); var keysV2 = Object.keys(v2); var intersection = _intersect(keysV1, keysV2); var prods = intersection.map(function (x) { return v1[x] * v2[x]; }) var numerator = prods.reduce(function(acc, x) { return acc + x; }, 0); var v1Prods = keysV1.map(function (x) { return Math.pow(v1[x], 2); }); var v1sum = v1Prods.reduce(function(acc, x) { return acc + x; }, 0); var v2Prods = keysV2.map(function (x) { return Math.pow(v2[x], 2); }); var v2sum = v2Prods.reduce(function(acc, x) { return acc + x; }, 0); var denominator = Math.sqrt(v1sum) * Math.sqrt(v2sum); return numerator / denominator; } var WILDCARD_KEY = "%*SuperUniqueWildcardKey*%"; var normalWarnCharCounts = false; function _getCharacterCounts(str, options) { var normalString = str; if (options.astral) { if (options.normalize) { if (String.prototype.normalize) { normalString = str.normalize(); } else { if (!normalWarnCharCounts) { if (typeof console !== undefined) console.warn("Normalization not supported in your environment"); normalWarnCharCounts = true; } } } var charArray = _toArray(normalString) } else { var charArray = normalString.split(""); } var charCounts = {}; if (options.wildcards) { for (var i = 0; i < charArray.length; i++) { var char = charArray[i]; if (options.wildcards.indexOf(char) > -1) { if (charCounts[WILDCARD_KEY]) { charCounts[WILDCARD_KEY] += 1 } else { charCounts[WILDCARD_KEY] = 1; } } else if (charCounts[char]) { charCounts[char] += 1 } else { charCounts[char] = 1; } } } else { for (var i = 0; i < charArray.length; i++) { var char = charArray[i]; if (charCounts[char]) { charCounts[char] += 1 } else { charCounts[char] = 1; } } } return charCounts; } // Sort sorted2 according to similarity to sorted1 function _token_similarity_sort(sorted1, sorted2, options) { var oldSorted2 = sorted2; var charCounts1 = sorted1.reduce(function(acc, str) { acc[str] = _getCharacterCounts(str, options); return acc; }, {}); var charCounts2 = oldSorted2.reduce(function(acc, str) { acc[str] = _getCharacterCounts(str, options); return acc; }, {}); var newSorted2 = []; var i = 0; while (oldSorted2.length && i < sorted1.length) { // most similar to first token in s1, 2nd token, ... n tokens // sort by similarity to sorted1[i], take most similar var sim = _orderBy(oldSorted2, function (x) { return _cosineSim(charCounts1[sorted1[i]], charCounts2[x]) }, 'desc')[0]; newSorted2.push(sim); i++; oldSorted2 = oldSorted2.filter(function (token) { return token !== sim}); } // if oldSorted2 is longer, append it to the end return newSorted2.concat(oldSorted2); } function _order_token_lists (str1, tokens1, str2, tokens2) { // To keep consistent ordering, assume shortest number of tokens, then str.length, // is more significant, else fallback to sort alphabetacally var first = tokens1; var second = tokens2; if (tokens1.length > tokens2.length) { first = tokens2; second = tokens1; } else if (tokens1.length === tokens2.length) { if (str1.length > str2.length) { first = tokens2; second = tokens1; } else { var sortedStrings = [str1, str2].sort(); if (sortedStrings[0] === str2) { first = tokens2; second = tokens1; } } } return [first, second]; } function _token_similarity_sort_ratio (str1, str2, options) { if (!options.tokens) { var tokens1 = tokenize(str1, options); var tokens2 = tokenize(str2, options); } else { var tokens1 = options.tokens[0]; var tokens2 = options.tokens[1]; } var sorted1 = tokens1.sort(); var sorted2 = tokens2.sort(); var orderedTokenLists = _order_token_lists(str1, sorted1, str2, sorted2); var first = orderedTokenLists[0]; var second = orderedTokenLists[1]; const newSecond = _token_similarity_sort(first, second, options); if (!options.partial) { return _ratio(first.join(" "), newSecond.join(" "), options); } else { return _partial_ratio(first.join(" "), newSecond.join(" "), options); } } function _token_set(str1, str2, options) { if (!options.tokens) { var tokens1 = tokenize(str1, options); var tokens2 = tokenize(str2, options); } else { var tokens1 = options.tokens[0]; var tokens2 = options.tokens[1]; } if (options.wildcards) { var partWild = _partialRight(wildleven, options, leven); var wildCompare = function (a, b) { return partWild(a, b) === 0; } var intersection = _intersectWith(tokens1, tokens2, wildCompare); var diff1to2 = _differenceWith(tokens1, tokens2, wildCompare); var diff2to1 = _differenceWith(tokens2, tokens1, wildCompare); } else { var intersection = _intersect(tokens1, tokens2); var diff1to2 = _difference(tokens1, tokens2); var diff2to1 = _difference(tokens2, tokens1); } var sorted_sect = intersection.sort().join(" "); var sorted_1to2List = diff1to2.sort(); var sorted_2to1List = diff2to1.sort(); if (options.sortBySimilarity) { var orderedTokenLists = _order_token_lists(str1, sorted_1to2List, str2, sorted_2to1List); var first = orderedTokenLists[0]; var second = orderedTokenLists[1]; var sorted_1to2 = first.join(" "); var sorted_2to1 = _token_similarity_sort(first, second, options).join(" "); } else { var sorted_1to2 = sorted_1to2List.join(" "); var sorted_2to1 = sorted_2to1List.join(" "); } var combined_1to2 = sorted_sect + " " + sorted_1to2; var combined_2to1 = sorted_sect + " " + sorted_2to1; sorted_sect = sorted_sect.trim(); combined_1to2 = combined_1to2.trim(); combined_2to1 = combined_2to1.trim(); var ratio_func = _ratio; if (options.partial) { ratio_func = _partial_ratio; if (sorted_sect.length > 0) return 100; // will always be 100 anyway } var pairwise = [ ratio_func(sorted_sect, combined_1to2, options), ratio_func(sorted_sect, combined_2to1, options), ratio_func(combined_1to2, combined_2to1, options) ] if (options.trySimple) { pairwise.push(ratio_func(str1, str2, options)); } return Math.max.apply(null, pairwise); } var normalWarn = false; function _ratio(str1, str2, options) { if (!validate(str1)) return 0; if (!validate(str2)) return 0; if (options.ratio_alg && options.ratio_alg === "difflib") { var m = new SequenceMatcher(null, str1, str2); var r = m.ratio(); return Math.round(100 * r); } //to match behavior of python-Levenshtein/fuzzywuzzy, substitution cost is 2 if not specified, or would default to 1 if (typeof options.subcost === "undefined") options.subcost = 2; var levdistance, lensum; if (options.astral) { if (options.normalize) { if (String.prototype.normalize) { str1 = str1.normalize(); str2 = str2.normalize(); } else { if (!normalWarn) { if (typeof console !== undefined) console.warn("Normalization not supported in your environment"); normalWarn = true; } } } levdistance = iLeven(str1, str2, options, _toArray); lensum = _toArray(str1).length + _toArray(str2).length } else { if (!options.wildcards) { levdistance = leven(str1, str2, options); lensum = str1.length + str2.length; } else { levdistance = wildleven(str1, str2, options, leven); // falls back to leven if invalid lensum = str1.length + str2.length; } } return Math.round(100 * ((lensum - levdistance)/lensum)); } function _partial_ratio(str1, str2, options) { if (!validate(str1)) return 0; if (!validate(str2)) return 0; if (str1.length <= str2.length) { var shorter = str1 var longer = str2 } else { var shorter = str2 var longer = str1 } var m = new SequenceMatcher(null, shorter, longer); var blocks = m.getMatchingBlocks(); var scores = []; for (var b = 0; b < blocks.length; b++) { var long_start = (blocks[b][1] - blocks[b][0]) > 0 ? (blocks[b][1] - blocks[b][0]) : 0; var long_end = long_start + shorter.length; var long_substr = longer.substring(long_start,long_end); var r = _ratio(shorter,long_substr,options); if (r > 99.5) return 100; else scores.push(r); } return Math.max.apply(null, scores); } //polyfill for Object.keys // From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys if (!Object.keys) { Object.keys = (function () { 'use strict'; var hasOwnProperty = Object.prototype.hasOwnProperty, hasDontEnumBug = !({ toString: null }).propertyIsEnumerable('toString'), dontEnums = [ 'toString', 'toLocaleString', 'valueOf', 'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable', 'constructor' ], dontEnumsLength = dontEnums.length; return function (obj) { if (typeof obj !== 'object' && (typeof obj !== 'function' || obj === null)) { throw new TypeError('Object.keys called on non-object'); } var result = [], prop, i; for (prop in obj) { if (hasOwnProperty.call(obj, prop)) { result.push(prop); } } if (hasDontEnumBug) { for (i = 0; i < dontEnumsLength; i++) { if (hasOwnProperty.call(obj, dontEnums[i])) { result.push(dontEnums[i]); } } } return result; }; } ()); } var extractAsPromised = undefined; if (typeof Promise !== 'undefined') { extractAsPromised = function(query, choices, options) { return new Promise(function(resolve, reject){ extractAsync(query, choices, options, function(err, response){ if (err) reject(err); else resolve(response); }); }); }; } var fuzzball = { distance: distance, ratio: QRatio, partial_ratio: partial_ratio, token_set_ratio: token_set_ratio, token_sort_ratio: token_sort_ratio, partial_token_set_ratio: partial_token_set_ratio, partial_token_sort_ratio: partial_token_sort_ratio, token_similarity_sort_ratio: token_similarity_sort_ratio, partial_token_similarity_sort_ratio: partial_token_similarity_sort_ratio, WRatio: WRatio, full_process: full_process, extract: extract, extractAsync: extractAsync, extractAsPromised: extractAsPromised, process_and_sort: process_and_sort, unique_tokens: tokenize, dedupe: dedupe }; module.exports = fuzzball; } ());