liquidMetal = (function() { var SCORE_NO_MATCH = 0.0; var SCORE_MATCH = 1.0; var SCORE_TRAILING = 0.8; var SCORE_TRAILING_BUT_STARTED = 0.9; var SCORE_BUFFER = 0.85; var WORD_SEPARATORS = " \t_-"; return { lastScore: null, lastScoreArray: null, score: function(string, abbrev) { // short circuits if (abbrev.length === 0) return SCORE_TRAILING; if (abbrev.length > string.length) return SCORE_NO_MATCH; // match & score all var allScores = []; var search = string.toLowerCase(); abbrev = abbrev.toLowerCase(); this._scoreAll(string, search, abbrev, -1, 0, [], allScores); // complete miss if (allScores.length == 0) return 0; // sum per-character scores into overall scores, // selecting the maximum score var maxScore = 0.0, maxArray = []; for (var i = 0; i < allScores.length; i++) { var scores = allScores[i]; var scoreSum = 0.0; for (var j = 0; j < string.length; j++) { scoreSum += scores[j]; } if (scoreSum > maxScore) { maxScore = scoreSum; maxArray = scores; } } // normalize max score by string length // s. t. the perfect match score = 1 maxScore /= string.length; // record maximum score & score array, return this.lastScore = maxScore; this.lastScoreArray = maxArray; return maxScore; }, _scoreAll: function(string, search, abbrev, searchIndex, abbrIndex, scores, allScores) { // save completed match scores at end of search if (abbrIndex == abbrev.length) { // add trailing score for the remainder of the match var started = (search.charAt(0) == abbrev.charAt(0)); var trailScore = started ? SCORE_TRAILING_BUT_STARTED : SCORE_TRAILING; fillArray(scores, trailScore, scores.length, string.length); // save score clone (since reference is persisted in scores) allScores.push(scores.slice(0)); return; } // consume current char to match var c = abbrev.charAt(abbrIndex); abbrIndex++; // cancel match if a character is missing var index = search.indexOf(c, searchIndex); if (index == -1) return; // match all instances of the abbreviaton char var scoreIndex = searchIndex; // score section to update while ((index = search.indexOf(c, searchIndex+1)) != -1) { // score this match according to context if (isNewWord(string, index)) { scores[index-1] = 1; fillArray(scores, SCORE_BUFFER, scoreIndex+1, index-1); } else if (isUpperCase(string, index)) { fillArray(scores, SCORE_BUFFER, scoreIndex+1, index); } else { fillArray(scores, SCORE_NO_MATCH, scoreIndex+1, index); } scores[index] = SCORE_MATCH; // consume matched string and continue search searchIndex = index; this._scoreAll(string, search, abbrev, searchIndex, abbrIndex, scores, allScores); } } }; function isUpperCase(string, index) { var c = string.charAt(index); return ("A" <= c && c <= "Z"); } function isNewWord(string, index) { var c = string.charAt(index-1); return (WORD_SEPARATORS.indexOf(c) != -1); } function fillArray(array, value, from, to) { for (var i = from; i < to; i++) { array[i] = value; } return array; } })();