122 lines
3.7 KiB
JavaScript
122 lines
3.7 KiB
JavaScript
/**
|
|
* LiquidMetal, version: 1.2.1 (2012-04-21)
|
|
*
|
|
* A mimetic poly-alloy of Quicksilver's scoring algorithm, essentially
|
|
* LiquidMetal.
|
|
*
|
|
* For usage and examples, visit:
|
|
* http://github.com/rmm5t/liquidmetal
|
|
*
|
|
* Licensed under the MIT:
|
|
* http://www.opensource.org/licenses/mit-license.php
|
|
*
|
|
* Copyright (c) 2009-2012, Ryan McGeary (ryan -[at]- mcgeary [*dot*] org)
|
|
*/
|
|
var 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;
|
|
}
|
|
})();
|