Skip to content

Instantly share code, notes, and snippets.

@cullenjohnson
Last active August 29, 2015 14:27
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cullenjohnson/7062e039b5fc46e5c1b8 to your computer and use it in GitHub Desktop.
Save cullenjohnson/7062e039b5fc46e5c1b8 to your computer and use it in GitHub Desktop.
Returns a compare function to be used in JavaScript's `Array.prototype.sort()` function that finds the string match of a search term nearest to the start of the word.
/** GetSearchTermRelevanceComparator
* Author: Cullen Johnson
* Github: https://github.com/cullenjohnson
* Website: cullenjohnson.ca
*
* Return an underscore sort comparator that can sort a list of strings based on the relevancy of the search term.
* Can be used to sort strings, objects with string properties, or Backbone objects with string properties.
*
* Search Priority: (Example search term: "FOO")
* 1. Exact match of search term ("FOO" > "super FOObar")
* 2. Nearest match of search term at the start of a word. ("super FOObar" > "super duper FOObar" > "kungFOO")
* 3. Match nearest to the start of the word. ("blablabla kungFOO" > "blablablaFOO")
*
* NOTE: When the search term contains a space, the function prioritizes the closest match, ignoring its word location.
* (Example: When the search term is "FOO BAR", "superFOO BAR" > "bla bla bla FOO BAR")
*
* Options:
* * searchTerm: the string to search for.
* * options
* caseSensitive: set to true if the search should be case sensitive. (False by default)
* propertyName: if the items being compared are objects, set this parameter to the name of the property to search
* for the term. (Leave null if the items being compared are strings.)
* isBackBoneObject: set to true if the items being compared are Backbone objects. (Leave null if the items being compared are strings.)
*/
getSearchTermRelevanceComparator = function(searchTerm, options) {
return function(a, b) {
var aWordList, bWordList, i,
compareMatchPosition,
aMatchingWordIndex = 0, aMatchPosition = null,
bMatchingWordIndex = 0, bMatchPosition = null,
caseSensitive = options.caseSensitive || false,
propertyName = options.propertyName || null,
isBackboneObject = options.isBackboneObject || false;
if (propertyName) {
if (isBackboneObject) {
a = a.get(propertyName);
b = b.get(propertyName);
} else {
a = a[propertyName];
b = b[propertyName];
}
}
if (!caseSensitive) {
a = a.toUpperCase();
b = b.toUpperCase();
searchTerm = searchTerm.toUpperCase();
}
// first of all, if a term does not contain the searchTerm, it is at the bottom.
if (a.indexOf(searchTerm) < 0 && b.indexOf(searchTerm) < 0) {
return 0;
} if (a.indexOf(searchTerm) < 0) {
return 1;
} else if (b.indexOf(searchTerm) < 0) {
return -1;
}
// top priority: exact match
if (a === searchTerm) {
return -1;
}
if (b === searchTerm) {
return 1;
}
// next priority: match at start of the word
// if the search term contains spaces, prioritize the match closer to the start of the entire string.
if (/\s/.test(searchTerm)) {
aMatchPosition = a.indexOf(searchTerm);
bMatchPosition = b.indexOf(searchTerm);
} else {
// split compared terms into collections of words.
aWordList = a.split(/\s+/g);
bWordList = b.split(/\s+/g);
// find the match that is closest to the start of the word for each collection of words.
for (i = 0; i < aWordList.length; ++i) {
compareMatchPosition = aWordList[i].indexOf(searchTerm);
if (compareMatchPosition >= 0 && (_.isNull(aMatchPosition) || compareMatchPosition < aMatchPosition )) {
aMatchingWordIndex = i;
aMatchPosition = compareMatchPosition
}
}
for (i = 0; i < bWordList.length; ++i) {
compareMatchPosition = bWordList[i].indexOf(searchTerm);
if (compareMatchPosition >= 0 && (_.isNull(bMatchPosition) || compareMatchPosition < bMatchPosition )) {
bMatchingWordIndex = i;
bMatchPosition = compareMatchPosition
}
}
}
// If the match is closer to the start of any word in the two terms, return that term.
if (aMatchPosition < bMatchPosition) {
return -1;
} else if (bMatchPosition < aMatchPosition) {
return 1;
} else {
// If the match is at the same position in both words, return the term with the earliest matching word.
if (aMatchingWordIndex < bMatchingWordIndex) {
return -1;
} else if (bMatchingWordIndex < aMatchingWordIndex) {
return 1;
} else {
return 0;
}
}
};
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment