UNPKG

talisman

Version:

Straightforward fuzzy matching, information retrieval and NLP building blocks for JavaScript.

243 lines (193 loc) 6.65 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: false }); exports.custom = custom; /** * Talisman metrics/sift4 * ======================= * * Implementation of the SIFT4 distance which is a linear time approximation of * the Levenshtein or Damerau-Levenshtein distance. * * [Reference]: * http://web.archive.org/web/20190613223908/https://siderite.blogspot.com/2014/11/super-fast-and-accurate-string-distance.html * * [Author]: Siderite Zackwehdex * * [Tags]: string metric, asymmetric. */ // TODO: implement options of the most complex version. /** * Defaults. */ var DEFAULTS = { transpositions: false, maxOffset: 5 }; /** * Simplest version of the SIFT4 algorithm. * * @param {number} maxOffset - Search window. * @param {number} maxDistance - Maximum distance before exiting. * @param {string|array} a - First sequence. * @param {string|array} b - Second sequence. * @return {number} - The distance. */ function withoutTranspositions(maxOffset, maxDistance, a, b) { // Early termination if (a === b) return 0; var la = a.length, lb = b.length; if (!la || !lb) return Math.max(la, lb); var cursorA = 0, cursorB = 0, longestCommonSubsequence = 0, localCommonSubstring = 0; while (cursorA < la && cursorB < lb) { if (a[cursorA] === b[cursorB]) { localCommonSubstring++; } else { longestCommonSubsequence += localCommonSubstring; localCommonSubstring = 0; if (cursorA !== cursorB) cursorA = cursorB = Math.max(cursorA, cursorB); for (var i = 0; i < maxOffset && (cursorA + i < la || cursorB + i < lb); i++) { if (cursorA + i < la && a[cursorA + i] === b[cursorB]) { cursorA += i; localCommonSubstring++; break; } if (cursorB + i < lb && a[cursorA] === b[cursorB + i]) { cursorB += i; localCommonSubstring++; break; } } } cursorA++; cursorB++; if (maxDistance) { var tempDistance = Math.max(cursorA, cursorB) - longestCommonSubsequence; if (tempDistance === maxDistance) return maxDistance; if (tempDistance > maxDistance) return Infinity; } } longestCommonSubsequence += localCommonSubstring; return Math.max(la, lb) - longestCommonSubsequence; } /** * Version of the SIFT4 function computing transpositions. * * @param {number} maxOffset - Search window. * @param {number} maxDistance - Maximum distance before exiting. * @param {string|array} a - First sequence. * @param {string|array} b - Second sequence. * @return {number} - The distance. */ function withTranspositions(maxOffset, maxDistance, a, b) { // Early termination if (a === b) return 0; var la = a.length, lb = b.length; if (!la || !lb) return Math.max(la, lb); var cursorA = 0, cursorB = 0, longestCommonSubsequence = 0, localCommonSubstring = 0, transpositions = 0; var offsetArray = []; while (cursorA < la && cursorB < lb) { if (a[cursorA] === b[cursorB]) { localCommonSubstring++; var isTransposition = false, i = 0; while (i < offsetArray.length) { var offset = offsetArray[i]; if (cursorA <= offset.cursorA || cursorB <= offset.cursorB) { isTransposition = Math.abs(cursorB - cursorA) >= Math.abs(offset.cursorB - offset.cursorA); if (isTransposition) { transpositions++; } else { if (!offset.isTransposition) { offset.isTransposition = true; transpositions++; } } break; } else { // NOTE: we could marginally enhance the performance of the algo // by using an object rather than splicing the array if (cursorA > offset.cursorB && cursorB > offset.cursorA) offsetArray.splice(i, 1);else i++; } } offsetArray.push({ cursorA: cursorA, cursorB: cursorB, isTransposition: isTransposition }); } else { longestCommonSubsequence += localCommonSubstring; localCommonSubstring = 0; if (cursorA !== cursorB) cursorA = cursorB = Math.min(cursorA, cursorB); for (var _i = 0; _i < maxOffset && (cursorA + _i < la || cursorB + _i < lb); _i++) { if (cursorA + _i < la && a[cursorA + _i] === b[cursorB]) { cursorA += _i - 1; cursorB--; break; } if (cursorB + _i < lb && a[cursorA] === b[cursorB + _i]) { cursorA--; cursorB += _i - 1; break; } } } cursorA++; cursorB++; // NOTE: this was below maxDistance check in original implemenation but // this looked suspicious if (cursorA >= la || cursorB >= lb) { longestCommonSubsequence += localCommonSubstring; localCommonSubstring = 0; cursorA = cursorB = Math.min(cursorA, cursorB); } if (maxDistance) { var tempDistance = Math.max(cursorA, cursorB) - longestCommonSubsequence + transpositions; if (tempDistance === maxDistance) return maxDistance; if (tempDistance > maxDistance) return Infinity; } } longestCommonSubsequence += localCommonSubstring; return Math.max(la, lb) - longestCommonSubsequence + transpositions; } /** * Function computing the SIFT4 distance. * * @param {object} options - Options: * @param {boolean} [symmetric] - Symmetric version of the algorithm. * @param {number} [maxOffset] - Search window. * @param {number} [maxDistance] - Maximum distance before exiting. * @param {string|array} a - First sequence. * @param {string|array} b - Second sequence. * @return {number} - The distance. */ function custom(options, a, b) { var maxOffset = options.maxOffset || DEFAULTS.maxOffset, maxDistance = options.maxDistance, transpositions = options.transpositions === true, symmetric = options.symmetric === true; var fn = transpositions ? withTranspositions : withoutTranspositions, distance = fn(maxOffset, maxDistance, a, b); if (symmetric) { var reversedDistance = fn(maxOffset, maxDistance, b, a); return Math.min(distance, reversedDistance); } return distance; } /** * Exporting default function. */ var sift4 = custom.bind(null, {}); exports.default = sift4; module.exports = exports["default"]; exports["default"].custom = exports.custom;