[Pkg-javascript-commits] [node-fuzzaldrin-plus] 02/17: New upstream version 0.3.1+git.20161008.da2cb58
Praveen Arimbrathodiyil
praveen at moszumanska.debian.org
Sat Nov 19 18:46:38 UTC 2016
This is an automated email from the git hooks/post-receive script.
praveen pushed a commit to branch master
in repository node-fuzzaldrin-plus.
commit 9931127b0fa1802c5c113849943a8e355e651293
Author: Praveen Arimbrathodiyil <praveen at debian.org>
Date: Sat Nov 19 23:33:48 2016 +0530
New upstream version 0.3.1+git.20161008.da2cb58
---
Gruntfile.coffee | 31 +-
LICENSE.md | 1 -
benchmark/benchmark.coffee | 59 +-
demo/demo.css | 123 +++++
demo/demo.html | 90 ++++
demo/movies.json | 1005 +++++++++++++++++++++++++++++++++++
dist-browser/fuzzaldrin-plus.js | 914 +++++++++++++++++++++++++++++++
dist-browser/fuzzaldrin-plus.min.js | 2 +
package.json | 16 +-
spec/filter-spec.coffee | 199 +++++--
spec/legacy-filter-spec.coffee | 142 -----
src/filter.coffee | 45 +-
src/fuzzaldrin.coffee | 92 ++--
src/legacy.coffee | 118 ----
src/matcher.coffee | 123 ++++-
src/pathScorer.coffee | 132 +++++
src/query.coffee | 69 +++
src/scorer.coffee | 231 +++-----
18 files changed, 2796 insertions(+), 596 deletions(-)
diff --git a/Gruntfile.coffee b/Gruntfile.coffee
index 45c521c..a170e6e 100644
--- a/Gruntfile.coffee
+++ b/Gruntfile.coffee
@@ -21,6 +21,30 @@ module.exports = (grunt) ->
test: ['spec/*.coffee']
gruntfile: ['Gruntfile.coffee']
+ browserify:
+
+ options:
+ banner: '/* <%= pkg.name %> - v<%= pkg.version %> - @license: <%= pkg.license %>; @author: Jean Christophe Roy; @site: <%= pkg.homepage %> */\n'
+ browserifyOptions:
+ standalone: 'fuzzaldrin'
+
+ dist:
+ src: ['lib/fuzzaldrin.js']
+ dest: 'dist-browser/fuzzaldrin-plus.js'
+
+
+ uglify:
+
+ options:
+ preserveComments: false
+ banner: '/* <%= pkg.name %> - v<%= pkg.version %> - @license: <%= pkg.license %>; @author: Jean Christophe Roy; @site: <%= pkg.homepage %> */\n'
+
+ dist:
+ src: 'dist-browser/fuzzaldrin-plus.js',
+ dest: 'dist-browser/fuzzaldrin-plus.min.js'
+
+
+
shell:
test:
command: 'node node_modules/jasmine-focused/bin/jasmine-focused --coffee --captureExceptions spec'
@@ -32,6 +56,9 @@ module.exports = (grunt) ->
grunt.loadNpmTasks('grunt-contrib-coffee')
grunt.loadNpmTasks('grunt-shell')
grunt.loadNpmTasks('grunt-coffeelint')
+ grunt.loadNpmTasks('grunt-browserify')
+ grunt.loadNpmTasks('grunt-contrib-uglify')
+
grunt.registerTask 'clean', ->
rm = (pathToDelete) ->
@@ -40,5 +67,7 @@ module.exports = (grunt) ->
grunt.registerTask('lint', ['coffeelint'])
grunt.registerTask('test', ['default', 'shell:test'])
- grunt.registerTask('prepublish', ['clean', 'test'])
+ grunt.registerTask('prepublish', ['clean', 'test', 'distribute'])
grunt.registerTask('default', ['coffee', 'lint'])
+ grunt.registerTask('distribute', ['default', 'browserify', 'uglify'])
+
diff --git a/LICENSE.md b/LICENSE.md
index 81f8b50..552d54c 100644
--- a/LICENSE.md
+++ b/LICENSE.md
@@ -1,5 +1,4 @@
Copyright (c) 2015 Jean Christophe Roy
-Copyright (c) 2013 GitHub Inc.
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
diff --git a/benchmark/benchmark.coffee b/benchmark/benchmark.coffee
index f532bd9..cdca07a 100644
--- a/benchmark/benchmark.coffee
+++ b/benchmark/benchmark.coffee
@@ -1,23 +1,21 @@
fs = require 'fs'
path = require 'path'
-{filter, match, prepQuery} = require '../src/fuzzaldrin'
+fuzzaldrinPlus = require '../src/fuzzaldrin'
+legacy = require 'fuzzaldrin'
lines = fs.readFileSync(path.join(__dirname, 'data.txt'), 'utf8').trim().split('\n')
-
-forceAllMatch = {maxInners:-1}
-legacy = {legacy:true}
-mitigation = {maxInners:Math.floor(0.2*lines.length)}
+forceAllMatch = {maxInners: -1}
+mitigation = {maxInners: Math.floor(0.2 * lines.length)}
#warmup + compile
-filter(lines, 'index', forceAllMatch)
-filter(lines, 'index', legacy)
-
+fuzzaldrinPlus.filter(lines, 'index', forceAllMatch)
+legacy.filter(lines, 'index')
console.log("======")
startTime = Date.now()
-results = filter(lines, 'index')
+results = fuzzaldrinPlus.filter(lines, 'index')
console.log("Filtering #{lines.length} entries for 'index' took #{Date.now() - startTime}ms for #{results.length} results (~10% of results are positive, mix exact & fuzzy)")
if results.length isnt 6168
@@ -25,75 +23,75 @@ if results.length isnt 6168
process.exit(1)
startTime = Date.now()
-results = filter(lines, 'index', legacy)
+results = legacy.filter(lines, 'index')
console.log("Filtering #{lines.length} entries for 'index' took #{Date.now() - startTime}ms for #{results.length} results (~10% of results are positive, Legacy method)")
console.log("======")
startTime = Date.now()
-results = filter(lines, 'indx')
+results = fuzzaldrinPlus.filter(lines, 'indx')
console.log("Filtering #{lines.length} entries for 'indx' took #{Date.now() - startTime}ms for #{results.length} results (~10% of results are positive, Fuzzy match)")
startTime = Date.now()
-results = filter(lines, 'indx', legacy)
+results = legacy.filter(lines, 'indx')
console.log("Filtering #{lines.length} entries for 'indx' took #{Date.now() - startTime}ms for #{results.length} results (~10% of results are positive, Fuzzy match, Legacy)")
console.log("======")
startTime = Date.now()
-results = filter(lines, 'walkdr')
+results = fuzzaldrinPlus.filter(lines, 'walkdr')
console.log("Filtering #{lines.length} entries for 'walkdr' took #{Date.now() - startTime}ms for #{results.length} results (~1% of results are positive, fuzzy)")
startTime = Date.now()
-results = filter(lines, 'walkdr', legacy)
+results = legacy.filter(lines, 'walkdr')
console.log("Filtering #{lines.length} entries for 'walkdr' took #{Date.now() - startTime}ms for #{results.length} results (~1% of results are positive, Legacy method)")
console.log("======")
startTime = Date.now()
-results = filter(lines, 'node', forceAllMatch)
+results = fuzzaldrinPlus.filter(lines, 'node', forceAllMatch)
console.log("Filtering #{lines.length} entries for 'node' took #{Date.now() - startTime}ms for #{results.length} results (~98% of results are positive, mostly Exact match)")
startTime = Date.now()
-results = filter(lines, 'node', legacy)
+results = legacy.filter(lines, 'node')
console.log("Filtering #{lines.length} entries for 'node' took #{Date.now() - startTime}ms for #{results.length} results (~98% of results are positive, mostly Exact match, Legacy method)")
console.log("======")
startTime = Date.now()
-results = filter(lines, 'nm', forceAllMatch)
+results = fuzzaldrinPlus.filter(lines, 'nm', forceAllMatch)
console.log("Filtering #{lines.length} entries for 'nm' took #{Date.now() - startTime}ms for #{results.length} results (~98% of results are positive, Acronym match)")
startTime = Date.now()
-results = filter(lines, 'nm', forceAllMatch)
+results = legacy.filter(lines, 'nm')
console.log("Filtering #{lines.length} entries for 'nm' took #{Date.now() - startTime}ms for #{results.length} results (~98% of results are positive, Acronym match, Legacy method)")
console.log("======")
startTime = Date.now()
-results = filter(lines, 'nodemodules', forceAllMatch)
+results = fuzzaldrinPlus.filter(lines, 'nodemodules', forceAllMatch)
console.log("Filtering #{lines.length} entries for 'nodemodules' took #{Date.now() - startTime}ms for #{results.length} results (~98% positive + Fuzzy match, [Worst case scenario])")
startTime = Date.now()
-results = filter(lines, 'nodemodules', mitigation)
+results = fuzzaldrinPlus.filter(lines, 'nodemodules', mitigation)
console.log("Filtering #{lines.length} entries for 'nodemodules' took #{Date.now() - startTime}ms for #{results.length} results (~98% positive + Fuzzy match, [Mitigation])")
startTime = Date.now()
-results = filter(lines, 'nodemodules', legacy)
+results = legacy.filter(lines, 'nodemodules')
console.log("Filtering #{lines.length} entries for 'nodemodules' took #{Date.now() - startTime}ms for #{results.length} results (Legacy)")
console.log("======")
startTime = Date.now()
-results = filter(lines, 'ndem', forceAllMatch)
+results = fuzzaldrinPlus.filter(lines, 'ndem', forceAllMatch)
console.log("Filtering #{lines.length} entries for 'ndem' took #{Date.now() - startTime}ms for #{results.length} results (~98% positive + Fuzzy match, [Worst case but shorter srting])")
startTime = Date.now()
-results = filter(lines, 'ndem', legacy)
+results = legacy.filter(lines, 'ndem')
console.log("Filtering #{lines.length} entries for 'ndem' took #{Date.now() - startTime}ms for #{results.length} results (Legacy)")
@@ -101,11 +99,16 @@ console.log("======")
startTime = Date.now()
query = 'index'
-prepared = prepQuery(query)
-match(line, query, prepared) for line in lines
-console.log("Matching #{results.length} results for 'index' took #{Date.now() - startTime}ms (Prepare in advance)")
+prepared = fuzzaldrinPlus.prepareQuery(query)
+fuzzaldrinPlus.match(line, query, {preparedQuery: prepared}) for line in lines
+console.log("Matching #{lines.length} results for 'index' took #{Date.now() - startTime}ms (Prepare in advance)")
+
+startTime = Date.now()
+fuzzaldrinPlus.match(line, query) for line in lines
+console.log("Matching #{lines.length} results for 'index' took #{Date.now() - startTime}ms (cache)")
+# replace by `prepQuery ?= scorer.prepQuery(query)`to test without cache.
startTime = Date.now()
-match(line, query) for line in lines
-console.log("Matching #{results.length} results for 'index' took #{Date.now() - startTime}ms (cache)")
+legacy.match(line, query) for line in lines
+console.log("Matching #{lines.length} results for 'index' took #{Date.now() - startTime}ms (legacy)")
# replace by `prepQuery ?= scorer.prepQuery(query)`to test without cache.
diff --git a/demo/demo.css b/demo/demo.css
new file mode 100644
index 0000000..e6c5e65
--- /dev/null
+++ b/demo/demo.css
@@ -0,0 +1,123 @@
+
+* {
+ box-sizing: border-box;
+}
+
+h1, h2, p, form, label {
+ text-align: center;
+}
+
+h1, h2 {
+ margin-top: 1.5em;
+ margin-bottom: 0.5em;
+}
+
+#sourcetxt {
+ display: block;
+ margin-left: auto;
+ margin-right: auto;
+ width: 90%;
+ max-width: 700px;
+ height: 250px;
+}
+
+.center {
+ display: block;
+ margin-left: auto;
+ margin-right: auto;
+ width: 90%;
+ max-width: 700px;
+}
+
+.typeahead {
+ width: 90%;
+ max-width: 700px;
+ margin: auto;
+ display: block;
+ padding-bottom: 30px;
+ position: relative;
+}
+
+.typeahead input {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ max-width: 700px;
+ padding: 10px;
+ opacity: 1;
+ background: none 0% 0% / auto repeat scroll padding-box border-box rgb(255, 255, 255);
+}
+
+.twitter-typeahead {
+ width: 100%;
+}
+
+.twitter-typeahead .tt-query,
+.twitter-typeahead .tt-hint {
+ margin-bottom: 0;
+}
+
+.typeahead ul, .typeahead li {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ border: 0;
+ font: inherit;
+ font-size: 100%;
+ vertical-align: baseline;
+}
+
+.tt-menu, .ui-menu {
+ width: 100%;
+ max-width: 700px;
+ margin-top: 2px;
+ padding: 5px 0;
+ background-color: #ffffff;
+ border: 1px solid rgba(0, 0, 0, 0.15);
+ border-radius: 4px;
+ -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
+ box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
+ background-clip: padding-box;
+
+}
+
+.tt-suggestion, .ui-menu-item {
+ display: block;
+ padding: 6px 20px;
+}
+
+.ui-helper-hidden-accessible {
+ display: none;
+}
+
+.typeahead-footer {
+ border-top: 1px solid #eee;
+ margin-top: 10px;
+ padding: 20px;
+}
+
+.title {
+ color: #222;
+}
+
+.title strong {
+ color: #000;
+}
+
+.author {
+ color: #666
+}
+
+.author strong {
+ color: #444
+}
+
+.score {
+ color: #888;
+}
+
+td {
+ vertical-align: top;
+ text-align: left;
+}
\ No newline at end of file
diff --git a/demo/demo.html b/demo/demo.html
new file mode 100644
index 0000000..5c0dd2c
--- /dev/null
+++ b/demo/demo.html
@@ -0,0 +1,90 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <title></title>
+
+
+ <script src="https://code.jquery.com/jquery-1.11.3.min.js"></script>
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/typeahead.js/0.11.1/typeahead.jquery.min.js"></script>
+ <script src="../dist-browser/fuzzaldrin-plus.min.js"></script>
+
+ <link href="demo.css" rel="stylesheet" type="text/css"/>
+
+
+</head>
+<body>
+
+<h2>Source</h2>
+<textarea id="sourcetxt"></textarea>
+
+<div class="center">
+ <label>Enter one item per line then press update</label>
+ <button onclick="set_txt_source()"> Update source</button>
+</div>
+
+<h2>Make a search</h2>
+
+<div class="typeahead">
+ <input id="demo-query" class="twitter-typeahead" name="demo-query" autocomplete="off" type="text">
+</div>
+
+<!-- allow to scroll so autocomplete is at top of page -->
+<div style="height: 500px;"></div>
+
+
+<script>
+
+ var global_query = "";
+ var global_candidates = [];
+
+
+ //- - - - - - - - - - -
+ // Typeahead Setup
+ // - - - - - - - - - - -
+ $('#demo-query').typeahead({
+ minLength: 2,
+ highlight: false //let FuzzySearch handle highlight
+ },
+ {
+ name: 'filtered',
+ limit: 10,
+
+ source: function (query, callback) {
+ global_query = query;
+ callback(fuzzaldrin.filter(global_candidates, query))
+ },
+
+ templates: {
+ suggestion: function (result) {
+ return "<div>" + fuzzaldrin.wrap(result, global_query) + "</div>"
+ }
+ }
+
+ });
+
+
+ // - - - - - - - - - - - -
+ // Setup dataset
+ //- - - - - - - - - - - - -
+
+ $.ajaxSetup({cache: true});
+ function set_json_source(url) {
+ $.getJSON(url).then(function (response) {
+ global_candidates = response;
+ $("#sourcetxt").val(response.join("\n"))
+ });
+ }
+ // Load movie dataset, default options are good for list of string.
+ set_json_source("movies.json");
+
+ function set_txt_source() {
+ global_candidates = $("#sourcetxt").val().split("\n")
+ }
+
+
+</script>
+
+
+</body>
+</html>
\ No newline at end of file
diff --git a/demo/movies.json b/demo/movies.json
new file mode 100644
index 0000000..7986534
--- /dev/null
+++ b/demo/movies.json
@@ -0,0 +1,1005 @@
+["A Nous la Liberte (1932)",
+ "About Schmidt (2002)",
+ "Absence of Malice (1981)",
+ "Adam's Rib (1949)",
+ "Adaptation (2002)",
+ "The Adjuster (1991)",
+ "The Adventures of Robin Hood (1938)",
+ "Affliction (1998)",
+ "The African Queen (1952)",
+ "L'Age d'Or (1930, reviewed 1964)",
+ "Aguirre, the Wrath of God (1972, reviewed 1977)",
+ "A.I. (2001)",
+ "Airplane! (1980)",
+ "Aladdin (1992)",
+ "Alexander Nevsky (1939)",
+ "Alice Doesn't Live Here Anymore (1975)",
+ "Alice's Restaurant (1969)",
+ "Aliens (1986)",
+ "All About Eve (1950)",
+ "All About My Mother (1999)",
+ "All Quiet on the Western Front (1930)",
+ "All That Heaven Allows (1956)",
+ "All the King's Men (1949)",
+ "All the President's Men (1976)",
+ "Amadeus (1984)",
+ "Amarcord (1974)",
+ "Amélie (2001)",
+ "America, America (1963)",
+ "The American Friend (1977)",
+ "American Graffiti (1973)",
+ "An American in Paris (1951)",
+ "The Americanization of Emily (1964)",
+ "American Movie (1999)",
+ "Amores Perros (2000)",
+ "Anastasia (1956)",
+ "Anatomy of a Murder (1959)",
+ "The Angry Silence (1960)",
+ "Anna and the King of Siam (1946)",
+ "Anna Christie (1930)",
+ "Annie Hall (1977)",
+ "The Apartment (1960)",
+ "Apocalypse Now (1979)",
+ "Apollo 13 (1995)",
+ "The Apostle (1997)",
+ "L'Argent (1983)",
+ "Ashes and Diamonds (1958, reviewed 1961)",
+ "Ashes and Diamonds (1958)",
+ "The Asphalt Jungle (1950)",
+ "L'Atalante (1934, reviewed 1947)",
+ "Atlantic City (1981)",
+ "Au Revoir Les Enfants (1988)",
+ "L'Avventura (1961)",
+ "The Awful Truth (1937)",
+ "Babette's Feast (1987)",
+ "Baby Doll (1956)",
+ "Back to the Future (1985)",
+ "The Bad and the Beautiful (1953)",
+ "Bad Day at Black Rock (1955)",
+ "Badlands (1973)",
+ "The Baker's Wife (1940)",
+ "Ball of Fire (1942)",
+ "The Ballad of Cable Hogue (1970)",
+ "Bambi (1942)",
+ "The Band Wagon (1953)",
+ "Bang the Drum Slowly (1973)",
+ "The Bank Dick (1940)",
+ "Barfly (1987)",
+ "Barry Lyndon (1975)",
+ "Barton Fink (1991)",
+ "The Battle of Algiers (1965, reviewed 1967)",
+ "Le Beau Mariage (1982)",
+ "Beautiful People (2000)",
+ "Beauty and the Beast (1947)",
+ "Beauty and the Beast (1991)",
+ "Bed and Board (1971)",
+ "Beetlejuice (1988)",
+ "Before Night Falls (2000)",
+ "Before the Rain (1994, reviewed 1995)",
+ "Being John Malkovich (1999)",
+ "Being There (1979)",
+ "Belle de Jour (1968)",
+ "Ben-Hur (1959)",
+ "Berlin Alexanderplatz (1983)",
+ "The Best Years of Our Lives (1946)",
+ "Beverly Hills Cop (1984)",
+ "The Bicycle Thief (1949)",
+ "The Big Chill (1983)",
+ "The Big Clock (1948)",
+ "The Big Deal on Madonna Street (1960)",
+ "The Big Heat (1953)",
+ "Big Night (1996)",
+ "The Big Red One (1980)",
+ "The Big Sky (1952)",
+ "The Big Sleep (1946)",
+ "Billy Liar (1963)",
+ "Biloxi Blues (1988)",
+ "The Birds (1963)",
+ "Birdy (1984)",
+ "Black Narcissus (1947)",
+ "Black Orpheus (1959)",
+ "Black Robe (1991)",
+ "Blazing Saddles (1974)",
+ "Bloody Sunday (2002)",
+ "Blow-Up (1966)",
+ "Blue Collar (1978)",
+ "Blue Velvet (1986)",
+ "Bob & Carol & Ted & Alice (1969)",
+ "Bob le Flambeur (1955, reviewed 1981)",
+ "Body Heat (1981)",
+ "Bonnie and Clyde (1967)",
+ "Boogie Nights (1997)",
+ "Born on the Fourth of July (1989)",
+ "Born Yesterday (1950)",
+ "Le Boucher (1970)",
+ "Bound for Glory (1976)",
+ "Boys Don't Cry (1999)",
+ "Boyz N the Hood (1991)",
+ "Brazil (1985)",
+ "Bread, Love and Dreams (1954)",
+ "Breaker Morant (1980)",
+ "The Breakfast Club (1985)",
+ "Breaking Away (1979)",
+ "Breaking the Waves (1996)",
+ "Breathless (1961)",
+ "The Bride Wore Black (1968)",
+ "The Bridge on the River Kwai (1957)",
+ "Brief Encounter (1946)",
+ "A Brief History of Time (1992)",
+ "Bringing Up Baby (1938)",
+ "Broadcast News (1987)",
+ "Brother's Keeper (1992)",
+ "The Buddy Holly Story (1978)",
+ "Bull Durham (1988)",
+ "Bullitt (1968)",
+ "Bus Stop (1956)",
+ "Butch Cassidy and the Sundance Kid (1969)",
+ "The Butcher Boy (1998)",
+ "Bye Bye Brasil (1980)",
+ "The Earrings of Madame De . . . (1954)",
+ "Cabaret (1972)",
+ "The Caine Mutiny (1954)",
+ "California Suite (1978)",
+ "Calle 54 (2000)",
+ "Camelot (1967)",
+ "Camille (1937)",
+ "Captains Courageous (1937)",
+ "Carmen Jones (1954)",
+ "Carnal Knowledge (1971)",
+ "Casablanca (1942)",
+ "Cat on a Hot Tin Roof (1958)",
+ "Catch-22 (1970)",
+ "Cavalcade (1933)",
+ "The Celebration (1998)",
+ "La Cérémonie (1996)",
+ "Chan Is Missing (1982)",
+ "Chariots of Fire (1981)",
+ "Charley Varrick (1973)",
+ "Chicago (2002)",
+ "Chicken Run (2000)",
+ "La Chienne (1931, reviewed 1975)",
+ "Chinatown (1974)",
+ "Chloë in the Afternoon (1972)",
+ "Chocolat (1988, reviewed 1989)",
+ "The Cider House Rules (1999)",
+ "The Citadel (1938)",
+ "Citizen Kane (1941)",
+ "Claire's Knee (1971)",
+ "The Clockmaker (1973, reviewed 1976)",
+ "A Clockwork Orange (1971)",
+ "Close Encounters of the Third Kind (1977)",
+ "Close-Up (1990, reviewed 1999)",
+ "Clueless (1995)",
+ "Coal Miner's Daughter (1980)",
+ "The Color of Money (1986)",
+ "Come Back, Little Sheba (1952)",
+ "Coming Home (1978)",
+ "The Conformist (1970)",
+ "The Conquest of Everest (1953)",
+ "Contempt (1964)",
+ "The Conversation (1974)",
+ "Cool Hand Luke (1967)",
+ "The Count of Monte Cristo (1934)",
+ "The Country Girl (1954)",
+ "The Cousins (1959)",
+ "The Cranes Are Flying (1960)",
+ "Cries and Whispers (1972)",
+ "Crossfire (1947)",
+ "Crumb (1994)",
+ "Cry, the Beloved Country (1952)",
+ "The Crying Game (1992)",
+ "Damn Yankees (1958)",
+ "The Damned (1969)",
+ "Dance with a Stranger (1985)",
+ "Dangerous Liaisons (1988)",
+ "Daniel (1983)",
+ "Danton (1983)",
+ "Dark Eyes (1987)",
+ "Dark Victory (1939)",
+ "Darling (1965)",
+ "David Copperfield (1935)",
+ "David Holtzman's Diary (1968, reviewed 1973)",
+ "Dawn of the Dead (1979)",
+ "Day for Night (1973)",
+ "The Day of the Jackal (1973)",
+ "The Day the Earth Stood Still (1951)",
+ "Days of Heaven (1978)",
+ "Days of Wine and Roses (1963)",
+ "The Dead (1987)",
+ "Dead Calm (1989)",
+ "Dead End (1937)",
+ "Dead Man Walking (1995)",
+ "Dead of Night (1946, reviewed 1946)",
+ "Dead Ringers (1988)",
+ "Death in Venice (1971)",
+ "Death of a Salesman (1951)",
+ "The Decalogue (2000)",
+ "Deep End (1971)",
+ "The Deer Hunter (1978)",
+ "The Defiant Ones (1958)",
+ "Deliverance (1972)",
+ "Desperately Seeking Susan (1985)",
+ "Destry Rides Again (1939)",
+ "Diabolique (1955)",
+ "Dial M for Murder (1954)",
+ "Diary of a Chambermaid (1964)",
+ "Diary of a Country Priest (1950, reviewed 1954)",
+ "Die Hard (1988)",
+ "Diner (1982)",
+ "Dinner at Eight (1933)",
+ "The Dirty Dozen (1967)",
+ "Dirty Harry (1971)",
+ "Dirty Rotten Scoundrels (1988)",
+ "The Discreet Charm of the Bourgeoisie (1972)",
+ "Disraeli (1929)",
+ "Distant Thunder (1973)",
+ "Diva (1982)",
+ "Divorce-Italian Style (1962)",
+ "Do the Right Thing (1989)",
+ "Dr. Jekyll and Mr. Hyde (1932)",
+ "Dr. Strangelove or: How I Learned to Stop Worrying and Love the Bomb (1964)",
+ "Doctor Zhivago (1965)",
+ "Dodsworth (1936)",
+ "La Dolce Vita (1961)",
+ "Donnie Brasco (1997)",
+ "Don't Look Back (1967)",
+ "Double Indemnity (1944)",
+ "Down by Law (1986)",
+ "Dracula (1931)",
+ "The Dreamlife of Angels (1998)",
+ "Dressed to Kill (1980)",
+ "The Dresser (1983)",
+ "Driving Miss Daisy (1989)",
+ "Drowning by Numbers (1991)",
+ "Drugstore Cowboy (1989)",
+ "Duck Soup (1933)",
+ "The Duellists (1978)",
+ "Dumbo (1941)",
+ "The Earrings of Madame De . . .",
+ "East of Eden (1955)",
+ "Easy Living (1937)",
+ "Eat Drink Man Woman (1994)",
+ "Effi Briest (1977)",
+ "8 1/2 (1963)",
+ "Eight Men Out (1988)",
+ "The Elephant Man (1980)",
+ "Elmer Gantry (1960)",
+ "Empire of the Sun (1987)",
+ "Enemies, A Love Story (1989)",
+ "Les Enfants du Paradis (1945, reviewed 1947)",
+ "The English Patient (1996)",
+ "The Entertainer (1960)",
+ "Entre Nous (1983)",
+ "E.T. the Extra-Terrestrial (1982)",
+ "Europa, Europa (1991)",
+ "Every Man for Himself (1980)",
+ "The Exorcist (1973)",
+ "The Exterminating Angel (1967)",
+ "A Face in the Crowd (1957)",
+ "Face to Face (1976)",
+ "Faces (1968)",
+ "The Family Game (1984)",
+ "Fanny & Alexander (1983)",
+ "Fantasia (1940)",
+ "Farewell, My Concubine (1993)",
+ "Far from Heaven (2002)",
+ "Fargo (1996)",
+ "Fast, Cheap & Out of Control (1997)",
+ "Fast Runner (Atanarjuat) (2002)",
+ "Fat City (1972)",
+ "Fatal Attraction (1987)",
+ "Father of the Bride (1950)",
+ "Fellini Satyricon (1970)",
+ "La Femme Infidèle (1969)",
+ "La Femme Nikita (1991)",
+ "The Fisher King (1991)",
+ "Fist in His Pocket (1968)",
+ "Fitzcarraldo (1982)",
+ "Five Easy Pieces (1970)",
+ "The Flamingo Kid (1984)",
+ "The Fly (1958)",
+ "The Flamingo Kid (1984)",
+ "Force of Evil (1948)",
+ "For Whom the Bell Tolls (1943)",
+ "Forbidden Games (1952)",
+ "A Foreign Affair (1948)",
+ "The Fortune Cookie (1966)",
+ "The 400 Blows (1959)",
+ "Frankenstein (1931)",
+ "The French Connection (1971)",
+ "Frenzy (1972)",
+ "Friendly Persuasion (1956)",
+ "From Here to Eternity (1953)",
+ "The Fugitive (1947)",
+ "Full Metal Jacket (1987)",
+ "The Full Monty (1997)",
+ "Funny Face (1957)",
+ "Funny Girl (1968)",
+ "Fury (1936)",
+ "Gallipoli (1981)",
+ "Gandhi (1982)",
+ "Gangs of New York (2002)",
+ "The Garden of the Finzi-Continis (1971)",
+ "Gas Food Lodging (1992)",
+ "Gaslight (1944)",
+ "Gate of Hell (1954)",
+ "A Geisha (1978)",
+ "The General (1998)",
+ "General Della Rovere (1960)",
+ "Genevieve (1954)",
+ "Gentlemen Prefer Blondes (1953)",
+ "Georgy Girl (1966)",
+ "Get Carter (1971)",
+ "Get Out Your Handkerchiefs (1978)",
+ "Ghost World (2001)",
+ "Giant (1956)",
+ "Gigi (1958)",
+ "Gimme Shelter (1970)",
+ "The Girl Can't Help It (1956)",
+ "Girl with a Suitcase (1961)",
+ "The Gleaners and I (2001)",
+ "The Goalie's Anxiety at the Penalty Kick (1977)",
+ "The Go-Between (1971)",
+ "The Godfather (1972)",
+ "The Godfather Part II (1974)",
+ "Going My Way (1944)",
+ "Goldfinger (1964)",
+ "Gone With the Wind (1939)",
+ "The Good, the Bad and the Ugly (1968)",
+ "The Good Earth (1937)",
+ "Goodbye, Mr. Chips (1939)",
+ "GoodFellas (1990)",
+ "Gosford Park (2001)",
+ "The Graduate (1967)",
+ "Grand Hotel (1932)",
+ "Grand Illusion (1938)",
+ "The Grapes of Wrath (1940)",
+ "The Great Dictator (1940)",
+ "Great Expectations (1947)",
+ "The Great Man (1957)",
+ "The Great McGinty (1940)",
+ "The Greatest Show on Earth (1952)",
+ "Green for Danger (1947)",
+ "Gregory's Girl (1982)",
+ "The Grifters (1990)",
+ "Groundhog Day (1993)",
+ "The Gunfighter (1950)",
+ "Gunga Din (1939)",
+ "Hail the Conquering Hero (1944)",
+ "Hair (1979)",
+ "Hamlet (1948)",
+ "Hamlet (2000)",
+ "Handle With Care (1977)",
+ "Hannah and Her Sisters (1986)",
+ "Happiness (1998)",
+ "A Hard Day's Night (1964)",
+ "Harlan County, USA (1976)",
+ "Harry and Tonto (1974)",
+ "A Hatful of Rain (1957)",
+ "The Heartbreak Kid (1972)",
+ "Heartland (1981)",
+ "Hearts of Darkness: A Filmmaker's Apocalypse (1991)",
+ "Heat and Dust (1983)",
+ "Heathers (1989)",
+ "Heavy Traffic (1973)",
+ "Heimat (1985)",
+ "The Heiress (1949)",
+ "Henry V (1946)",
+ "Henry V (1989)",
+ "Henry Fool (1998)",
+ "Here Comes Mr. Jordan (1941)",
+ "High and Low (Japan) (1963)",
+ "The High and the Mighty (1954)",
+ "High Art (1998)",
+ "High Hopes (1988)",
+ "High Noon (1952)",
+ "High Sierra (1941)",
+ "The Hill (1965)",
+ "Hiroshima Mon Amour (1960)",
+ "His Girl Friday (1940)",
+ "The Homecoming (1973)",
+ "Hoop Dreams (1994)",
+ "Hope and Glory (1987)",
+ "Hotel Terminus: Klaus Barbie et son Temps (1988)",
+ "The Hours (2002)",
+ "Household Saints (1993)",
+ "House of Games (1987)",
+ "How Green Was My Valley (1941)",
+ "How to Marry a Millionaire (1953)",
+ "Howards End (1992)",
+ "Hud (1963)",
+ "Ken Burns' America: Huey Long (1985)",
+ "Husbands and Wives (1992)",
+ "The Hustler (1961)",
+ "I Know Where I'm Going! (1947)",
+ "I Remember Mama (1948)",
+ "I Want to Live! (1958)",
+ "If... (1969)",
+ "Ikiru (1952, reviewed 1960)",
+ "I'm All Right Jack (1960)",
+ "Imitation of Life (1959)",
+ "In Cold Blood (1967)",
+ "In the Bedroom (2001)",
+ "In the Heat of the Night (1967)",
+ "The Informer (1935)",
+ "Inherit the Wind (1960)",
+ "The Insider (1999)",
+ "Internal Affairs (1990)",
+ "The Ipcress File (1965)",
+ "It Happened One Night (1934)",
+ "It's a Gift (1935)",
+ "It's a Wonderful Life (1946)",
+ "Jailhouse Rock (1957)",
+ "Jaws (1975)",
+ "The Jazz Singer (1927)",
+ "Jean de Florette (1987)",
+ "Jerry Maguire (1996)",
+ "Johnny Guitar (1954)",
+ "The Judge and the Assassin (1982)",
+ "Judgment at Nuremberg (1961)",
+ "Ju Dou (1990)",
+ "Jules and Jim (1962)",
+ "Juliet of the Spirits (1965)",
+ "Junior Bonner (1972)",
+ "Kagemusha (1980)",
+ "The Killers (1946)",
+ "The Killing Fields (1984)",
+ "Kind Hearts and Coronets (1950)",
+ "The King and I (1956)",
+ "King Kong (1933)",
+ "King Lear (1971)",
+ "The King of Comedy (1983)",
+ "The King of Marvin Gardens (1972)",
+ "Kiss of the Spider Woman (1985)",
+ "Klute (1971)",
+ "Knife in the Water (1963)",
+ "Kramer vs. Kramer (1979)",
+ "L.A. Confidential (1997)",
+ "Lacombe Lucien (1974)",
+ "The Lady Eve (1941)",
+ "The Lady Vanishes (1938)",
+ "Ladybird, Ladybird (1994)",
+ "Lamerica (1994, reviewed 1995)",
+ "The Last American Hero (1973)",
+ "The Last Emperor (1987)",
+ "The Last Metro (1980)",
+ "The Last Picture Show (1971)",
+ "The Last Seduction (1994)",
+ "Last Tango in Paris (1973)",
+ "The Last Temptation of Christ (1988)",
+ "The Last Waltz (1978)",
+ "Laura (1944)",
+ "The Lavender Hill Mob (1951)",
+ "Lawrence of Arabia (1962)",
+ "A League of Their Own (1992)",
+ "Leaving Las Vegas (1995)",
+ "The Leopard (1963)",
+ "The Letter (1963)",
+ "A Letter to Three Wives (1949)",
+ "Les Liaisons Dangereuses 1960 (1961)",
+ "The Life and Death of Colonel Blimp (1945)",
+ "Life Is Sweet (1991)",
+ "The Life of Emile Zola (1937)",
+ "Life With Father (1947)",
+ "Like Water for Chocolate (1992, reviewed 1993)",
+ "Lili (1953)",
+ "Little Big Man (1970)",
+ "Little Caesar (1931)",
+ "The Little Foxes (1941)",
+ "The Little Fugitive (1953)",
+ "The Little Kidnappers (1954)",
+ "Little Vera (1988, reviewed 1989)",
+ "Little Women (1933)",
+ "Little Women (1994)",
+ "The Lives of a Bengal Lancer (1935)",
+ "Living in Oblivion (1995)",
+ "Local Hero (1983)",
+ "Lola (1982)",
+ "Lola Montès (1968)",
+ "Lolita (1962)",
+ "Lone Star (1996)",
+ "The Loneliness of the Long Distance Runner (1962)",
+ "Long Day's Journey into Night (1962)",
+ "The Long Goodbye (1973)",
+ "The Long Good Friday (1982)",
+ "The Long Voyage Home (1940)",
+ "The Longest Day (1962)",
+ "Look Back in Anger (1959)",
+ "Lost Horizon (1937)",
+ "Lost in America (1985)",
+ "The Lost Weekend (1945)",
+ "Love (1973)",
+ "Love Affair (1939)",
+ "Love and Death (1975)",
+ "A Love in Germany (1984)",
+ "Love in the Afternoon (1957)",
+ "Lovely and Amazing (2002)",
+ "Love on the Run (1979)",
+ "Lover Come Back (1962)",
+ "The Lovers (1959)",
+ "Loves of a Blonde (1966)",
+ "Loving (1970)",
+ "Lust for Life (1956)",
+ "M (1931, reviewed 1933)",
+ "Mad Max (1980)",
+ "The Madness of King George (1994)",
+ "The Magic Flute (1975)",
+ "The Major and the Minor (1942)",
+ "Major Barbara (1941)",
+ "Make Way for Tomorrow (1937)",
+ "Malcolm X (1992)",
+ "The Maltese Falcon (1941)",
+ "A Man for All Seasons (1966)",
+ "Man Hunt (1941)",
+ "The Man Who Came to Dinner (1942)",
+ "The Man Who Loved Women (1977)",
+ "The Man Who Wasn't There (2001)",
+ "The Man With the Golden Arm (1955)",
+ "The Manchurian Candidate (1962)",
+ "Manhattan (1979)",
+ "Manon of the Spring (1987)",
+ "Marriage Italian Style (1964)",
+ "The Marriage of Maria Braun (1979)",
+ "Married to the Mob (1988)",
+ "The Marrying Kind (1952)",
+ "Marty (1955)",
+ "Mary Poppins (1964)",
+ "M*A*S*H (1970)",
+ "The Match Factory Girl (1990)",
+ "Mayerling (1937)",
+ "McCabe & Mrs. Miller (1971)",
+ "Mean Streets (1973)",
+ "Meet Me in St. Louis (1944)",
+ "Melvin and Howard (1980)",
+ "Memories of Underdevelopment (1973)",
+ "The Memory of Justice (1976)",
+ "The Men (1950)",
+ "Ménage (1986)",
+ "Metropolitan (1990)",
+ "Midnight (1939)",
+ "Midnight Cowboy (1969)",
+ "Minnie and Moskowitz (1971)",
+ "The Miracle of Morgan's Creek (1944)",
+ "Miracle on 34th Street (1947)",
+ "The Miracle Worker (1962)",
+ "Les Miserables (1935)",
+ "The Misfits (1961)",
+ "Missing (1982)",
+ "Mr. and Mrs. Bridge (1990)",
+ "Mr. Deeds Goes to Town (1936)",
+ "Mr. Hulot's Holiday (1954)",
+ "Mister Roberts (1955)",
+ "Mr. Smith Goes to Washington (1939)",
+ "Mrs. Miniver (1942)",
+ "Mon Oncle d'Amérique (1980)",
+ "Mona Lisa (1986)",
+ "Monsieur Verdoux (1947, reviewed 1964)",
+ "Monsters, Inc. (2001)",
+ "Moonlighting (1982)",
+ "Moonstruck (1987)",
+ "The More the Merrier (1943)",
+ "Morgan! (1966)",
+ "The Mortal Storm (1940)",
+ "Mother (1996)",
+ "Moulin Rouge (1953)",
+ "The Mouthpiece (1932)",
+ "Much Ado About Nothing (1993)",
+ "Mulholland Dr. (2001)",
+ "Murmur of the Heart (1971)",
+ "Mutiny on the Bounty (1935)",
+ "My Beautiful Laundrette (1986)",
+ "My Darling Clementine (1946)",
+ "My Dinner With Andre (1981)",
+ "My Fair Lady (1964)",
+ "My Left Foot (1989)",
+ "My Life as a Dog (1987)",
+ "My Man Godfrey (1936)",
+ "My Night at Maud's (1969)",
+ "My Own Private Idaho (1991)",
+ "My 20th Century (1990)",
+ "Mon Oncle (1958)",
+ "The Naked Gun: From the Files of Police Squad! (1988)",
+ "Nashville (1975)",
+ "National Lampoon's Animal House (1978)",
+ "National Velvet (1944)",
+ "Network (1976)",
+ "Never on Sunday (1960)",
+ "Night Moves (1975)",
+ "The Night of the Hunter (1955)",
+ "Night of the Living Dead (1968)",
+ "A Night to Remember (1958)",
+ "A Nightmare on Elm Street (1984)",
+ "1900 (1977)",
+ "Ninotchka (1939)",
+ "Nobody's Fool (1994)",
+ "Norma Rae (1979)",
+ "North by Northwest (1959)",
+ "Nothing But the Best (1964)",
+ "Notorious (1946)",
+ "Now, Voyager (1942)",
+ "La Nuit De Varennes (1983)",
+ "The Nun's Story (1959)",
+ "Odd Man Out (1947)",
+ "Of Mice and Men (1940)",
+ "Oklahoma! (1955)",
+ "Oliver Twist (1951)",
+ "Los Olvidados (1950, reviewed 1952)",
+ "On the Beach (1959)",
+ "On the Town (1949)",
+ "On the Waterfront (1954)",
+ "One False Move (1992)",
+ "One Flew Over the Cuckoo's Nest (1975)",
+ "One Foot in Heaven (1941)",
+ "One Hour with You (1932)",
+ "One Night of Love (1934)",
+ "One Potato, Two Potato (1964)",
+ "One, Two, Three (1961)",
+ "Only Angels Have Wings (1939)",
+ "Open City (1946)",
+ "Operation Crossbow (1965)",
+ "The Opposite of Sex (1998)",
+ "Ordinary People (1980)",
+ "Ossessione (1942, reviewed 1976)",
+ "Othello (1952, reviewed 1955)",
+ "Our Town (1940)",
+ "Out of the Past (1947)",
+ "The Outlaw Josey Wales (1976)",
+ "The Overlanders (1946)",
+ "The Ox-Bow Incident (1943)",
+ "Paint Your Wagon (1969)",
+ "Paisan (1948)",
+ "The Palm Beach Story (1942)",
+ "The Parallax View (1974)",
+ "A Passage to India (1984)",
+ "The Passion of Anna (1970)",
+ "Pather Panchali (1958)",
+ "Paths of Glory (1957)",
+ "Patton (1970)",
+ "The Pawnbroker (1965)",
+ "Payday (1973)",
+ "Pelle the Conqueror (1988)",
+ "The People Vs. Larry Flynt (1996)",
+ "Persona (1967)",
+ "Persuasion (1995)",
+ "Le Petit Theatre de Jean Renoir (1974)",
+ "Petulia (1968)",
+ "The Philadelphia Story (1940)",
+ "The Pianist (2002)",
+ "The Piano (1993)",
+ "Pickup on South Street (1953)",
+ "The Pillow Book (1997)",
+ "Pillow Talk (1959)",
+ "The Pink Panther (1964)",
+ "Pinocchio (1940)",
+ "Pixote (1981)",
+ "A Place in the Sun (1951)",
+ "Places in the Heart (1984)",
+ "Platoon (1986)",
+ "Play Misty for Me (1971)",
+ "The Player (1992)",
+ "Playtime (1967, reviewed 1973)",
+ "Point Blank (1967)",
+ "Poltergeist (1982)",
+ "Ponette (1997)",
+ "Il Postino (The Postman) (1994)",
+ "The Postman Always Rings Twice (1946)",
+ "Pretty Baby (1978)",
+ "Pride and Prejudice (1940)",
+ "The Pride of the Yankees (1942)",
+ "Prince of the City (1981)",
+ "The Prisoner (1955)",
+ "The Private Life of Henry VIII (1933)",
+ "Prizzi's Honor (1985)",
+ "The Producers (1968)",
+ "Psycho (1960)",
+ "The Public Enemy (1931)",
+ "Pulp Fiction (1994)",
+ "The Purple Rose of Cairo (1985)",
+ "Pygmalion (1938)",
+ "Quadrophenia (1979)",
+ "The Quiet Man (1952)",
+ "Raging Bull (1980)",
+ "Raiders of the Lost Ark (1981)",
+ "Rain Man (1988)",
+ "Raise the Red Lantern (1991, reviewed 1992)",
+ "Raising Arizona (1987)",
+ "Ran (1985)",
+ "The Rapture (1991)",
+ "Rashomon (1951)",
+ "Re-Animator (1985)",
+ "Rear Window (1954)",
+ "Rebecca (1940)",
+ "Rebel Without a Cause (1955)",
+ "Red (1994)",
+ "The Red Badge of Courage (1951)",
+ "Red River (1948)",
+ "The Red Shoes (1948)",
+ "Reds (1981)",
+ "The Remains of the Day (1993)",
+ "Repo Man (1984)",
+ "Repulsion (1965)",
+ "Reservoir Dogs (1992)",
+ "The Return of Martin Guerre (1983)",
+ "Reuben, Reuben (1983)",
+ "Reversal of Fortune (1990)",
+ "Richard III (1956)",
+ "Ride the High Country (1962)",
+ "Rififi (1956)",
+ "The Right Stuff (1983)",
+ "Risky Business (1983)",
+ "River's Edge (1987)",
+ "The Road Warrior (1982)",
+ "Robocop (1987)",
+ "Rocco and His Brothers (1960, reviewed 1961)",
+ "Roger & Me (1989)",
+ "Roman Holiday (1953)",
+ "Romeo and Juliet (1936)",
+ "Romeo and Juliet (1968)",
+ "Room at the Top (1959)",
+ "A Room With a View (1986)",
+ "The Rose Tattoo (1955)",
+ "Rosemary's Baby (1968)",
+ "'Round Midnight (1986)",
+ "Ruggles of Red Gap (1935)",
+ "The Rules of the Game (1939, reviewed 1950 and 1961)",
+ "The Ruling Class (1972)",
+ "Rushmore (1998)",
+ "Ruthless People (1986)",
+ "Sahara (1943)",
+ "Salaam Bombay! (1988)",
+ "Salesman (1969)",
+ "Sanjuro (1963)",
+ "Sansho the Bailiff (1969)",
+ "Saturday Night and Sunday Morning (1961)",
+ "Saturday Night Fever (1977)",
+ "Saving Private Ryan (1998)",
+ "Say Anything... (1989)",
+ "Sayonara (1957)",
+ "Scenes From a Marriage (1974)",
+ "Schindler's List (1993)",
+ "The Scoundrel (1935)",
+ "The Search (1948)",
+ "The Searchers (1956)",
+ "Secret Honor (1985)",
+ "Secrets and Lies (1996)",
+ "Sense and Sensibility (1995)",
+ "Sergeant York (1941)",
+ "Serpico (1973)",
+ "The Servant (1963, reviewed 1964)",
+ "The Set-Up (1949)",
+ "Seven Beauties (1976)",
+ "Seven Brides for Seven Brothers (1954)",
+ "Seven Days to Noon (1950)",
+ "The Seven Samurai (1956)",
+ "7 Up/28 Up (1985)",
+ "The Seven Year Itch (1955)",
+ "The Seventh Seal (1958)",
+ "Sex, Lies and Videotape (1989)",
+ "Sexy Beast (2001)",
+ "Shadow of a Doubt (1943)",
+ "Shaft (1971)",
+ "Shakespeare in Love (1998)",
+ "Shane (1953)",
+ "She Wore a Yellow Ribbon (1949)",
+ "Sherman's March (1986)",
+ "She's Gotta Have It (1986)",
+ "The Shining (1980)",
+ "Ship of Fools (1965)",
+ "Shoah (1985)",
+ "Shock Corridor (1963)",
+ "Shoeshine (1947)",
+ "Shoot the Piano Player (1962)",
+ "The Shooting Party (1985)",
+ "The Shootist (1976)",
+ "The Shop Around the Corner (1940)",
+ "The Shop on Main Street (1966)",
+ "A Shot in the Dark (1964)",
+ "Shrek (2001)",
+ "Sid and Nancy (1986)",
+ "The Silence (1964)",
+ "The Silence of the Lambs (1991)",
+ "The Silent World (1956)",
+ "Silk Stockings (1957)",
+ "Silkwood (1983)",
+ "Singin' in the Rain (1952)",
+ "Sitting Pretty (1948)",
+ "Sleeper (1973)",
+ "A Slight Case of Murder (1938)",
+ "Smash Palace (1982)",
+ "Smile (1975)",
+ "Smiles of a Summer Night (1956, reviewed 1957)",
+ "The Snake Pit (1948)",
+ "Snow White and the Seven Dwarfs (1938)",
+ "Some Like It Hot (1959)",
+ "The Sorrow and the Pity (Le Chagrin et la Pitié) (1971)",
+ "The Sound of Music (1965)",
+ "South Pacific (1958)",
+ "Spartacus (1960)",
+ "Spellbound (1945)",
+ "The Spiral Staircase (1946)",
+ "Spirited Away (2002)",
+ "Splendor in the Grass (1961)",
+ "Stage Door (1937)",
+ "Stagecoach (1939)",
+ "Stairway to Heaven (1946)",
+ "Stalag 17 (1953)",
+ "A Star Is Born (1937)",
+ "Star Trek II: The Wrath of Khan (1982)",
+ "Star Wars (1977)",
+ "Starman (1984)",
+ "The Stars Look Down (1941)",
+ "State Fair (1933)",
+ "Stevie (1981)",
+ "Stolen Kisses (1969)",
+ "Stop Making Sense (1984)",
+ "Stormy Monday (1988)",
+ "The Story of Adèle H. (1975)",
+ "The Story of G.I. Joe (1945)",
+ "The Story of Qiu Ju (1992)",
+ "Story of Women (1989)",
+ "Storytelling (2001)",
+ "La Strada (1956)",
+ "The Straight Story (1999)",
+ "Straight Time (1978)",
+ "Stranger Than Paradise (1984)",
+ "Strangers on a Train (1951)",
+ "Straw Dogs (1971)",
+ "A Streetcar Named Desire (1951)",
+ "Stroszek (1977)",
+ "Suddenly, Last Summer (1959)",
+ "The Sugarland Express (1974)",
+ "Sullivan's Travels (1941)",
+ "Summer (1986)",
+ "Summertime (1955)",
+ "Sunday Bloody Sunday (1971)",
+ "Sundays and Cybele (1962)",
+ "Sunset Boulevard (1950)",
+ "Suspicion (1941)",
+ "The Sweet Hereafter (1997)",
+ "Sweet Smell of Success (1957)",
+ "Sweet Sweetback's Baadasssss Song (1971)",
+ "Swept Away (By an Unusual Destiny in the Blue Sea of August) (1974)",
+ "Swing Time (1936)",
+ "The Taking of Pelham One Two Three (1974)",
+ "Talk to Her (2002)",
+ "Tampopo (1986)",
+ "Taste of Cherry (1997)",
+ "A Taste of Honey (1961, reviewed 1962)",
+ "Taxi Driver (1976)",
+ "A Taxing Woman (1987)",
+ "A Taxing Woman's Return (1988)",
+ "Tell Them Willie Boy Is Here (1969)",
+ "10 (1979)",
+ "The Ten Commandments (1956)",
+ "Tender Mercies (1983)",
+ "The Tender Trap (1955)",
+ "Terms of Endearment (1983)",
+ "La Terra trema (1947, reviewed 1965)",
+ "Tess (1980)",
+ "That Obscure Object of Desire (1977)",
+ "That's Life! (1986)",
+ "Thelma & Louise (1991)",
+ "These Three (1936)",
+ "They Live by Night (1949)",
+ "They Shoot Horses, Don't They? (1969)",
+ "They Were Expendable (1945)",
+ "They Won't Forget (1937)",
+ "The Thief of Bagdad (1940)",
+ "The Thin Blue Line (1988)",
+ "The Thin Man (1934)",
+ "The Thin Red Line (1998)",
+ "The Third Generation (1979, reviewed 1980)",
+ "The Third Man (1949)",
+ "The Thirty-Nine Steps (1935)",
+ "Thirty Two Short Films About Glenn Gould (1994)",
+ "This Is Spinal Tap (1984)",
+ "The Man Must Die (1970)",
+ "This Sporting Life (1963)",
+ "Three Comrades (1938)",
+ "Three Days of the Condor (1975)",
+ "Throne of Blood (1957)",
+ "Tight Little Island (1949)",
+ "The Tin Drum (1979)",
+ "To Be or Not to Be (1942)",
+ "To Catch a Thief (1955)",
+ "To Have and Have Not (1944)",
+ "To Kill a Mockingbird (1962)",
+ "To Live (1994)",
+ "Tokyo Story (1953)",
+ "Tom Jones (1963)",
+ "Tootsie (1982)",
+ "Top Hat (1935)",
+ "Topaz (1969)",
+ "Topkapi (1964)",
+ "Total Recall (1990)",
+ "Touch of Evil (1958)",
+ "Toy Story (1995)",
+ "Traffic (2000)",
+ "The Train (1965)",
+ "Trainspotting (1996)",
+ "The Treasure of the Sierra Madre (1948)",
+ "A Tree Grows in Brooklyn (1945)",
+ "The Tree of the Wooden Clogs (1979)",
+ "The Trip to Bountiful (1985)",
+ "Tristana (1970)",
+ "Trouble in Paradise (1932)",
+ "The Trouble with Harry (1955)",
+ "True Grit (1969)",
+ "True Love (1989)",
+ "Trust (1991)",
+ "Tunes of Glory (1960)",
+ "12 Angry Men (1957)",
+ "Twelve O'Clock High (1949)",
+ "Twentieth Century (1934)",
+ "Two English Girls (1971)",
+ "The Two of Us (1968)",
+ "2001: A Space Odyssey (1968)",
+ "Two Women (1961)",
+ "Ugetsu (1954)",
+ "Ulzana's Raid (1972)",
+ "Umberto D. (1952)",
+ "The Unbearable Lightness of Being (1988)",
+ "Unforgiven (1992)",
+ "The Usual Suspects (1995)",
+ "Vanya on 42nd Street (1994)",
+ "The Verdict (1982)",
+ "Vertigo (1958)",
+ "Videodrome (1982)",
+ "Violette Nozière (1978)",
+ "Viridiana (1962)",
+ "Viva Zapata! (1952)",
+ "The Voice of the Turtle (1947)",
+ "The Wages of Fear (1955)",
+ "Waking Life (2001)",
+ "Walkabout (1971)",
+ "A Walk in the Sun (1945)",
+ "The War Game (1966)",
+ "The War of the Roses (1989)",
+ "The Warriors (1979)",
+ "Watch on the Rhine (1943)",
+ "The Waterdance (1991)",
+ "The Way We Were (1973)",
+ "Weekend (1968)",
+ "Welcome to the Dollhouse (1996)",
+ "The Well-Digger's Daughter (1941)",
+ "West Side Story (1961)",
+ "The Whales of August (1987)",
+ "What Ever Happened to Baby Jane? (1962)",
+ "What's Eating Gilbert Grape (1993)",
+ "What's Up, Doc? (1972)",
+ "When Harry Met Sally (1989)",
+ "White Heat (1949)",
+ "Who Framed Roger Rabbit (1988)",
+ "Who's Afraid of Virginia Woolf? (1966)",
+ "The Wild Bunch (1969)",
+ "The Wild Child (1970)",
+ "Wild Reeds (1994)",
+ "Wild Strawberries (1959)",
+ "Wilson (1944)",
+ "Wings of Desire (1988)",
+ "Wise Blood (1979)",
+ "The Wizard of Oz (1939)",
+ "Woman in the Dunes (1964)",
+ "Woman of the Year (1942)",
+ "The Women (1939)",
+ "Women in Love (1970)",
+ "Women on the Verge of a Nervous Breakdown (1988)",
+ "Woodstock (1970)",
+ "Working Girl (1988)",
+ "The World of Apu (1959, reviewed 1960)",
+ "The World of Henry Orient (1964)",
+ "Written on the Wind (1956)",
+ "Wuthering Heights (1939)",
+ "Yankee Doodle Dandy (1942)",
+ "The Year of Living Dangerously (1982)",
+ "The Yearling (1983)",
+ "Yellow Submarine (1968)",
+ "Yi Yi: A One and a Two (2000)",
+ "Yojimbo (1961)",
+ "You Can Count On Me (2000)",
+ "You Only Live Once (1937)",
+ "Young Frankenstein (1974)",
+ "Young Mr. Lincoln (1939)",
+ "Y Tu Mamá También (2001)",
+ "Z (1969)",
+ "Zero for Conduct (1933)"]
\ No newline at end of file
diff --git a/dist-browser/fuzzaldrin-plus.js b/dist-browser/fuzzaldrin-plus.js
new file mode 100644
index 0000000..198ec16
--- /dev/null
+++ b/dist-browser/fuzzaldrin-plus.js
@@ -0,0 +1,914 @@
+/* fuzzaldrin-plus - v0.3.1 - @license: MIT; @author: Jean Christophe Roy; @site: https://github.com/jeancroy/fuzzaldrin-plus */
+
+(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.fuzzaldrin = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);i [...]
+(function() {
+ var defaultPathSeparator, pluckCandidates, scorer, sortCandidates;
+
+ scorer = require('./scorer');
+
+ defaultPathSeparator = scorer.pathSeparator;
+
+ pluckCandidates = function(a) {
+ return a.candidate;
+ };
+
+ sortCandidates = function(a, b) {
+ return b.score - a.score;
+ };
+
+ module.exports = function(candidates, query, options) {
+ var allowErrors, bKey, candidate, isPath, key, maxInners, maxResults, optCharRegEx, pathSeparator, prepQuery, score, scoredCandidates, spotLeft, string, useExtensionBonus, _i, _len;
+ if (options == null) {
+ options = {};
+ }
+ scoredCandidates = [];
+ key = options.key, maxResults = options.maxResults, maxInners = options.maxInners, allowErrors = options.allowErrors, isPath = options.isPath, useExtensionBonus = options.useExtensionBonus, optCharRegEx = options.optCharRegEx, pathSeparator = options.pathSeparator;
+ spotLeft = (maxInners != null) && maxInners > 0 ? maxInners : candidates.length;
+ bKey = key != null;
+ prepQuery = scorer.prepQuery(query, options);
+ for (_i = 0, _len = candidates.length; _i < _len; _i++) {
+ candidate = candidates[_i];
+ string = bKey ? candidate[key] : candidate;
+ if (!string) {
+ continue;
+ }
+ score = scorer.score(string, query, prepQuery, allowErrors, isPath, useExtensionBonus, pathSeparator);
+ if (score > 0) {
+ scoredCandidates.push({
+ candidate: candidate,
+ score: score
+ });
+ if (!--spotLeft) {
+ break;
+ }
+ }
+ }
+ scoredCandidates.sort(sortCandidates);
+ candidates = scoredCandidates.map(pluckCandidates);
+ if (maxResults != null) {
+ candidates = candidates.slice(0, maxResults);
+ }
+ return candidates;
+ };
+
+}).call(this);
+
+},{"./scorer":4}],2:[function(require,module,exports){
+(function() {
+ var filter, matcher, parseOptions, prepQueryCache, scorer;
+
+ scorer = require('./scorer');
+
+ filter = require('./filter');
+
+ matcher = require('./matcher');
+
+ prepQueryCache = null;
+
+ module.exports = {
+ filter: function(candidates, query, options) {
+ if (options == null) {
+ options = {};
+ }
+ if (!((query != null ? query.length : void 0) && (candidates != null ? candidates.length : void 0))) {
+ return [];
+ }
+ options = parseOptions(options);
+ return filter(candidates, query, options);
+ },
+ prepQuery: function(query, options) {
+ if (options == null) {
+ options = {};
+ }
+ options = parseOptions(options);
+ return scorer.prepQuery(query, options);
+ },
+ score: function(string, query, prepQuery, options) {
+ var allowErrors, isPath, optCharRegEx, pathSeparator, useExtensionBonus;
+ if (options == null) {
+ options = {};
+ }
+ if (!((string != null ? string.length : void 0) && (query != null ? query.length : void 0))) {
+ return 0;
+ }
+ options = parseOptions(options);
+ allowErrors = options.allowErrors, isPath = options.isPath, useExtensionBonus = options.useExtensionBonus, optCharRegEx = options.optCharRegEx, pathSeparator = options.pathSeparator;
+ if (prepQuery == null) {
+ prepQuery = prepQueryCache && prepQueryCache.query === query ? prepQueryCache : (prepQueryCache = scorer.prepQuery(query, options));
+ }
+ return scorer.score(string, query, prepQuery, allowErrors, isPath, useExtensionBonus, pathSeparator);
+ },
+ match: function(string, query, prepQuery, options) {
+ var allowErrors, baseMatches, isPath, matches, optCharRegEx, pathSeparator, string_lw, useExtensionBonus, _i, _ref, _results;
+ if (options == null) {
+ options = {};
+ }
+ if (!string) {
+ return [];
+ }
+ if (!query) {
+ return [];
+ }
+ if (string === query) {
+ return (function() {
+ _results = [];
+ for (var _i = 0, _ref = string.length; 0 <= _ref ? _i < _ref : _i > _ref; 0 <= _ref ? _i++ : _i--){ _results.push(_i); }
+ return _results;
+ }).apply(this);
+ }
+ options = parseOptions(options);
+ allowErrors = options.allowErrors, isPath = options.isPath, useExtensionBonus = options.useExtensionBonus, optCharRegEx = options.optCharRegEx, pathSeparator = options.pathSeparator;
+ if (prepQuery == null) {
+ prepQuery = prepQueryCache && prepQueryCache.query === query ? prepQueryCache : (prepQueryCache = scorer.prepQuery(query, options));
+ }
+ if (!(allowErrors || scorer.isMatch(string, prepQuery.core_lw, prepQuery.core_up))) {
+ return [];
+ }
+ string_lw = string.toLowerCase();
+ matches = matcher.match(string, string_lw, prepQuery);
+ if (matches.length === 0) {
+ return matches;
+ }
+ if (string.indexOf(pathSeparator) > -1) {
+ baseMatches = matcher.basenameMatch(string, string_lw, prepQuery, pathSeparator);
+ matches = matcher.mergeMatches(matches, baseMatches);
+ }
+ return matches;
+ }
+ };
+
+ parseOptions = function(options) {
+ if (options.allowErrors == null) {
+ options.allowErrors = false;
+ }
+ if (options.isPath == null) {
+ options.isPath = true;
+ }
+ if (options.useExtensionBonus == null) {
+ options.useExtensionBonus = true;
+ }
+ if (options.pathSeparator == null) {
+ options.pathSeparator = scorer.pathSeparator;
+ }
+ if (options.optCharRegEx == null) {
+ options.optCharRegEx = null;
+ }
+ return options;
+ };
+
+}).call(this);
+
+},{"./filter":1,"./matcher":3,"./scorer":4}],3:[function(require,module,exports){
+(function() {
+ var scorer;
+
+ scorer = require('./scorer');
+
+ exports.basenameMatch = function(subject, subject_lw, prepQuery, pathSeparator) {
+ var basePos, depth, end;
+ end = subject.length - 1;
+ while (subject[end] === pathSeparator) {
+ end--;
+ }
+ basePos = subject.lastIndexOf(pathSeparator, end);
+ if (basePos === -1) {
+ return [];
+ }
+ depth = prepQuery.depth;
+ while (depth-- > 0) {
+ basePos = subject.lastIndexOf(pathSeparator, basePos - 1);
+ if (basePos === -1) {
+ return [];
+ }
+ }
+ basePos++;
+ end++;
+ return exports.match(subject.slice(basePos, end), subject_lw.slice(basePos, end), prepQuery, basePos);
+ };
+
+ exports.mergeMatches = function(a, b) {
+ var ai, bj, i, j, m, n, out;
+ m = a.length;
+ n = b.length;
+ if (n === 0) {
+ return a.slice();
+ }
+ if (m === 0) {
+ return b.slice();
+ }
+ i = -1;
+ j = 0;
+ bj = b[j];
+ out = [];
+ while (++i < m) {
+ ai = a[i];
+ while (bj <= ai && ++j < n) {
+ if (bj < ai) {
+ out.push(bj);
+ }
+ bj = b[j];
+ }
+ out.push(ai);
+ }
+ while (j < n) {
+ out.push(b[j++]);
+ }
+ return out;
+ };
+
+ exports.match = function(subject, subject_lw, prepQuery, offset) {
+ var DIAGONAL, LEFT, STOP, UP, acro_score, align, backtrack, csc_diag, csc_row, csc_score, i, j, m, matches, move, n, pos, query, query_lw, score, score_diag, score_row, score_up, si_lw, start, trace;
+ if (offset == null) {
+ offset = 0;
+ }
+ query = prepQuery.query;
+ query_lw = prepQuery.query_lw;
+ m = subject.length;
+ n = query.length;
+ acro_score = scorer.scoreAcronyms(subject, subject_lw, query, query_lw).score;
+ score_row = new Array(n);
+ csc_row = new Array(n);
+ STOP = 0;
+ UP = 1;
+ LEFT = 2;
+ DIAGONAL = 3;
+ trace = new Array(m * n);
+ pos = -1;
+ j = -1;
+ while (++j < n) {
+ score_row[j] = 0;
+ csc_row[j] = 0;
+ }
+ i = -1;
+ while (++i < m) {
+ score = 0;
+ score_up = 0;
+ csc_diag = 0;
+ si_lw = subject_lw[i];
+ j = -1;
+ while (++j < n) {
+ csc_score = 0;
+ align = 0;
+ score_diag = score_up;
+ if (query_lw[j] === si_lw) {
+ start = scorer.isWordStart(i, subject, subject_lw);
+ csc_score = csc_diag > 0 ? csc_diag : scorer.scoreConsecutives(subject, subject_lw, query, query_lw, i, j, start);
+ align = score_diag + scorer.scoreCharacter(i, j, start, acro_score, csc_score);
+ }
+ score_up = score_row[j];
+ csc_diag = csc_row[j];
+ if (score > score_up) {
+ move = LEFT;
+ } else {
+ score = score_up;
+ move = UP;
+ }
+ if (align > score) {
+ score = align;
+ move = DIAGONAL;
+ } else {
+ csc_score = 0;
+ }
+ score_row[j] = score;
+ csc_row[j] = csc_score;
+ trace[++pos] = score > 0 ? move : STOP;
+ }
+ }
+ i = m - 1;
+ j = n - 1;
+ pos = i * n + j;
+ backtrack = true;
+ matches = [];
+ while (backtrack && i >= 0 && j >= 0) {
+ switch (trace[pos]) {
+ case UP:
+ i--;
+ pos -= n;
+ break;
+ case LEFT:
+ j--;
+ pos--;
+ break;
+ case DIAGONAL:
+ matches.push(i + offset);
+ j--;
+ i--;
+ pos -= n + 1;
+ break;
+ default:
+ backtrack = false;
+ }
+ }
+ matches.reverse();
+ return matches;
+ };
+
+}).call(this);
+
+},{"./scorer":4}],4:[function(require,module,exports){
+(function (process){
+(function() {
+ var AcronymResult, Query, coreChars, countDir, defaultPathSeparator, emptyAcronymResult, file_coeff, getCharCodes, getExtension, getExtensionScore, isAcronymFullWord, isMatch, isSeparator, isWordEnd, isWordStart, miss_coeff, opt_char_re, pos_bonus, scoreAcronyms, scoreCharacter, scoreConsecutives, scoreExact, scoreExactMatch, scoreMain, scorePath, scorePattern, scorePosition, scoreSize, tau_depth, tau_size, truncatedUpperCase, wm;
+
+ defaultPathSeparator = exports.pathSeparator = process && (process.platform === "win32") ? '\\' : '/';
+
+ wm = 150;
+
+ pos_bonus = 20;
+
+ tau_depth = 13;
+
+ tau_size = 85;
+
+ file_coeff = 1.2;
+
+ miss_coeff = 0.75;
+
+ opt_char_re = /[ _\-:\/\\]/g;
+
+ exports.coreChars = coreChars = function(query, optCharRegEx) {
+ if (optCharRegEx == null) {
+ optCharRegEx = opt_char_re;
+ }
+ return query.replace(optCharRegEx, '');
+ };
+
+ exports.score = function(string, query, prepQuery, allowErrors, isPath, useExtensionBonus, pathSeparator) {
+ var score, string_lw;
+ if (!(allowErrors || isMatch(string, prepQuery.core_lw, prepQuery.core_up))) {
+ return 0;
+ }
+ string_lw = string.toLowerCase();
+ score = scoreMain(string, string_lw, prepQuery);
+ if (isPath) {
+ score = scorePath(string, string_lw, prepQuery, score, useExtensionBonus, pathSeparator);
+ }
+ return Math.ceil(score);
+ };
+
+ Query = (function() {
+ function Query(query, _arg) {
+ var optCharRegEx, pathSeparator, _ref;
+ _ref = _arg != null ? _arg : {}, optCharRegEx = _ref.optCharRegEx, pathSeparator = _ref.pathSeparator;
+ if (!(query && query.length)) {
+ return null;
+ }
+ this.query = query;
+ this.query_lw = query.toLowerCase();
+ this.core = coreChars(query, optCharRegEx);
+ this.core_lw = this.core.toLowerCase();
+ this.core_up = truncatedUpperCase(this.core);
+ this.depth = countDir(query, query.length, pathSeparator);
+ this.ext = getExtension(this.query_lw);
+ this.charCodes = getCharCodes(this.query_lw);
+ }
+
+ return Query;
+
+ })();
+
+ exports.prepQuery = function(query, options) {
+ return new Query(query, options);
+ };
+
+ exports.isMatch = isMatch = function(subject, query_lw, query_up) {
+ var i, j, m, n, qj_lw, qj_up, si;
+ m = subject.length;
+ n = query_lw.length;
+ if (!m || n > m) {
+ return false;
+ }
+ i = -1;
+ j = -1;
+ while (++j < n) {
+ qj_lw = query_lw.charCodeAt(j);
+ qj_up = query_up.charCodeAt(j);
+ while (++i < m) {
+ si = subject.charCodeAt(i);
+ if (si === qj_lw || si === qj_up) {
+ break;
+ }
+ }
+ if (i === m) {
+ return false;
+ }
+ }
+ return true;
+ };
+
+ scoreMain = function(subject, subject_lw, prepQuery) {
+ var acro, acro_score, align, csc_diag, csc_invalid, csc_row, csc_score, i, j, m, miss_budget, miss_left, mm, n, pos, query, query_lw, record_miss, score, score_diag, score_row, score_up, si_lw, start, sz;
+ query = prepQuery.query;
+ query_lw = prepQuery.query_lw;
+ m = subject.length;
+ n = query.length;
+ acro = scoreAcronyms(subject, subject_lw, query, query_lw);
+ acro_score = acro.score;
+ if (acro.count === n) {
+ return scoreExact(n, m, acro_score, acro.pos);
+ }
+ pos = subject_lw.indexOf(query_lw);
+ if (pos > -1) {
+ return scoreExactMatch(subject, subject_lw, query, query_lw, pos, n, m);
+ }
+ score_row = new Array(n);
+ csc_row = new Array(n);
+ sz = scoreSize(n, m);
+ miss_budget = Math.ceil(miss_coeff * n) + 5;
+ miss_left = miss_budget;
+ j = -1;
+ while (++j < n) {
+ score_row[j] = 0;
+ csc_row[j] = 0;
+ }
+ i = subject_lw.indexOf(query_lw[0]);
+ if (i > -1) {
+ i--;
+ }
+ mm = subject_lw.lastIndexOf(query_lw[n - 1], m);
+ if (mm > i) {
+ m = mm + 1;
+ }
+ csc_invalid = true;
+ while (++i < m) {
+ si_lw = subject_lw[i];
+ if (prepQuery.charCodes[si_lw.charCodeAt(0)] == null) {
+ if (csc_invalid !== true) {
+ j = -1;
+ while (++j < n) {
+ csc_row[j] = 0;
+ }
+ csc_invalid = true;
+ }
+ continue;
+ }
+ score = 0;
+ score_diag = 0;
+ csc_diag = 0;
+ record_miss = true;
+ csc_invalid = false;
+ j = -1;
+ while (++j < n) {
+ score_up = score_row[j];
+ if (score_up > score) {
+ score = score_up;
+ }
+ csc_score = 0;
+ if (query_lw[j] === si_lw) {
+ start = isWordStart(i, subject, subject_lw);
+ csc_score = csc_diag > 0 ? csc_diag : scoreConsecutives(subject, subject_lw, query, query_lw, i, j, start);
+ align = score_diag + scoreCharacter(i, j, start, acro_score, csc_score);
+ if (align > score) {
+ score = align;
+ miss_left = miss_budget;
+ } else {
+ if (record_miss && --miss_left <= 0) {
+ return score_row[n - 1] * sz;
+ }
+ record_miss = false;
+ }
+ }
+ score_diag = score_up;
+ csc_diag = csc_row[j];
+ csc_row[j] = csc_score;
+ score_row[j] = score;
+ }
+ }
+ score = score_row[n - 1];
+ return score * sz;
+ };
+
+ exports.isWordStart = isWordStart = function(pos, subject, subject_lw) {
+ var curr_s, prev_s;
+ if (pos === 0) {
+ return true;
+ }
+ curr_s = subject[pos];
+ prev_s = subject[pos - 1];
+ return isSeparator(prev_s) || (curr_s !== subject_lw[pos] && prev_s === subject_lw[pos - 1]);
+ };
+
+ exports.isWordEnd = isWordEnd = function(pos, subject, subject_lw, len) {
+ var curr_s, next_s;
+ if (pos === len - 1) {
+ return true;
+ }
+ curr_s = subject[pos];
+ next_s = subject[pos + 1];
+ return isSeparator(next_s) || (curr_s === subject_lw[pos] && next_s !== subject_lw[pos + 1]);
+ };
+
+ isSeparator = function(c) {
+ return c === ' ' || c === '.' || c === '-' || c === '_' || c === '/' || c === '\\';
+ };
+
+ scorePosition = function(pos) {
+ var sc;
+ if (pos < pos_bonus) {
+ sc = pos_bonus - pos;
+ return 100 + sc * sc;
+ } else {
+ return Math.max(100 + pos_bonus - pos, 0);
+ }
+ };
+
+ scoreSize = function(n, m) {
+ return tau_size / (tau_size + Math.abs(m - n));
+ };
+
+ scoreExact = function(n, m, quality, pos) {
+ return 2 * n * (wm * quality + scorePosition(pos)) * scoreSize(n, m);
+ };
+
+ exports.scorePattern = scorePattern = function(count, len, sameCase, start, end) {
+ var bonus, sz;
+ sz = count;
+ bonus = 6;
+ if (sameCase === count) {
+ bonus += 2;
+ }
+ if (start) {
+ bonus += 3;
+ }
+ if (end) {
+ bonus += 1;
+ }
+ if (count === len) {
+ if (start) {
+ if (sameCase === len) {
+ sz += 2;
+ } else {
+ sz += 1;
+ }
+ }
+ if (end) {
+ bonus += 1;
+ }
+ }
+ return sameCase + sz * (sz + bonus);
+ };
+
+ exports.scoreCharacter = scoreCharacter = function(i, j, start, acro_score, csc_score) {
+ var posBonus;
+ posBonus = scorePosition(i);
+ if (start) {
+ return posBonus + wm * ((acro_score > csc_score ? acro_score : csc_score) + 10);
+ }
+ return posBonus + wm * csc_score;
+ };
+
+ exports.scoreConsecutives = scoreConsecutives = function(subject, subject_lw, query, query_lw, i, j, startOfWord) {
+ var k, m, mi, n, nj, sameCase, sz;
+ m = subject.length;
+ n = query.length;
+ mi = m - i;
+ nj = n - j;
+ k = mi < nj ? mi : nj;
+ sameCase = 0;
+ sz = 0;
+ if (query[j] === subject[i]) {
+ sameCase++;
+ }
+ while (++sz < k && query_lw[++j] === subject_lw[++i]) {
+ if (query[j] === subject[i]) {
+ sameCase++;
+ }
+ }
+ if (sz === 1) {
+ return 1 + 2 * sameCase;
+ }
+ return scorePattern(sz, n, sameCase, startOfWord, isWordEnd(i, subject, subject_lw, m));
+ };
+
+ exports.scoreExactMatch = scoreExactMatch = function(subject, subject_lw, query, query_lw, pos, n, m) {
+ var end, i, pos2, sameCase, start;
+ start = isWordStart(pos, subject, subject_lw);
+ if (!start) {
+ pos2 = subject_lw.indexOf(query_lw, pos + 1);
+ if (pos2 > -1) {
+ start = isWordStart(pos2, subject, subject_lw);
+ if (start) {
+ pos = pos2;
+ }
+ }
+ }
+ i = -1;
+ sameCase = 0;
+ while (++i < n) {
+ if (query[pos + i] === subject[i]) {
+ sameCase++;
+ }
+ }
+ end = isWordEnd(pos + n - 1, subject, subject_lw, m);
+ return scoreExact(n, m, scorePattern(n, n, sameCase, start, end), pos);
+ };
+
+ AcronymResult = (function() {
+ function AcronymResult(score, pos, count) {
+ this.score = score;
+ this.pos = pos;
+ this.count = count;
+ }
+
+ return AcronymResult;
+
+ })();
+
+ emptyAcronymResult = new AcronymResult(0, 0.1, 0);
+
+ exports.scoreAcronyms = scoreAcronyms = function(subject, subject_lw, query, query_lw) {
+ var count, fullWord, i, j, m, n, qj_lw, sameCase, score, sepCount, sumPos;
+ m = subject.length;
+ n = query.length;
+ if (!(m > 1 && n > 1)) {
+ return emptyAcronymResult;
+ }
+ count = 0;
+ sepCount = 0;
+ sumPos = 0;
+ sameCase = 0;
+ i = -1;
+ j = -1;
+ while (++j < n) {
+ qj_lw = query_lw[j];
+ if (isSeparator(qj_lw)) {
+ i = subject_lw.indexOf(qj_lw, i + 1);
+ if (i > -1) {
+ sepCount++;
+ continue;
+ } else {
+ break;
+ }
+ }
+ while (++i < m) {
+ if (qj_lw === subject_lw[i] && isWordStart(i, subject, subject_lw)) {
+ if (query[j] === subject[i]) {
+ sameCase++;
+ }
+ sumPos += i;
+ count++;
+ break;
+ }
+ }
+ if (i === m) {
+ break;
+ }
+ }
+ if (count < 2) {
+ return emptyAcronymResult;
+ }
+ fullWord = count === n ? isAcronymFullWord(subject, subject_lw, query, count) : false;
+ score = scorePattern(count, n, sameCase, true, fullWord);
+ return new AcronymResult(score, sumPos / count, count + sepCount);
+ };
+
+ isAcronymFullWord = function(subject, subject_lw, query, nbAcronymInQuery) {
+ var count, i, m, n;
+ m = subject.length;
+ n = query.length;
+ count = 0;
+ if (m > 12 * n) {
+ return false;
+ }
+ i = -1;
+ while (++i < m) {
+ if (isWordStart(i, subject, subject_lw) && ++count > nbAcronymInQuery) {
+ return false;
+ }
+ }
+ return true;
+ };
+
+ scorePath = function(subject, subject_lw, prepQuery, fullPathScore, useExtensionBonus, pathSeparator) {
+ var alpha, basePathScore, basePos, depth, end, extAdjust;
+ if (fullPathScore === 0) {
+ return 0;
+ }
+ end = subject.length - 1;
+ while (subject[end] === pathSeparator) {
+ end--;
+ }
+ basePos = subject.lastIndexOf(pathSeparator, end);
+ extAdjust = 1.0;
+ if (useExtensionBonus) {
+ extAdjust += getExtensionScore(subject_lw, prepQuery.ext, basePos, end, 2);
+ fullPathScore *= extAdjust;
+ }
+ if (basePos === -1) {
+ return fullPathScore;
+ }
+ depth = prepQuery.depth;
+ while (basePos > -1 && depth-- > 0) {
+ basePos = subject.lastIndexOf(pathSeparator, basePos - 1);
+ }
+ basePathScore = basePos === -1 ? fullPathScore : extAdjust * scoreMain(subject.slice(basePos + 1, end + 1), subject_lw.slice(basePos + 1, end + 1), prepQuery);
+ alpha = 0.5 * tau_depth / (tau_depth + countDir(subject, end + 1, pathSeparator));
+ return alpha * basePathScore + (1 - alpha) * fullPathScore * scoreSize(0, file_coeff * (end - basePos));
+ };
+
+ exports.countDir = countDir = function(path, end, pathSeparator) {
+ var count, i;
+ if (end < 1) {
+ return 0;
+ }
+ count = 0;
+ i = -1;
+ while (++i < end && path[i] === pathSeparator) {
+ continue;
+ }
+ while (++i < end) {
+ if (path[i] === pathSeparator) {
+ count++;
+ while (++i < end && path[i] === pathSeparator) {
+ continue;
+ }
+ }
+ }
+ return count;
+ };
+
+ getExtension = function(str) {
+ var pos;
+ pos = str.lastIndexOf(".");
+ if (pos < 0) {
+ return "";
+ } else {
+ return str.substr(pos + 1);
+ }
+ };
+
+ getExtensionScore = function(candidate, ext, startPos, endPos, maxDepth) {
+ var m, matched, n, pos;
+ if (!ext.length) {
+ return 0;
+ }
+ pos = candidate.lastIndexOf(".", endPos);
+ if (!(pos > startPos)) {
+ return 0;
+ }
+ n = ext.length;
+ m = endPos - pos;
+ if (m < n) {
+ n = m;
+ m = ext.length;
+ }
+ pos++;
+ matched = -1;
+ while (++matched < n) {
+ if (candidate[pos + matched] !== ext[matched]) {
+ break;
+ }
+ }
+ if (matched === 0 && maxDepth > 0) {
+ return 0.9 * getExtensionScore(candidate, ext, startPos, pos - 2, maxDepth - 1);
+ }
+ return matched / m;
+ };
+
+ truncatedUpperCase = function(str) {
+ var char, upper, _i, _len;
+ upper = "";
+ for (_i = 0, _len = str.length; _i < _len; _i++) {
+ char = str[_i];
+ upper += char.toUpperCase()[0];
+ }
+ return upper;
+ };
+
+ getCharCodes = function(str) {
+ var charCodes, i, len;
+ len = str.length;
+ i = -1;
+ charCodes = [];
+ while (++i < len) {
+ charCodes[str.charCodeAt(i)] = true;
+ }
+ return charCodes;
+ };
+
+}).call(this);
+
+}).call(this,require('_process'))
+},{"_process":5}],5:[function(require,module,exports){
+// shim for using process in browser
+
+var process = module.exports = {};
+
+// cached from whatever global is present so that test runners that stub it
+// don't break things. But we need to wrap it in a try catch in case it is
+// wrapped in strict mode code which doesn't define any globals. It's inside a
+// function because try/catches deoptimize in certain engines.
+
+var cachedSetTimeout;
+var cachedClearTimeout;
+
+(function () {
+ try {
+ cachedSetTimeout = setTimeout;
+ } catch (e) {
+ cachedSetTimeout = function () {
+ throw new Error('setTimeout is not defined');
+ }
+ }
+ try {
+ cachedClearTimeout = clearTimeout;
+ } catch (e) {
+ cachedClearTimeout = function () {
+ throw new Error('clearTimeout is not defined');
+ }
+ }
+} ())
+var queue = [];
+var draining = false;
+var currentQueue;
+var queueIndex = -1;
+
+function cleanUpNextTick() {
+ if (!draining || !currentQueue) {
+ return;
+ }
+ draining = false;
+ if (currentQueue.length) {
+ queue = currentQueue.concat(queue);
+ } else {
+ queueIndex = -1;
+ }
+ if (queue.length) {
+ drainQueue();
+ }
+}
+
+function drainQueue() {
+ if (draining) {
+ return;
+ }
+ var timeout = cachedSetTimeout(cleanUpNextTick);
+ draining = true;
+
+ var len = queue.length;
+ while(len) {
+ currentQueue = queue;
+ queue = [];
+ while (++queueIndex < len) {
+ if (currentQueue) {
+ currentQueue[queueIndex].run();
+ }
+ }
+ queueIndex = -1;
+ len = queue.length;
+ }
+ currentQueue = null;
+ draining = false;
+ cachedClearTimeout(timeout);
+}
+
+process.nextTick = function (fun) {
+ var args = new Array(arguments.length - 1);
+ if (arguments.length > 1) {
+ for (var i = 1; i < arguments.length; i++) {
+ args[i - 1] = arguments[i];
+ }
+ }
+ queue.push(new Item(fun, args));
+ if (queue.length === 1 && !draining) {
+ cachedSetTimeout(drainQueue, 0);
+ }
+};
+
+// v8 likes predictible objects
+function Item(fun, array) {
+ this.fun = fun;
+ this.array = array;
+}
+Item.prototype.run = function () {
+ this.fun.apply(null, this.array);
+};
+process.title = 'browser';
+process.browser = true;
+process.env = {};
+process.argv = [];
+process.version = ''; // empty string to avoid regexp issues
+process.versions = {};
+
+function noop() {}
+
+process.on = noop;
+process.addListener = noop;
+process.once = noop;
+process.off = noop;
+process.removeListener = noop;
+process.removeAllListeners = noop;
+process.emit = noop;
+
+process.binding = function (name) {
+ throw new Error('process.binding is not supported');
+};
+
+process.cwd = function () { return '/' };
+process.chdir = function (dir) {
+ throw new Error('process.chdir is not supported');
+};
+process.umask = function() { return 0; };
+
+},{}]},{},[2])(2)
+});
\ No newline at end of file
diff --git a/dist-browser/fuzzaldrin-plus.min.js b/dist-browser/fuzzaldrin-plus.min.js
new file mode 100644
index 0000000..38531d9
--- /dev/null
+++ b/dist-browser/fuzzaldrin-plus.min.js
@@ -0,0 +1,2 @@
+/* fuzzaldrin-plus - v0.3.1 - @license: MIT; @author: Jean Christophe Roy; @site: https://github.com/jeancroy/fuzzaldrin-plus */
+!function(a){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=a();else if("function"==typeof define&&define.amd)define([],a);else{var b;b="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,b.fuzzaldrin=a()}}(function(){return function a(b,c,d){function e(g,h){if(!c[g]){if(!b[g]){var i="function"==typeof require&&require;if(!h&&i)return i(g,!0);if(f)return f(g,!0);var j=new Error("Cannot find module '"+g+"'");throw [...]
\ No newline at end of file
diff --git a/package.json b/package.json
index baeaea2..30166f7 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,8 @@
{
"name": "fuzzaldrin-plus",
"version": "0.3.1",
- "description": "Fuzzy filtering and string scoring - compatible with fuzzaldrin",
+ "description": "Fuzzy filtering and string similarity scoring - compatible with fuzzaldrin",
+ "license": "MIT",
"licenses": [
{
"type": "MIT",
@@ -33,12 +34,15 @@
"sublime"
],
"devDependencies": {
- "jasmine-focused": "1.x",
- "grunt-contrib-coffee": "~0.9.0",
- "grunt-cli": "~0.1.8",
+ "coffee-script": "~1.7",
+ "fuzzaldrin": "~2.1.0",
"grunt": "~0.4.1",
- "grunt-shell": "~0.2.2",
+ "grunt-browserify": "^5.0.0",
+ "grunt-cli": "~0.1.8",
"grunt-coffeelint": "0.0.6",
- "coffee-script": "~1.7"
+ "grunt-contrib-coffee": "~0.9.0",
+ "grunt-contrib-uglify": "^1.0.1",
+ "grunt-shell": "~0.2.2",
+ "jasmine-focused": "1.x"
}
}
diff --git a/spec/filter-spec.coffee b/spec/filter-spec.coffee
index 8725dbb..7778fb2 100644
--- a/spec/filter-spec.coffee
+++ b/spec/filter-spec.coffee
@@ -1,13 +1,14 @@
path = require 'path'
{filter,score} = require '../src/fuzzaldrin'
-bestMatch = (candidates, query, {debug}={}) ->
+bestMatch = (candidates, query, options = {}) ->
+ {debug} = options
if debug?
console.log("\n = Against query: #{query} = ")
console.log(" #{score(c, query)}: #{c}") for c in candidates
- filter(candidates, query, maxResults: 1)[0]
+ filter(candidates, query, options)[0]
rootPath = (segments...) ->
joinedPath = if process.platform is 'win32' then 'C:\\' else '/'
@@ -31,10 +32,10 @@ describe "filtering", ->
it "support unicode character with different length uppercase", ->
- candidates = ["Bernauer Stra\u00DFe Wall"] # Bernauer Stra�e Wall
+ candidates = ["Bernauer Stra\u00DFe Wall"] # Bernauer Straße Wall
expect(filter(candidates, 'Stra\u00DFe Wall')).toEqual candidates
- # before correction, The map �->SS , place the W out of sync and prevent a match.
- # After correction we map �->S.
+ # before correction, The map ß->SS , place the W out of sync and prevent a match.
+ # After correction we map ß->S.
describe "when the maxResults option is set", ->
it "limits the results to the result size", ->
@@ -135,7 +136,7 @@ describe "filtering", ->
candidates = [
'TODO',
- path.join('doc','README')
+ path.join('doc', 'README')
]
expect(bestMatch(candidates, 'd')).toBe candidates[1]
@@ -148,7 +149,6 @@ describe "filtering", ->
expect(bestMatch(candidates, 'es')).toBe candidates[0]
-
#---------------------------------------------------
#
# Consecutive letters
@@ -297,7 +297,7 @@ describe "filtering", ->
expect(bestMatch(['sub_zero', 'sub-zero', 'sub zero'], 'sz')).toBe 'sub_zero'
- it "weighs CamelCase matches higher", ->
+ it "weighs acronym matches higher than middle of word letter", ->
candidates = [
'FilterFactors.html',
@@ -430,8 +430,32 @@ describe "filtering", ->
expect(bestMatch(candidates, 'CCCa')).toBe candidates[0]
expect(bestMatch(candidates, 'ccca')).toBe candidates[1]
+ it "prefers acronym matches that correspond to the full candidate acronym", ->
+ candidates = [
+ 'JaVaScript',
+ 'JavaScript'
+ ]
+ # <js vs JS> scores better than <js vs JVS>
+ expect(bestMatch(candidates, 'js')).toBe candidates[1]
+ candidates = [
+ 'JSON',
+ 'J.S.O.N.',
+ 'JavaScript'
+ ]
+
+ # here 1:1 match outdo shorter start-of-word
+ expect(bestMatch(candidates, 'js')).toBe candidates[2]
+
+ candidates = [
+ 'CSON',
+ 'C.S.O.N.',
+ 'CoffeeScript'
+ ]
+
+ # here 1:1 match outdo shorter start-of-word
+ expect(bestMatch(candidates, 'cs')).toBe candidates[2]
#---------------------------------------------------
@@ -476,13 +500,24 @@ describe "filtering", ->
it "prefers matches that are together in the basename (even if basename is longer)", ->
candidates = [
- path.join('tests','buyers','orders_e2e.js'),
- path.join('tests','buyers','users-addresses_e2e.js')
+ path.join('tests', 'buyers', 'orders_e2e.js'),
+ path.join('tests', 'buyers', 'users-addresses_e2e.js')
]
expect(bestMatch(candidates, 'us_e2')).toBe candidates[1]
+ candidates = [
+ path.join('app', 'controllers', 'match_controller.rb'),
+ path.join('app', 'controllers', 'application_controller.rb')
+ ]
+
+ expect(bestMatch(candidates, 'appcontr')).toBe candidates[1]
+ expect(bestMatch(candidates, 'appcontro')).toBe candidates[1]
+ #expect(bestMatch(candidates, 'appcontrol', debug:true)).toBe candidates[1] # TODO support this case ?
+ #expect(bestMatch(candidates, 'appcontroll', debug:true)).toBe candidates[1] #Also look at issue #6
+
+
it "allows to select using folder name", ->
candidates = [
@@ -529,6 +564,31 @@ describe "filtering", ->
expect(bestMatch(candidates, 'core')).toBe candidates[1]
expect(bestMatch(candidates, 'foo')).toBe candidates[0]
+ it "prefers file of the specified extension when useExtensionBonus is true ", ->
+
+ candidates = [
+ path.join('meas_astrom', 'include', 'Isst', 'meas', 'astrom', 'matchOptimisticB.h')
+ path.join('IsstDoxygen', 'html', 'match_optimistic_b_8cc.html')
+ ]
+
+ expect(bestMatch(candidates, 'mob.h', {useExtensionBonus: true})).toBe candidates[0]
+
+ candidates = [
+ path.join('matchOptimisticB.htaccess')
+ path.join('matchOptimisticB_main.html')
+ ]
+
+ expect(bestMatch(candidates, 'mob.ht', {useExtensionBonus: true})).toBe candidates[1]
+
+ it "support file with multiple extension", ->
+ candidates = [
+ path.join('something-foobar.class')
+ path.join('something.class.php')
+ ]
+
+ expect(bestMatch(candidates, 'some.cl', {useExtensionBonus: true})).toBe candidates[1]
+
+
it "ignores trailing slashes", ->
candidates = [
@@ -539,7 +599,7 @@ describe "filtering", ->
expect(bestMatch(candidates, 'br')).toBe candidates[1]
- it "allows candidate to be all slashes", ->
+ it "allows candidates to be all slashes", ->
candidates = [path.sep, path.sep + path.sep + path.sep]
expect(filter(candidates, 'bar')).toEqual []
@@ -556,36 +616,59 @@ describe "filtering", ->
expect(bestMatch(candidates, 'project file')).toBe candidates[0]
expect(bestMatch(candidates, path.join('project', 'file'))).toBe candidates[1]
+ it "prefers overall better match to shorter end-of-path length", ->
+
+ candidates = [
+
+ path.join('CommonControl', 'Controls', 'Shared')
+ path.join('CommonControl', 'Controls', 'Shared', 'Mouse')
+ path.join('CommonControl', 'Controls', 'Shared', 'Keyboard')
+ path.join('CommonControl', 'Controls', 'Shared', 'Keyboard', 'cc.js')
+
+ ]
+
+ expect(bestMatch(candidates, path.join('CC','Controls','Shared'))).toBe candidates[0]
+ expect(bestMatch(candidates, 'CC Controls Shared')).toBe candidates[0]
+
+
+ expect(bestMatch(candidates, 'CCCShared')).toBe candidates[0]
+ expect(bestMatch(candidates, 'ccc shared')).toBe candidates[0]
+ expect(bestMatch(candidates, 'cc c shared')).toBe candidates[0]
+
+ expect(bestMatch(candidates, path.join('ccc','shared'))).toBe candidates[0]
+ expect(bestMatch(candidates, path.join('cc','c','shared'))).toBe candidates[0]
+
+
describe "when the entries are of differing directory depths", ->
it "prefers shallow path", ->
- candidate = [
+ candidates = [
path.join('b', 'z', 'file'),
path.join('b_z', 'file')
]
- expect(bestMatch(candidate, "file")).toBe candidate[1]
- expect(bestMatch(candidate, "fle")).toBe candidate[1]
+ expect(bestMatch(candidates, "file")).toBe candidates[1]
+ expect(bestMatch(candidates, "fle")).toBe candidates[1]
- candidate = [
+ candidates = [
path.join('foo', 'bar', 'baz', 'file'),
path.join('foo', 'bar_baz', 'file')
]
- expect(bestMatch(candidate, "file")).toBe candidate[1]
- expect(bestMatch(candidate, "fle")).toBe candidate[1]
+ expect(bestMatch(candidates, "file")).toBe candidates[1]
+ expect(bestMatch(candidates, "fle")).toBe candidates[1]
- candidate = [
+ candidates = [
path.join('A Long User Full-Name', 'My Documents', 'file'),
path.join('bin', 'lib', 'src', 'test', 'spec', 'file')
]
- expect(bestMatch(candidate, "file")).toBe candidate[0]
+ expect(bestMatch(candidates, "file")).toBe candidates[0]
- # We have plenty of report on how this or that should win because file is a better basename match
- # But we have no report of searching too deep, because of that folder-depth penalty is pretty weak.
+ # We have plenty of report on how this or that should win because file is a better basename match
+ # But we have no report of searching too deep, because of that folder-depth penalty is pretty weak.
it "allows better basename match to overcome slightly deeper directory / longer overall path", ->
@@ -689,19 +772,19 @@ describe "filtering", ->
it "allows to match path using either backward slash, forward slash, space or colon", ->
- candidate = [
+ candidates = [
path.join('foo', 'bar'),
path.join('model', 'user'),
]
- expect(bestMatch(candidate, "model user")).toBe candidate[1]
- expect(bestMatch(candidate, "model/user")).toBe candidate[1]
- expect(bestMatch(candidate, "model\\user")).toBe candidate[1]
- expect(bestMatch(candidate, "model::user")).toBe candidate[1]
+ expect(bestMatch(candidates, "model user")).toBe candidates[1]
+ expect(bestMatch(candidates, "model/user")).toBe candidates[1]
+ expect(bestMatch(candidates, "model\\user")).toBe candidates[1]
+ expect(bestMatch(candidates, "model::user")).toBe candidates[1]
it "prefer matches where the optional character is present", ->
- candidate = [
+ candidates = [
'ModelUser',
'model user',
'model/user',
@@ -711,12 +794,12 @@ describe "filtering", ->
'model-user',
]
- expect(bestMatch(candidate, "mdl user")).toBe candidate[1]
- expect(bestMatch(candidate, "mdl/user")).toBe candidate[2]
- expect(bestMatch(candidate, "mdl\\user")).toBe candidate[3]
- expect(bestMatch(candidate, "mdl::user")).toBe candidate[4]
- expect(bestMatch(candidate, "mdl_user")).toBe candidate[5]
- expect(bestMatch(candidate, "mdl-user")).toBe candidate[6]
+ expect(bestMatch(candidates, "mdl user")).toBe candidates[1]
+ expect(bestMatch(candidates, "mdl/user")).toBe candidates[2]
+ expect(bestMatch(candidates, "mdl\\user")).toBe candidates[3]
+ expect(bestMatch(candidates, "mdl::user")).toBe candidates[4]
+ expect(bestMatch(candidates, "mdl_user")).toBe candidates[5]
+ expect(bestMatch(candidates, "mdl-user")).toBe candidates[6]
it "weighs basename matches higher (space don't have a strict preference for slash)", ->
@@ -732,43 +815,47 @@ describe "filtering", ->
# Without support for optional character, the basename bonus
# would not be able to find "model" inside "user.rb" so the bonus would be 0
- candidate = [
+ candidates = [
path.join('www', 'lib', 'models', 'user.rb'),
path.join('migrate', 'moderator_column_users.rb')
]
- expect(bestMatch(candidate, "model user")).toBe candidate[0]
- expect(bestMatch(candidate, "modeluser")).toBe candidate[0]
- expect(bestMatch(candidate, path.join("model", "user"))).toBe candidate[0]
+ expect(bestMatch(candidates, "model user")).toBe candidates[0]
+ expect(bestMatch(candidates, "modeluser")).toBe candidates[0]
+ expect(bestMatch(candidates, path.join("model", "user"))).toBe candidates[0]
- candidate = [
+ candidates = [
path.join('destroy_discard_pool.png'),
path.join('resources', 'src', 'app_controller.coffee')
]
- expect(bestMatch(candidate, "src app")).toBe candidate[1]
- expect(bestMatch(candidate, path.join("src", "app"))).toBe candidate[1]
+ expect(bestMatch(candidates, "src app")).toBe candidates[1]
+ expect(bestMatch(candidates, path.join("src", "app"))).toBe candidates[1]
- candidate = [
+ candidates = [
path.join('template', 'emails-dialogs.handlebars'),
path.join('emails', 'handlers.py')
]
- expect(bestMatch(candidate, "email handlers")).toBe candidate[1]
- expect(bestMatch(candidate, path.join("email", "handlers"))).toBe candidate[1]
+ expect(bestMatch(candidates, "email handlers")).toBe candidates[1]
+ expect(bestMatch(candidates, path.join("email", "handlers"))).toBe candidates[1]
it "allows to select between full query and basename using path.sep", ->
- candidate = [
+ candidates = [
path.join('models', 'user.rb'),
path.join('migrate', 'model_users.rb')
]
- expect(bestMatch(candidate, "modeluser")).toBe candidate[1]
- expect(bestMatch(candidate, "model user")).toBe candidate[1]
- expect(bestMatch(candidate, path.join("model","user"))).toBe candidate[0]
+ expect(bestMatch(candidates, "modeluser")).toBe candidates[1]
+ expect(bestMatch(candidates, "model user")).toBe candidates[1]
+ expect(bestMatch(candidates, path.join("model", "user"))).toBe candidates[0]
+ describe "when query is made only of optional characters", ->
+ it "only return results having at least one specified optional character", ->
+ candidates = ["bla", "_test", " test"]
+ expect(filter(candidates, '_')).toEqual ['_test']
#---------------------------------------------------
@@ -805,13 +892,13 @@ describe "filtering", ->
expect(result[3]).toBe candidates[1]
expect(result[4]).toBe candidates[0]
- # for this one, complete word "Install" should win against:
- #
- # - case-sensitive end-of-word match "Uninstall",
- # - start of word match "Installed",
- # - double acronym match "in S t A ll" -> "Select All"
- #
- # also "Install" by itself should win against "Install" in a sentence
+ # for this one, complete word "Install" should win against:
+ #
+ # - case-sensitive end-of-word match "Uninstall",
+ # - start of word match "Installed",
+ # - double acronym match "in S t A ll" -> "Select All"
+ #
+ # also "Install" by itself should win against "Install" in a sentence
it "weighs substring higher than individual characters", ->
@@ -826,6 +913,6 @@ describe "filtering", ->
expect(bestMatch(candidates, 'git push')).toBe candidates[2]
expect(bestMatch(candidates, 'gpush')).toBe candidates[2]
- # Here "Plus Stage Hunk" accidentally match acronym on PuSH.
- # Using two words disable exactMatch bonus, we have to rely on consecutive match
+# Here "Plus Stage Hunk" accidentally match acronym on PuSH.
+# Using two words disable exactMatch bonus, we have to rely on consecutive match
diff --git a/spec/legacy-filter-spec.coffee b/spec/legacy-filter-spec.coffee
deleted file mode 100644
index dfd5ccf..0000000
--- a/spec/legacy-filter-spec.coffee
+++ /dev/null
@@ -1,142 +0,0 @@
-path = require 'path'
-fuzzaldrin = require '../src/fuzzaldrin'
-
-filter = (candidates, query) ->
- fuzzaldrin.filter(candidates, query, {legacy:true})
-
-bestMatch = (candidates, query) ->
- fuzzaldrin.filter(candidates, query, { maxResults: 1, legacy:true})[0]
-
-rootPath = (segments...) ->
- joinedPath = if process.platform is 'win32' then 'C:\\' else '/'
- for segment in segments
- if segment is path.sep
- joinedPath += segment
- else
- joinedPath = path.join(joinedPath, segment)
- joinedPath
-
-describe "filtering", ->
- it "returns an array of the most accurate results", ->
- candidates = ['Gruntfile','filter', 'bile', null, '', undefined]
- expect(filter(candidates, 'file')).toEqual ['filter', 'Gruntfile']
-
- describe "when the maxResults option is set", ->
- it "limits the results to the result size", ->
- candidates = ['Gruntfile', 'filter', 'bile']
- expect(bestMatch(candidates, 'file')).toBe 'filter'
-
- describe "when the entries contains slashes", ->
- it "weighs basename matches higher", ->
- candidates = [
- rootPath('bar', 'foo')
- rootPath('foo', 'bar')
- ]
- expect(bestMatch(candidates, 'bar')).toBe candidates[1]
-
- candidates = [
- rootPath('bar', 'foo')
- rootPath('foo', 'bar', path.sep, path.sep, path.sep, path.sep, path.sep)
- ]
- expect(bestMatch(candidates, 'bar')).toBe candidates[1]
-
- candidates = [
- rootPath('bar', 'foo')
- rootPath('foo', 'bar')
- 'bar'
- ]
- expect(bestMatch(candidates, 'bar')).toEqual candidates[2]
-
- candidates = [
- rootPath('bar', 'foo')
- rootPath('foo', 'bar')
- rootPath('bar')
- ]
- expect(bestMatch(candidates, 'bar')).toBe candidates[2]
-
- candidates = [
- rootPath('bar', 'foo')
- "bar#{path.sep}#{path.sep}#{path.sep}#{path.sep}#{path.sep}#{path.sep}"
- ]
- expect(bestMatch(candidates, 'bar')).toBe candidates[1]
-
- expect(bestMatch([path.join('f', 'o', '1_a_z'), path.join('f', 'o', 'a_z')], 'az')).toBe path.join('f', 'o', 'a_z')
- expect(bestMatch([path.join('f', '1_a_z'), path.join('f', 'o', 'a_z')], 'az')).toBe path.join('f', 'o', 'a_z')
-
- describe "when the candidate is all slashes", ->
- it "does not throw an exception", ->
- candidates = [path.sep]
- expect(filter(candidates, 'bar', maxResults: 1)).toEqual []
-
- describe "when the entries contains spaces", ->
- it "treats spaces as slashes", ->
- candidates = [
- rootPath('bar', 'foo')
- rootPath('foo', 'bar')
- ]
- expect(bestMatch(candidates, 'br f')).toBe candidates[0]
-
- it "weighs basename matches higher", ->
- candidates = [
- rootPath('bar', 'foo')
- rootPath('foo', 'bar foo')
- ]
- expect(bestMatch(candidates, 'br f')).toBe candidates[1]
-
- candidates = [
- rootPath('barfoo', 'foo')
- rootPath('foo', 'barfoo')
- ]
- expect(bestMatch(candidates, 'br f')).toBe candidates[1]
-
- candidates = [
- path.join('lib', 'exportable.rb')
- path.join('app', 'models', 'table.rb')
- ]
- expect(bestMatch(candidates, 'table')).toBe candidates[1]
-
- describe "when the entries contains mixed case", ->
- it "weighs exact case matches higher", ->
- candidates = ['statusurl', 'StatusUrl']
- expect(bestMatch(candidates, 'Status')).toBe 'StatusUrl'
- expect(bestMatch(candidates, 'SU')).toBe 'StatusUrl'
- expect(bestMatch(candidates, 'status')).toBe 'statusurl'
- expect(bestMatch(candidates, 'su')).toBe 'statusurl'
- expect(bestMatch(candidates, 'statusurl')).toBe 'statusurl'
- expect(bestMatch(candidates, 'StatusUrl')).toBe 'StatusUrl'
-
- it "weighs abbreviation matches after spaces, underscores, and dashes the same", ->
- expect(bestMatch(['sub-zero', 'sub zero', 'sub_zero'], 'sz')).toBe 'sub-zero'
- expect(bestMatch(['sub zero', 'sub_zero', 'sub-zero'], 'sz')).toBe 'sub zero'
- expect(bestMatch(['sub_zero', 'sub-zero', 'sub zero'], 'sz')).toBe 'sub_zero'
-
- it "weighs matches at the start of the string or base name higher", ->
- expect(bestMatch(['a_b_c', 'a_b'], 'ab')).toBe 'a_b'
- expect(bestMatch(['z_a_b', 'a_b'], 'ab')).toBe 'a_b'
- expect(bestMatch(['a_b_c', 'c_a_b'], 'ab')).toBe 'a_b_c'
-
- describe "when the entries are of differing directory depths", ->
- it "places exact matches first, even if they're deeper", ->
- candidates = [
- path.join('app', 'models', 'automotive', 'car.rb')
- path.join('spec', 'factories', 'cars.rb')
- ]
- expect(bestMatch(candidates, 'car.rb')).toBe candidates[0]
-
- candidates = [
- path.join('app', 'models', 'automotive', 'car.rb')
- 'car.rb'
- ]
- expect(bestMatch(candidates, 'car.rb')).toBe candidates[1]
-
- candidates = [
- 'car.rb',
- path.join('app', 'models', 'automotive', 'car.rb')
- ]
- expect(bestMatch(candidates, 'car.rb')).toBe candidates[0]
-
- candidates = [
- path.join('app', 'models', 'cars', 'car.rb')
- path.join('spec', 'cars.rb')
- ]
- expect(bestMatch(candidates, 'car.rb')).toBe candidates[0]
\ No newline at end of file
diff --git a/src/filter.coffee b/src/filter.coffee
index 2f89819..8bc4961 100644
--- a/src/filter.coffee
+++ b/src/filter.coffee
@@ -1,44 +1,35 @@
scorer = require './scorer'
-legacy_scorer = require './legacy'
+pathScorer = require './pathScorer'
+Query = require './query'
pluckCandidates = (a) -> a.candidate
sortCandidates = (a, b) -> b.score - a.score
-PathSeparator = require('path').sep
-module.exports = (candidates, query, {key, maxResults, maxInners, allowErrors, legacy }={}) ->
+module.exports = (candidates, query, options) ->
scoredCandidates = []
- spotLeft = if maxInners? and maxInners > 0 then maxInners else candidates.length
- bAllowErrors = !!allowErrors
+ #See also option parsing on main module for default
+ {key, maxResults, maxInners, usePathScoring} = options
+ spotLeft = if maxInners? and maxInners > 0 then maxInners else candidates.length + 1
bKey = key?
- prepQuery = scorer.prepQuery(query)
-
- if(not legacy)
- for candidate in candidates
- string = if bKey then candidate[key] else candidate
- continue unless string
- score = scorer.score(string, query, prepQuery, bAllowErrors)
- if score > 0
- scoredCandidates.push({candidate, score})
- break unless --spotLeft
-
- else
- queryHasSlashes = prepQuery.depth > 0
- coreQuery = prepQuery.core
-
- for candidate in candidates
- string = if key? then candidate[key] else candidate
- continue unless string
- score = legacy_scorer.score(string, coreQuery, queryHasSlashes)
- unless queryHasSlashes
- score = legacy_scorer.basenameScore(string, coreQuery, score)
- scoredCandidates.push({candidate, score}) if score > 0
+ scoreProvider = if usePathScoring then pathScorer else scorer
+ for candidate in candidates
+ string = if bKey then candidate[key] else candidate
+ continue unless string
+ score = scoreProvider.score(string, query, options)
+ if score > 0
+ scoredCandidates.push({candidate, score})
+ break unless --spotLeft
# Sort scores in descending order
scoredCandidates.sort(sortCandidates)
+ #Extract original candidate
candidates = scoredCandidates.map(pluckCandidates)
+ #Trim to maxResults if specified
candidates = candidates[0...maxResults] if maxResults?
+
+ #And return
candidates
diff --git a/src/fuzzaldrin.coffee b/src/fuzzaldrin.coffee
index 947cb44..93cad5a 100644
--- a/src/fuzzaldrin.coffee
+++ b/src/fuzzaldrin.coffee
@@ -1,77 +1,63 @@
-scorer = require './scorer'
-legacy_scorer = require './legacy'
filter = require './filter'
matcher = require './matcher'
+scorer = require './scorer'
+pathScorer = require './pathScorer'
+Query = require './query'
+
+preparedQueryCache = null
+defaultPathSeparator = if process and (process.platform is "win32") then '\\' else '/'
-PathSeparator = require('path').sep
-prepQueryCache = null
module.exports =
- filter: (candidates, query, options) ->
+ filter: (candidates, query, options = {}) ->
return [] unless query?.length and candidates?.length
+ options = parseOptions(options, query)
filter(candidates, query, options)
- prepQuery: (query) ->
- scorer.prepQuery(query)
-
-#
-# While the API is backward compatible,
-# the following pattern is recommended for speed.
-#
-# query = "..."
-# prepared = fuzzaldrin.prepQuery(query)
-# for candidate in candidates
-# score = fuzzaldrin.score(candidate, query, prepared)
-#
-# --
-# Alternatively we provide caching of prepQuery to ease direct swap of one library to another.
-#
-
- score: (string, query, prepQuery, {allowErrors, legacy}={}) ->
+ score: (string, query, options = {}) ->
return 0 unless string?.length and query?.length
+ options = parseOptions(options, query)
+ if options.usePathScoring
+ return pathScorer.score(string, query, options)
+ else return scorer.score(string, query, options)
- # if prepQuery is given -> use it, else if prepQueryCache match the same query -> use cache, else -> compute & cache
- prepQuery ?= if prepQueryCache and prepQueryCache.query is query then prepQueryCache else (prepQueryCache = scorer.prepQuery(query))
-
- if not legacy
- score = scorer.score(string, query, prepQuery, !!allowErrors)
- else
- queryHasSlashes = prepQuery.depth > 0
- coreQuery = prepQuery.core
- score = legacy_scorer.score(string, coreQuery, queryHasSlashes)
- unless queryHasSlashes
- score = legacy_scorer.basenameScore(string, coreQuery, score)
-
- score
-
- match: (string, query, prepQuery, {allowErrors}={}) ->
+ match: (string, query, options = {}) ->
return [] unless string
return [] unless query
return [0...string.length] if string is query
+ options = parseOptions(options, query)
+ return matcher.match(string, query, options)
+
+ wrap: (string, query, options = {}) ->
+ return [] unless string
+ return [] unless query
+ options = parseOptions(options, query)
+ return matcher.wrap(string, query, options)
+
+ prepareQuery: (query, options = {}) ->
+ options = parseOptions(options, query)
+ return options.preparedQuery
- # if prepQuery is given -> use it, else if prepQueryCache match the same query -> use cache, else -> compute & cache
- prepQuery ?= if prepQueryCache and prepQueryCache.query is query then prepQueryCache else (prepQueryCache = scorer.prepQuery(query))
+#Setup default values
+parseOptions = (options, query) ->
- return [] unless allowErrors or scorer.isMatch(string, prepQuery.core_lw, prepQuery.core_up)
- string_lw = string.toLowerCase()
- query_lw = prepQuery.query_lw
+ options.allowErrors ?= false
+ options.usePathScoring ?= true
+ options.useExtensionBonus ?= false
+ options.pathSeparator ?= defaultPathSeparator
+ options.optCharRegEx ?= null
+ options.wrap ?= null
- # Full path results
- matches = matcher.match(string, string_lw, prepQuery)
+ options.preparedQuery ?=
+ if preparedQueryCache and preparedQueryCache.query is query
+ then preparedQueryCache
+ else (preparedQueryCache = new Query(query, options))
- #if there is no matches on the full path, there should not be any on the base path either.
- return matches if matches.length is 0
+ return options
- # Is there a base path ?
- if(string.indexOf(PathSeparator) > -1)
- # Base path results
- baseMatches = matcher.basenameMatch(string, string_lw, prepQuery)
- # Combine the results, removing duplicate indexes
- matches = matcher.mergeMatches(matches, baseMatches)
- matches
diff --git a/src/legacy.coffee b/src/legacy.coffee
deleted file mode 100644
index 43160c4..0000000
--- a/src/legacy.coffee
+++ /dev/null
@@ -1,118 +0,0 @@
-# Original ported from:
-#
-# string_score.js: String Scoring Algorithm 0.1.10
-#
-# http://joshaven.com/string_score
-# https://github.com/joshaven/string_score
-#
-# Copyright (C) 2009-2011 Joshaven Potter <yourtech at gmail.com>
-# Special thanks to all of the contributors listed here https://github.com/joshaven/string_score
-# MIT license: http://www.opensource.org/licenses/mit-license.php
-#
-# Date: Tue Mar 1 2011
-
-PathSeparator = require('path').sep
-
-exports.basenameScore = (string, query, score) ->
- index = string.length - 1
- index-- while string[index] is PathSeparator # Skip trailing slashes
- slashCount = 0
- lastCharacter = index
- base = null
- while index >= 0
- if string[index] is PathSeparator
- slashCount++
- base ?= string.substring(index + 1, lastCharacter + 1)
- else if index is 0
- if lastCharacter < string.length - 1
- base ?= string.substring(0, lastCharacter + 1)
- else
- base ?= string
- index--
-
- # Basename matches count for more.
- if base is string
- score *= 2
- else if base
- score += exports.score(base, query)
-
- # Shallow files are scored higher
- segmentCount = slashCount + 1
- depth = Math.max(1, 10 - segmentCount)
- score *= depth * 0.01
- score
-
-exports.score = (string, query) ->
- return 1 if string is query
-
- # Return a perfect score if the file name itself matches the query.
- return 1 if queryIsLastPathSegment(string, query)
-
- totalCharacterScore = 0
- queryLength = query.length
- stringLength = string.length
-
- indexInQuery = 0
- indexInString = 0
-
- while indexInQuery < queryLength
- character = query[indexInQuery++]
- lowerCaseIndex = string.indexOf(character.toLowerCase())
- upperCaseIndex = string.indexOf(character.toUpperCase())
- minIndex = Math.min(lowerCaseIndex, upperCaseIndex)
- minIndex = Math.max(lowerCaseIndex, upperCaseIndex) if minIndex is -1
- indexInString = minIndex
- return 0 if indexInString is -1
-
- characterScore = 0.1
-
- # Same case bonus.
- characterScore += 0.1 if string[indexInString] is character
-
- if indexInString is 0 or string[indexInString - 1] is PathSeparator
- # Start of string bonus
- characterScore += 0.8
- else if string[indexInString - 1] in ['-', '_', ' ']
- # Start of word bonus
- characterScore += 0.7
-
- # Trim string to after current abbreviation match
- string = string.substring(indexInString + 1, stringLength)
-
- totalCharacterScore += characterScore
-
- queryScore = totalCharacterScore / queryLength
- ((queryScore * (queryLength / stringLength)) + queryScore) / 2
-
-queryIsLastPathSegment = (string, query) ->
- if string[string.length - query.length - 1] is PathSeparator
- string.lastIndexOf(query) is string.length - query.length
-
-
-exports.match = (string, query, stringOffset = 0) ->
- return [stringOffset...stringOffset + string.length] if string is query
-
- queryLength = query.length
- stringLength = string.length
-
- indexInQuery = 0
- indexInString = 0
-
- matches = []
-
- while indexInQuery < queryLength
- character = query[indexInQuery++]
- lowerCaseIndex = string.indexOf(character.toLowerCase())
- upperCaseIndex = string.indexOf(character.toUpperCase())
- minIndex = Math.min(lowerCaseIndex, upperCaseIndex)
- minIndex = Math.max(lowerCaseIndex, upperCaseIndex) if minIndex is -1
- indexInString = minIndex
- return [] if indexInString is -1
-
- matches.push(stringOffset + indexInString)
-
- # Trim string to after current abbreviation match
- stringOffset += indexInString + 1
- string = string.substring(indexInString + 1, stringLength)
-
- matches
\ No newline at end of file
diff --git a/src/matcher.coffee b/src/matcher.coffee
index bf84b18..e9f9ba1 100644
--- a/src/matcher.coffee
+++ b/src/matcher.coffee
@@ -2,34 +2,123 @@
# This file should closely follow `scorer` except that it returns an array
# of indexes instead of a score.
-PathSeparator = require('path').sep
-scorer = require './scorer'
+{isMatch, isWordStart, scoreConsecutives, scoreCharacter, scoreAcronyms} = require './scorer'
+
+#
+# Main export
+#
+# Return position of character which matches
+
+exports.match = match = (string, query, options) ->
+
+ {allowErrors, preparedQuery, pathSeparator} = options
+
+ return [] unless allowErrors or isMatch(string, preparedQuery.core_lw, preparedQuery.core_up)
+ string_lw = string.toLowerCase()
+
+ # Full path results
+ matches = computeMatch(string, string_lw, preparedQuery)
+
+ #if there is no matches on the full path, there should not be any on the base path either.
+ return matches if matches.length is 0
+
+ # Is there a base path ?
+ if(string.indexOf(pathSeparator) > -1)
+
+ # Base path results
+ baseMatches = basenameMatch(string, string_lw, preparedQuery, pathSeparator)
+
+ # Combine the results, removing duplicate indexes
+ matches = mergeMatches(matches, baseMatches)
+
+ matches
+
+
+#
+# Wrap
+#
+# Helper around match if you want a string with result wrapped by some delimiter text
+
+exports.wrap = (string, query, options) ->
+
+ if(options.wrap?)
+ {tagClass, tagOpen, tagClose} = options.wrap
+
+ tagClass ?= 'highlight'
+ tagOpen ?= '<strong class="' + tagClass + '">'
+ tagClose ?= '</strong>'
+
+ if string == query
+ return tagOpen + string + tagClose
+
+ #Run get position where a match is found
+ matchPositions = match(string, query, options)
+
+ #If no match return as is
+ if matchPositions.length == 0
+ return string
+
+ #Loop over match positions
+ output = ''
+ matchIndex = -1
+ strPos = 0
+ while ++matchIndex < matchPositions.length
+ matchPos = matchPositions[matchIndex]
+
+ # Get text before the current match position
+ if matchPos > strPos
+ output += string.substring(strPos, matchPos)
+ strPos = matchPos
+
+ # Get consecutive matches to wrap under a single tag
+ while ++matchIndex < matchPositions.length
+ if matchPositions[matchIndex] == matchPos + 1
+ matchPos++
+ else
+ matchIndex--
+ break
+
+ #Get text inside the match, including current character
+ matchPos++
+ if matchPos > strPos
+ output += tagOpen
+ output += string.substring(strPos, matchPos)
+ output += tagClose
+ strPos = matchPos
+
+ #Get string after last match
+ if(strPos < string.length - 1)
+ output += string.substring(strPos)
+
+ #return wrapped text
+ output
+
-exports.basenameMatch = (subject, subject_lw, prepQuery) ->
+basenameMatch = (subject, subject_lw, preparedQuery, pathSeparator) ->
# Skip trailing slashes
end = subject.length - 1
- end-- while subject[end] is PathSeparator
+ end-- while subject[end] is pathSeparator
# Get position of basePath of subject.
- basePos = subject.lastIndexOf(PathSeparator, end)
+ basePos = subject.lastIndexOf(pathSeparator, end)
#If no PathSeparator, no base path exist.
return [] if (basePos is -1)
# Get the number of folder in query
- depth = prepQuery.depth
+ depth = preparedQuery.depth
# Get that many folder from subject
while(depth-- > 0)
- basePos = subject.lastIndexOf(PathSeparator, basePos - 1)
+ basePos = subject.lastIndexOf(pathSeparator, basePos - 1)
return [] if (basePos is -1) #consumed whole subject ?
# Get basePath match
basePos++
end++
- exports.match(subject[basePos ... end], subject_lw[basePos... end], prepQuery, basePos)
+ computeMatch(subject[basePos ... end], subject_lw[basePos... end], preparedQuery, basePos)
#
@@ -37,7 +126,7 @@ exports.basenameMatch = (subject, subject_lw, prepQuery) ->
# (Assume sequences are sorted, matches are sorted by construction.)
#
-exports.mergeMatches = (a, b) ->
+mergeMatches = (a, b) ->
m = a.length
n = b.length
@@ -70,7 +159,7 @@ exports.mergeMatches = (a, b) ->
# Align sequence (used for fuzzaldrin.match)
# Return position of subject characters that match query.
#
-# Follow closely scorer.doScore.
+# Follow closely scorer.computeScore.
# Except at each step we record what triggered the best score.
# Then we trace back to output matched characters.
#
@@ -80,15 +169,15 @@ exports.mergeMatches = (a, b) ->
# - no hit miss limit
-exports.match = (subject, subject_lw, prepQuery, offset = 0) ->
- query = prepQuery.query
- query_lw = prepQuery.query_lw
+computeMatch = (subject, subject_lw, preparedQuery, offset = 0) ->
+ query = preparedQuery.query
+ query_lw = preparedQuery.query_lw
m = subject.length
n = query.length
#this is like the consecutive bonus, but for camelCase / snake_case initials
- acro_score = scorer.scoreAcronyms(subject, subject_lw, query, query_lw).score
+ acro_score = scoreAcronyms(subject, subject_lw, query, query_lw).score
#Init
score_row = new Array(n)
@@ -129,14 +218,14 @@ exports.match = (subject, subject_lw, prepQuery, offset = 0) ->
#Compute a tentative match
if ( query_lw[j] is si_lw )
- start = scorer.isWordStart(i, subject, subject_lw)
+ start = isWordStart(i, subject, subject_lw)
# Forward search for a sequence of consecutive char
csc_score = if csc_diag > 0 then csc_diag else
- scorer.scoreConsecutives(subject, subject_lw, query, query_lw, i, j, start)
+ scoreConsecutives(subject, subject_lw, query, query_lw, i, j, start)
# Determine bonus for matching A[i] with B[j]
- align = score_diag + scorer.scoreCharacter(i, j, start, acro_score, csc_score)
+ align = score_diag + scoreCharacter(i, j, start, acro_score, csc_score)
#Prepare next sequence & match score.
score_up = score_row[j] # Current score_up is next run score diag
diff --git a/src/pathScorer.coffee b/src/pathScorer.coffee
new file mode 100644
index 0000000..c392201
--- /dev/null
+++ b/src/pathScorer.coffee
@@ -0,0 +1,132 @@
+{isMatch, computeScore, scoreSize} = require './scorer'
+
+
+tau_depth = 13 # Directory depth at which the full path influence is halved.
+file_coeff = 1.2 # Full path is also penalized for length of basename. This adjust a scale factor for that penalty.
+
+#
+# Main export
+#
+# Manage the logic of testing if there's a match and calling the main scoring function
+# Also manage scoring a path and optional character.
+
+exports.score = (string, query, options) ->
+ {preparedQuery, allowErrors} = options
+ return 0 unless allowErrors or isMatch(string, preparedQuery.core_lw, preparedQuery.core_up)
+ string_lw = string.toLowerCase()
+ score = computeScore(string, string_lw, preparedQuery)
+ score = scorePath(string, string_lw, score, options)
+ return Math.ceil(score)
+
+
+#
+# Score adjustment for path
+#
+
+scorePath = (subject, subject_lw, fullPathScore, options) ->
+ return 0 if fullPathScore is 0
+
+ {preparedQuery, useExtensionBonus, pathSeparator} = options
+
+ # Skip trailing slashes
+ end = subject.length - 1
+ while subject[end] is pathSeparator then end--
+
+ # Get position of basePath of subject.
+ basePos = subject.lastIndexOf(pathSeparator, end)
+ fileLength = end-basePos
+
+ # Get a bonus for matching extension
+ extAdjust = 1.0
+
+ if useExtensionBonus
+ extAdjust += getExtensionScore(subject_lw, preparedQuery.ext, basePos, end, 2)
+ fullPathScore *= extAdjust
+
+ # no basePath, nothing else to compute.
+ return fullPathScore if (basePos is -1)
+
+ # Get the number of folder in query
+ depth = preparedQuery.depth
+
+ # Get that many folder from subject
+ while basePos > -1 and depth-- > 0
+ basePos = subject.lastIndexOf(pathSeparator, basePos - 1)
+
+ # Get basePath score, if BaseName is the whole string, no need to recompute
+ # We still need to apply the folder depth and filename penalty.
+ basePathScore = if (basePos is -1) then fullPathScore else
+ extAdjust * computeScore(subject.slice(basePos + 1, end + 1), subject_lw.slice(basePos + 1, end + 1), preparedQuery)
+
+ # Final score is linear interpolation between base score and full path score.
+ # For low directory depth, interpolation favor base Path then include more of full path as depth increase
+ #
+ # A penalty based on the size of the basePath is applied to fullPathScore
+ # That way, more focused basePath match can overcome longer directory path.
+
+ alpha = 0.5 * tau_depth / ( tau_depth + countDir(subject, end + 1, pathSeparator) )
+ return alpha * basePathScore + (1 - alpha) * fullPathScore * scoreSize(0, file_coeff * (fileLength))
+
+
+#
+# Count number of folder in a path.
+# (consecutive slashes count as a single directory)
+#
+
+exports.countDir = countDir = (path, end, pathSeparator) ->
+ return 0 if end < 1
+
+ count = 0
+ i = -1
+
+ #skip slash at the start so `foo/bar` and `/foo/bar` have the same depth.
+ while ++i < end and path[i] is pathSeparator
+ continue
+
+ while ++i < end
+ if (path[i] is pathSeparator)
+ count++ #record first slash, but then skip consecutive ones
+ while ++i < end and path[i] is pathSeparator
+ continue
+
+ return count
+
+#
+# Find fraction of extension that is matched by query.
+# For example mf.h prefers myFile.h to myfile.html
+# This need special handling because it give point for not having characters (the `tml` in above example)
+#
+
+exports.getExtension = getExtension = (str) ->
+ pos = str.lastIndexOf(".")
+ if pos < 0 then "" else str.substr(pos + 1)
+
+
+getExtensionScore = (candidate, ext, startPos, endPos, maxDepth) ->
+ # startPos is the position of last slash of candidate, -1 if absent.
+
+ return 0 unless ext.length
+
+ # Check that (a) extension exist, (b) it is after the start of the basename
+ pos = candidate.lastIndexOf(".", endPos)
+ return 0 unless pos > startPos # (note that startPos >= -1)
+
+ n = ext.length
+ m = endPos - pos
+
+ # n contain the smallest of both extension length, m the largest.
+ if( m < n)
+ n = m
+ m = ext.length
+
+ #place cursor after dot & count number of matching characters in extension
+ pos++
+ matched = -1
+ while ++matched < n then break if candidate[pos + matched] isnt ext[matched]
+
+ # if nothing found, try deeper for multiple extensions, with some penalty for depth
+ if matched is 0 and maxDepth > 0
+ return 0.9 * getExtensionScore(candidate, ext, startPos, pos - 2, maxDepth - 1)
+
+ # cannot divide by zero because m is the largest extension length and we return if either is 0
+ return matched / m
diff --git a/src/query.coffee b/src/query.coffee
new file mode 100644
index 0000000..600879b
--- /dev/null
+++ b/src/query.coffee
@@ -0,0 +1,69 @@
+#
+# Query object
+#
+# Allow to reuse some quantities computed from query.
+# Optional char can optionally be specified in the form of a regular expression.
+#
+
+{countDir, getExtension} = require "./pathScorer"
+
+module.exports =
+
+class Query
+ constructor: (query, {optCharRegEx, pathSeparator} = {} ) ->
+ return null unless query and query.length
+
+ @query = query
+ @query_lw = query.toLowerCase()
+ @core = coreChars(query, optCharRegEx)
+ @core_lw = @core.toLowerCase()
+ @core_up = truncatedUpperCase(@core)
+ @depth = countDir(query, query.length, pathSeparator )
+ @ext = getExtension(@query_lw)
+ @charCodes = getCharCodes(@query_lw)
+
+
+#
+# Optional chars
+# Those char improve the score if present, but will not block the match (score=0) if absent.
+
+opt_char_re = /[ _\-:\/\\]/g
+
+coreChars = (query, optCharRegEx = opt_char_re) ->
+ return query.replace(optCharRegEx, '')
+
+#
+# Truncated Upper Case:
+# --------------------
+#
+# A fundamental mechanic is that we are able to keep uppercase and lowercase variant of the strings in sync.
+# For that we assume uppercase and lowercase version of the string have the same length. Of course unicode being unicode there's exceptions.
+# See ftp://ftp.unicode.org/Public/UCD/latest/ucd/SpecialCasing.txt for the list
+#
+# "Stra�e".toUpperCase() -> "STRASSE"
+# truncatedUpperCase("Stra�e") -> "STRASE"
+# iterating over every character, getting uppercase variant and getting first char of that.
+#
+
+truncatedUpperCase = (str) ->
+ upper = ""
+ upper += char.toUpperCase()[0] for char in str
+ return upper
+
+#
+# Get character codes:
+# --------------------
+#
+# Get character codes map for a given string
+#
+
+getCharCodes = (str) ->
+ len = str.length
+ i = -1
+
+ charCodes = []
+ # create map
+ while ++i < len
+ charCodes[str.charCodeAt i] = true
+
+ return charCodes
diff --git a/src/scorer.coffee b/src/scorer.coffee
index 9b1c2bf..3839a5d 100644
--- a/src/scorer.coffee
+++ b/src/scorer.coffee
@@ -8,7 +8,6 @@
# Copyright (C) 2015 Jean Christophe Roy and contributors
# MIT License: http://opensource.org/licenses/MIT
-PathSeparator = require('path').sep
# Base point for a single character match
# This balance making patterns VS position and size penalty.
@@ -16,9 +15,7 @@ wm = 150
#Fading function
pos_bonus = 20 # The character from 0..pos_bonus receive a greater bonus for being at the start of string.
-tau_depth = 13 # Directory depth at which the full path influence is halved.
tau_size = 85 # Full path length at which the whole match score is halved.
-file_coeff = 1.2 # Full path is also penalized for length of basename. This adjust a scale factor for that penalty.
# Miss count
# When subject[i] is query[j] we register a hit.
@@ -29,14 +26,6 @@ file_coeff = 1.2 # Full path is also penalized for length of basename. This adju
# This has a direct influence on worst case scenario benchmark.
miss_coeff = 0.75 #Max number missed consecutive hit = ceil(miss_coeff*query.length) + 5
-#
-# Optional chars
-# Those char improve the score if present, but will not block the match (score=0) if absent.
-
-opt_char_re = /[ _\-:\/\\]/g
-exports.coreChars = coreChars = (query) ->
- return query.replace(opt_char_re, '')
-
#
# Main export
@@ -44,31 +33,12 @@ exports.coreChars = coreChars = (query) ->
# Manage the logic of testing if there's a match and calling the main scoring function
# Also manage scoring a path and optional character.
-exports.score = (string, query, prepQuery = new Query(query), allowErrors = false) ->
- return 0 unless allowErrors or isMatch(string, prepQuery.core_lw, prepQuery.core_up)
+exports.score = (string, query, options) ->
+ {preparedQuery, allowErrors} = options
+ return 0 unless allowErrors or isMatch(string, preparedQuery.core_lw, preparedQuery.core_up)
string_lw = string.toLowerCase()
- score = doScore(string, string_lw, prepQuery)
- return Math.ceil(basenameScore(string, string_lw, prepQuery, score))
-
-
-#
-# Query object
-#
-# Allow to reuse some quantities computed from query.
-class Query
- constructor: (query) ->
- return null unless query?.length
-
- @query = query
- @query_lw = query.toLowerCase()
- @core = coreChars(query)
- @core_lw = @core.toLowerCase()
- @core_up = truncatedUpperCase(@core)
- @depth = countDir(query, query.length)
-
-
-exports.prepQuery = (query) ->
- return new Query(query)
+ score = computeScore(string, string_lw, preparedQuery)
+ return Math.ceil(score)
#
@@ -89,13 +59,13 @@ exports.isMatch = isMatch = (subject, query_lw, query_up) ->
#foreach char of query
while ++j < n
- qj_lw = query_lw[j]
- qj_up = query_up[j]
+ qj_lw = query_lw.charCodeAt j
+ qj_up = query_up.charCodeAt j
# continue walking the subject from where we have left with previous query char
# until we have found a character that is either lowercase or uppercase query.
while ++i < m
- si = subject[i]
+ si = subject.charCodeAt i
break if si is qj_lw or si is qj_up
# if we passed the last char, query is not in subject
@@ -110,9 +80,9 @@ exports.isMatch = isMatch = (subject, query_lw, query_up) ->
# Main scoring algorithm
#
-doScore = (subject, subject_lw, prepQuery) ->
- query = prepQuery.query
- query_lw = prepQuery.query_lw
+exports.computeScore = computeScore = (subject, subject_lw, preparedQuery) ->
+ query = preparedQuery.query
+ query_lw = preparedQuery.query_lw
m = subject.length
n = query.length
@@ -157,6 +127,8 @@ doScore = (subject, subject_lw, prepQuery) ->
score_row[j] = 0
csc_row[j] = 0
+ #TODO: Check if speedup is important on average.
+ #This assume first and last char are correct. Possibly breaking some allowError score.
# Limit the search to the active region
# for example with query `abc`, subject `____a_bc_ac_c____`
@@ -171,13 +143,26 @@ doScore = (subject, subject_lw, prepQuery) ->
mm = subject_lw.lastIndexOf(query_lw[n - 1], m)
if(mm > i) then m = mm + 1
+ csc_invalid = true
+
while ++i < m #foreach char si of subject
+ si_lw = subject_lw[i]
+
+ # if si_lw is not in query
+ if not preparedQuery.charCodes[si_lw.charCodeAt 0]?
+ # reset csc_row and move to next
+ if csc_invalid isnt true
+ j = -1
+ while ++j < n
+ csc_row[j] = 0
+ csc_invalid = true
+ continue
score = 0
score_diag = 0
csc_diag = 0
- si_lw = subject_lw[i]
record_miss = true
+ csc_invalid = false
j = -1 #0..n-1
while ++j < n #foreach char qj of query
@@ -197,8 +182,8 @@ doScore = (subject, subject_lw, prepQuery) ->
start = isWordStart(i, subject, subject_lw)
# Forward search for a sequence of consecutive char
- csc_score = if csc_diag > 0 then csc_diag else scoreConsecutives(subject, subject_lw, query, query_lw, i,
- j, start)
+ csc_score = if csc_diag > 0 then csc_diag else
+ scoreConsecutives(subject, subject_lw, query, query_lw, i, j, start)
# Determine bonus for matching A[i] with B[j]
align = score_diag + scoreCharacter(i, j, start, acro_score, csc_score)
@@ -223,7 +208,8 @@ doScore = (subject, subject_lw, prepQuery) ->
csc_row[j] = csc_score
score_row[j] = score
-
+ # get hightest score so far
+ score = score_row[n - 1]
return score * sz
#
@@ -237,7 +223,7 @@ exports.isWordStart = isWordStart = (pos, subject, subject_lw) ->
return true if pos is 0 # match is FIRST char ( place a virtual token separator before first char of string)
curr_s = subject[pos]
prev_s = subject[pos - 1]
- return isSeparator(curr_s) or isSeparator(prev_s) or # match IS or FOLLOW a separator
+ return isSeparator(prev_s) or # match FOLLOW a separator
( curr_s isnt subject_lw[pos] and prev_s is subject_lw[pos - 1] ) # match is Capital in camelCase (preceded by lowercase)
@@ -245,7 +231,7 @@ exports.isWordEnd = isWordEnd = (pos, subject, subject_lw, len) ->
return true if pos is len - 1 # last char of string
curr_s = subject[pos]
next_s = subject[pos + 1]
- return isSeparator(curr_s) or isSeparator(next_s) or # match IS or IS FOLLOWED BY a separator
+ return isSeparator(next_s) or # match IS FOLLOWED BY a separator
( curr_s is subject_lw[pos] and next_s isnt subject_lw[pos + 1] ) # match is lowercase, followed by uppercase
@@ -263,7 +249,7 @@ scorePosition = (pos) ->
else
return Math.max(100 + pos_bonus - pos, 0)
-scoreSize = (n, m) ->
+exports.scoreSize = scoreSize = (n, m) ->
# Size penalty, use the difference of size (m-n)
return tau_size / ( tau_size + Math.abs(m - n))
@@ -321,7 +307,7 @@ exports.scoreCharacter = scoreCharacter = (i, j, start, acro_score, csc_score) -
# Forward search for a sequence of consecutive character.
#
-exports.scoreConsecutives = scoreConsecutives = (subject, subject_lw, query, query_lw, i, j, start) ->
+exports.scoreConsecutives = scoreConsecutives = (subject, subject_lw, query, query_lw, i, j, startOfWord) ->
m = subject.length
n = query.length
@@ -329,7 +315,6 @@ exports.scoreConsecutives = scoreConsecutives = (subject, subject_lw, query, que
nj = n - j
k = if mi < nj then mi else nj
- startPos = i #record start position
sameCase = 0
sz = 0 #sz will be one more than the last qi is sj
@@ -346,7 +331,7 @@ exports.scoreConsecutives = scoreConsecutives = (subject, subject_lw, query, que
# Acronym should be addressed with acronym context bonus instead of consecutive.
return 1 + 2 * sameCase if sz is 1
- return scorePattern(sz, n, sameCase, start, isWordEnd(i, subject, subject_lw, m))
+ return scorePattern(sz, n, sameCase, startOfWord, isWordEnd(i, subject, subject_lw, m))
#
@@ -399,7 +384,8 @@ exports.scoreAcronyms = scoreAcronyms = (subject, subject_lw, query, query_lw) -
return emptyAcronymResult unless m > 1 and n > 1
count = 0
- pos = 0
+ sepCount = 0
+ sumPos = 0
sameCase = 0
i = -1
@@ -410,118 +396,69 @@ exports.scoreAcronyms = scoreAcronyms = (subject, subject_lw, query, query_lw) -
qj_lw = query_lw[j]
- while ++i < m
+ # Separator will not score point but will continue the prefix when present.
+ # Test that the separator is in the candidate and advance cursor to that position.
+ # If no separator break the prefix
+
+ if isSeparator(qj_lw)
+ i = subject_lw.indexOf(qj_lw, i + 1)
+ if i > -1
+ sepCount++
+ continue
+ else
+ break
- #test if subject match
- # Only record match that are also start-of-word.
+ # For other characters we search for the first match where subject[i] = query[j]
+ # that also happens to be a start-of-word
+
+ while ++i < m
if qj_lw is subject_lw[i] and isWordStart(i, subject, subject_lw)
sameCase++ if ( query[j] is subject[i] )
- pos += i
+ sumPos += i
count++
break
- #all of subject is consumed, stop processing the query.
+ # All of subject is consumed, stop processing the query.
if i is m then break
- #all of query is consumed.
- #a single char is not an acronym (also prevent division by 0)
+
+ # Here, all of query is consumed (or we have reached a character not in acronym)
+ # A single character is not an acronym (also prevent division by 0)
if(count < 2)
return emptyAcronymResult
- #Acronym are scored as start of word, but not full word
- score = scorePattern(count, n, sameCase, true, false) # wordStart = true, wordEnd = false
-
- return new AcronymResult(score, pos / count, count)
-
-
-#----------------------------------------------------------------------
-
-#
-# Score adjustment for path
-#
-
-basenameScore = (subject, subject_lw, prepQuery, fullPathScore) ->
- return 0 if fullPathScore is 0
-
-
- # Skip trailing slashes
- end = subject.length - 1
- end-- while subject[end] is PathSeparator
-
- # Get position of basePath of subject.
- basePos = subject.lastIndexOf(PathSeparator, end)
+ # Acronym are scored as start-of-word
+ # Unless the acronym is a 1:1 match with candidate then it is upgraded to full-word.
+ fullWord = if count is n then isAcronymFullWord(subject, subject_lw, query, count) else false
+ score = scorePattern(count, n, sameCase, true, fullWord)
- #If no PathSeparator, no base path exist.
- return fullPathScore if (basePos is -1)
-
- # Get the number of folder in query
- depth = prepQuery.depth
-
- # Get that many folder from subject
- while(depth-- > 0)
- basePos = subject.lastIndexOf(PathSeparator, basePos - 1)
- if (basePos is -1) then return fullPathScore #consumed whole subject ?
-
- # Get basePath score
- basePos++
- end++
- basePathScore = doScore(subject[basePos...end], subject_lw[basePos...end], prepQuery)
-
- # Final score is linear interpolation between base score and full path score.
- # For low directory depth, interpolation favor base Path then include more of full path as depth increase
- #
- # A penalty based on the size of the basePath is applied to fullPathScore
- # That way, more focused basePath match can overcome longer directory path.
-
- alpha = 0.5 * tau_depth / ( tau_depth + countDir(subject, end + 1) )
- return alpha * basePathScore + (1 - alpha) * fullPathScore * scoreSize(0, file_coeff * (end - basePos))
+ return new AcronymResult(score, sumPos / count, count + sepCount)
#
-# Count number of folder in a path.
-# (consecutive slashes count as a single directory)
+# Test whether there's a 1:1 relationship between query and acronym of candidate.
+# For that to happens
+# (a) All character of query must be matched to an acronym of candidate
+# (b) All acronym of candidate must be matched to a character of query.
#
+# This method check for (b) assuming (a) has been checked before entering.
-exports.countDir = countDir = (path, end) ->
- return 0 if end < 1
-
+isAcronymFullWord = (subject, subject_lw, query, nbAcronymInQuery) ->
+ m = subject.length
+ n = query.length
count = 0
- i = -1
- #skip slash at the start so `foo/bar` and `/foo/bar` have the same depth.
- while ++i < end and path[i] is PathSeparator
- continue
-
- while ++i < end
- if (path[i] is PathSeparator)
- count++ #record first slash, but then skip consecutive ones
- while ++i < end and path[i] is PathSeparator
- continue
-
- return count
-
-#
-# Truncated Upper Case:
-# --------------------
-#
-# A fundamental mechanic is that we are able to keep uppercase and lowercase variant of the strings in sync.
-# For that we assume uppercase and lowercase version of the string have the same length
-#
-# Of course unicode being unicode there's exceptions.
-# See ftp://ftp.unicode.org/Public/UCD/latest/ucd/SpecialCasing.txt for the list
-#
-# One common example is 'LATIN SMALL LETTER SHARP S' (U+00DF)
-# "Stra�e".toUpperCase() === "STRASSE" // length goes from 6 char to 7 char
-#
-# Fortunately only uppercase is touched by the exceptions.
-#
-# truncatedUpperCase("Stra�e") returns "STRASE"
-# iterating over every character, getting uppercase variant and getting first char of that.
-#
-# This works for isMatch because we require candidate to contain at least this string.
-# Aka second S of STRASSE is still valid, simply an optional character.
+ # Heuristic:
+ # Assume one acronym every (at most) 12 character on average
+ # This filter out long paths, but then they can match on the filename.
+ if (m > 12 * n) then return false
-truncatedUpperCase = (str) ->
- upper = ""
- upper += char.toUpperCase()[0] for char in str
- return upper
\ No newline at end of file
+ i = -1
+ while ++i < m
+ #For each char of subject
+ #Test if we have an acronym, if so increase acronym count.
+ #If the acronym count is more than nbAcronymInQuery (number of non separator char in query)
+ #Then we do not have 1:1 relationship.
+ if isWordStart(i, subject, subject_lw) and ++count > nbAcronymInQuery then return false
+
+ return true
\ No newline at end of file
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-javascript/node-fuzzaldrin-plus.git
More information about the Pkg-javascript-commits
mailing list