Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Add version 0.1

  • Loading branch information...
commit 128aed78f8aedb291793f223e9d538ff4312384c 0 parents
@ConradIrwin ConradIrwin authored
7 LICENSE.MIT
@@ -0,0 +1,7 @@
+Copyright (c) 2011 Conrad Irwin <conrad@rapportive.com>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
45 README.md
@@ -0,0 +1,45 @@
+
+jQuery.fuzzyMatch
+-----------------
+
+jQuery.fuzzyMatch implements a fuzzy-string-matching algorithm inspired by
+QuickSilver and Command-T in javascript.
+
+It is designed for use as part of an autocompleter, where a finite list of
+possible strings are to be matched against the few characters that the user has
+typed.
+
+It offers some improvement over existing prefix-based matches, though it is
+careful to ensure that if a prefix match has been typed, it will be shown
+first.
+
+
+Example
+=======
+
+To use this with jQuery.ui.autocomplete, you do:
+
+```javascript
+
+ $("input").autocomplete({
+ source: function (context, callback) {
+ callback($(['dog', 'cat', 'cow']).filter(function (a) {
+ // Only show items which match
+ return $.fuzzyMatch(a, context.term);
+ }).sort(function (a, b) {
+ // And sort them by matchiness.
+ var score_a = $.fuzzyMatch(a, context.term),
+ score_b = $.fuzzyMatch(b, context.term);
+
+ return score_a < score_b ? -1 : score_a === score_b ? 0 : 1;
+ }));
+ },
+ delay: 0
+ });
+
+```
+
+Meta-foo
+========
+
+This is all licensed under the MIT license, contributions are most welcome.
167 jquery.fuzzymatch.js
@@ -0,0 +1,167 @@
+/**
+ * jQuery.fuzzyMatch.js, version 0.1 (2011-06-19)
+ *
+ * https://github.com/rapportive-oss/jquery-fuzzymatch
+ *
+ * A fuzzy string-matching plugin for autocompleting in jQuery,
+ * based on LiquidMetal http://github.com/rmm5t/liquidmetal/blob/master/liquidmetal.js
+ * quicksilver.js http://code.google.com/p/rails-oceania/source/browse/lachiecox/qs_score/trunk/qs_score.js
+ * QuickSilver http://code.google.com/p/blacktree-alchemy/source/browse/trunk/Crucible/Code/NSString_BLTRExtensions.m#61
+ * FuzzyString https://github.com/dcparker/jquery_plugins/blob/master/fuzzy-string/fuzzy-string.js
+ *
+ * Copyright (c) 2011, Conrad Irwin (conrad@rapportive.com)
+ * Licensed under the MIT: http://www.opensource.org/licenses/mit-license.php
+ *
+ * TODO: Tweak heuristics, typo correction support?
+**/
+(function ($) {
+
+ // The scores are arranged so that a continuous match of characters will
+ // result in a total score of 1.
+ //
+ // The best case, this character is a match, and either this is the start
+ // of the string, or the previous character was also a match.
+ var SCORE_CONTINUE_MATCH = 1,
+
+ // A new match at the start of a word scores better than a new match
+ // elsewhere as it's more likely that the user will type the starts
+ // of fragments.
+ // (Our notion of word includes CamelCase and hypen-separated, etc.)
+ SCORE_START_WORD = 0.9,
+
+ // Any other match isn't ideal, but it's probably ok.
+ SCORE_OK = 0.8,
+
+ // The goodness of a match should decay slightly with each missing
+ // character.
+ //
+ // i.e. "bad" is more likely than "bard" when "bd" is typed.
+ //
+ // This will not change the order of suggestions based on SCORE_* until
+ // 100 characters are inserted between matches.
+ PENALTY_SKIPPED = 0.999,
+
+ // The goodness of an exact-case match should be higher than a
+ // case-insensitive match by a small amount.
+ //
+ // i.e. "HTML" is more likely than "haml" when "HM" is typed.
+ //
+ // This will not change the order of suggestions based on SCORE_* until
+ // 1000 characters are inserted between matches.
+ PENALTY_CASE_MISMATCH = 0.9999,
+
+ // The goodness of matches should decay slightly with trailing
+ // characters.
+ //
+ // i.e. "quirk" is more likely than "quirkier" when "qu" is typed.
+ //
+ // This will not change the order of suggestions based on SCORE_* until
+ // 10000 characters are appended.
+ PENALTY_TRAILING = 0.99999;
+
+ /**
+ * Generates all possible split objects by splitting a string around a
+ * character in as many ways as possible.
+ *
+ * @param string The string to split
+ * @param char A character on which to split it.
+ *
+ * @return [{
+ * before: The fragment of the string before this occurance of the
+ * character.
+ *
+ * char: The original coy of this character (which may differ in case
+ * from the "char" parameter).
+ *
+ * after: The fragment of the string after the occurance of the character.
+ * }]
+ **/
+ function allCaseInsensitiveSplits(string, chr) {
+ var lower = string.toLowerCase(),
+ lchr = chr.toLowerCase(),
+
+ i = lower.indexOf(lchr),
+ result = [];
+
+ while (i > -1) {
+ result.push({
+ before: string.slice(0, i),
+ chr: string.charAt(i),
+ after: string.slice(i + 1)
+ });
+
+ i = lower.indexOf(lchr, i + 1);
+ }
+ return result;
+ }
+
+ /**
+ * Generates a case-insensitive match of the abbreviation against the string
+ *
+ * @param string, a canonical string to be matched against.
+ * @param abbreviation, an abbreviation that a user may have typed
+ * in order to specify that string.
+ *
+ * @return {
+ * score: A score (0 <= score <= 1) that indicates how likely it is that
+ * the abbreviation matches the string.
+ *
+ * The score is 0 if the characters in the abbreviation do not
+ * all appear in order in the string.
+ *
+ * The score is 1 if the user typed the exact string.
+ *
+ * Scores are designed to be comparable when many different
+ * strings are matched against the same abbreviation, for example
+ * for autocompleting.
+ *
+ * html: A copy of the input string html-escaped, with matching letters
+ * surrounded by <b> and </b>.
+ *
+ * }
+ **/
+ $.fuzzyMatch = function (string, abbreviation) {
+ if (abbreviation === "") {
+ return {
+ score: Math.pow(PENALTY_TRAILING, string.length),
+ html: $('<div>').text(string).html()
+ };
+ }
+
+ return $(allCaseInsensitiveSplits(string, abbreviation.charAt(0)))
+ .map(function (i, split) {
+ var result = $.fuzzyMatch(split.after, abbreviation.slice(1)),
+ preceding_char = split.before.charAt(split.before.length - 1);
+
+ if (split.before === "") {
+ result.score *= SCORE_CONTINUE_MATCH;
+
+ } else if (preceding_char.match(/[\\\/\-_+.# \t"@\[\(\{&]/) ||
+ (split.chr.toLowerCase() !== split.chr && preceding_char.toLowerCase() === preceding_char)) {
+
+ result.score *= SCORE_START_WORD;
+ } else {
+ result.score *= SCORE_OK;
+ }
+
+ if (split.chr !== abbreviation.charAt(0)) {
+ result.score *= PENALTY_CASE_MISMATCH;
+ }
+
+ result.score *= Math.pow(PENALTY_SKIPPED, split.before.length);
+ result.html = $('<div>').text(split.before).append($('<b>').text(split.chr)).append(result.html).html();
+
+ return result;
+ })
+ .sort(function (a, b) {
+ return a.score < b.score ? 1 : a.score === b.score ? 0 : -1;
+ })[0] ||
+
+ // No matches for the next character in the abbreviation, abort!
+ {
+ score: 0, // This 0 will multiply up to the top, giving a total of 0
+ html: $('<div>').text(string).html()
+ };
+ };
+/*global jQuery */
+}(jQuery));
3  jquery.fuzzymatch.min.js
@@ -0,0 +1,3 @@
+// jQuery.fuzzyMatch.js: Copyright 2011 conrad@rapportive.com (MIT License)
+(function(d){function g(a,e){for(var b=a.toLowerCase(),f=e.toLowerCase(),c=b.indexOf(f),d=[];c>-1;)d.push({before:a.slice(0,c),chr:a.charAt(c),after:a.slice(c+1)}),c=b.indexOf(f,c+1);return d}d.fuzzyMatch=function(a,e){if(e==="")return{score:Math.pow(0.99999,a.length),html:d("<div>").text(a).html()};return _(g(a,e.charAt(0))).chain().map(function(b){var a=d.fuzzyMatch(b.after,e.slice(1)),c=b.before.replace(/.*(.)$/,"$1");a.score*=b.before===""?1:c.match(/[\\\/\-_+.# \t"@\[\(\{&]/)||b.chr.toLowerCase()!==
+b.chr&&c.toLowerCase()===c?0.9:0.8;b.chr!==e.charAt(0)&&(a.score*=0.9999);a.score*=Math.pow(0.999,b.before.length);a.html=d("<div>").text(b.before).append(d("<b>").text(b.chr)).append(a.html).html();return a}).sortBy(_.plucker("score")).last().value()||{score:0,html:d("<div>").text(a).html()}}})(jQuery);
56 spec.html
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<title>Jasmine Specs</title>
+<script src="http://jasmine.jelzo.com/1.0.2.js"></script>
+<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.1/jquery.min.js"></script>
+<script src="http://ajax.cdnjs.com/ajax/libs/underscore.js/1.1.4/underscore-min.js"></script>
+<script src="jquery.fuzzymatch.js"></script>
+<script>
+describe("$.fuzzyMatch", function () {
+
+ it("should give a score of 1 for an exact match", function () {
+ expect($.fuzzyMatch("a", "a").score).toEqual(1);
+ expect($.fuzzyMatch("abcdef", "abcdef").score).toEqual(1);
+ expect($.fuzzyMatch("abCdef", "abCdef").score).toEqual(1);
+ });
+
+ it("should return 0 for abbreviations that don't match", function () {
+ expect($.fuzzyMatch("hello", "bye").score).toEqual(0);
+ expect($.fuzzyMatch("hello", "le").score).toEqual(0);
+ expect($.fuzzyMatch("hello world", "hell oworld").score).toEqual(0);
+ });
+
+ it("should prefer exact matches over prefix matches", function () {
+ expect($.fuzzyMatch("grab", "grab").score).toBeGreaterThan($.fuzzyMatch("grabbed", "grab").score);
+ expect($.fuzzyMatch("grabbed", "grab").score).toBeGreaterThan($.fuzzyMatch("grabbing", "grab").score);
+ });
+
+ it("should prefer closer-together matches over further-apart matches", function () {
+ expect($.fuzzyMatch("garden", "ga").score).toBeGreaterThan($.fuzzyMatch("gray", "ga").score);
+ expect($.fuzzyMatch("aggravate", "ga").score).toBeGreaterThan($.fuzzyMatch("aglutamate", "ga").score);
+ });
+
+ it("should prefer case-sensitive matches over case-insensitivei matches", function () {
+ expect($.fuzzyMatch("CSS", "CS").score).toBeGreaterThan($.fuzzyMatch("css", "CS").score);
+ expect($.fuzzyMatch("arm", "ar").score).toBeGreaterThan($.fuzzyMatch("ARM", "ar").score);
+ });
+
+ it("should prefer things that starts of words more", function () {
+ expect($.fuzzyMatch("outlook-user", "ous").score).toBeGreaterThan($.fuzzyMatch("zoho-user", "ous").score);
+ expect($.fuzzyMatch("outlook-the-user", "ous").score).toBeGreaterThan($.fuzzyMatch("the-outlook-user", "ous").score);
+ expect($.fuzzyMatch("lots-of-outlook-users", "ous").score).toBeGreaterThan($.fuzzyMatch("millions-of-outlook-users", "ous").score);
+ expect($.fuzzyMatch("camelCaseStuff", "css").score).toBeGreaterThan($.fuzzyMatch("CAMELCASESTUFF", "css").score);
+ expect($.fuzzyMatch("CSS", "css").score).toBeGreaterThan($.fuzzyMatch("camelCaseStuff", "css").score);
+ });
+
+ it("should return the html of the best match", function () {
+ expect($.fuzzyMatch("outlook-user", "ous").html).toEqual("<b>o</b>utlook-<b>u</b><b>s</b>er");
+ // Nom — should put the suggestions through a hyphenator first? :p
+ expect($.fuzzyMatch("outsourced user", "ous").html).toEqual("<b>o</b>utsourced <b>u</b><b>s</b>er");
+ expect($.fuzzyMatch("out-sourced user", "ous").html).toEqual("<b>o</b><b>u</b>t-<b>s</b>ourced user");
+ });
+
+ it("should escape HTML", function () {
+ expect($.fuzzyMatch("<foo> b&r <bar>", "fob&").html).toEqual("&lt;<b>f</b><b>o</b>o&gt; <b>b</b><b>&amp;</b>r &lt;bar&gt;");
+ });
+});
+</script>
Please sign in to comment.
Something went wrong with that request. Please try again.