UNPKG

4.05 kBJavaScriptView Raw
1import Levenshtein from 'levenshtein'
2import sortBy from 'lodash.sortby'
3
4/**
5 * Creates a fuzzy matcher.
6 *
7 * Usage:
8 *
9 * ```
10 * var m = new Matcher({
11 * values: 'init install update upgrade',
12 * threshold: 4
13 * });
14 *
15 * m.list('udpate') // [ { value: 'update', distance: 2 }, { value: 'upgrade', distance: 4 } ]
16 * m.get('udpate') // { value: 'update', distance: 2 }
17 * ```
18 *
19 * You can also initialize with an array or with a string directly:
20 *
21 * ```
22 * new Matcher('init install update upgrade');
23 * new Matcher(['init', 'install', 'update', 'upgrade']);
24 * ```
25 *
26 * @param options {Object} may contain:
27 *
28 * * `threshold` (number, default `2`) — maximum search distance, increase
29 * for more relaxed search
30 *
31 * * `values` (array or string, default `[]`) — an array of words to match from;
32 * if a string is given, values can be separated with comma or whitespace
33 *
34 * * `caseSensitive` (boolean, default `false`) — ignore case when matching
35 */
36var Matcher = function(options) {
37 options = options || {}
38 if (typeof options == 'string' || Array.isArray(options))
39 options = { values: options }
40 this.values = []
41 this.threshold = options.threshold || 2
42
43 // Try to initialize via `options.values`
44
45 if (Array.isArray(options.values)) this.values = options.values
46 else if (typeof options.values == 'string')
47 this.values = options.values.split(/[, ]\s*/g)
48
49 this.caseSensitive = options.caseSensitive || false
50}
51
52/**
53 * Adds values to the dictionary.
54 *
55 * Usage:
56 *
57 * ```
58 * new Matcher().add('update', 'upgrade', 'delete').get('deete')
59 * ```
60 *
61 * @returns {Matcher} this for chaining
62 */
63Matcher.prototype.add = function() {
64 ;[].push.apply(this.values, arguments)
65 return this
66}
67
68/**
69 * Chainable helper for settings `this.caseSensitive = false`.
70 *
71 * @returns {Matcher} this for chaining
72 */
73Matcher.prototype.ignoreCase = function() {
74 this.caseSensitive = false
75 return this
76}
77
78/**
79 * Chainable helper for settings `this.caseSensitive = true`.
80 *
81 * @returns {Matcher} this for chaining
82 */
83Matcher.prototype.matchCase = function() {
84 this.caseSensitive = true
85 return this
86}
87
88/**
89 * Chainable helper for settings `this.threshold`.
90 *
91 * @param num {Number} new threshold
92 * @returns {Matcher} this for chaining
93 */
94Matcher.prototype.setThreshold = function(num) {
95 this.threshold = num
96 return this
97}
98
99/**
100 * Calculate distance (how much difference) between two strings
101 *
102 * @param word1 {String} a string input
103 * @param word2 {String} the other string input
104 * @returns {Number} Levenshtein distance between word1 and word2
105 */
106Matcher.prototype.distance = function(word1, word2) {
107 return new Levenshtein(word1, word2).distance
108}
109
110/**
111 * Lists all results from dictionary which are similar to `q`.
112 *
113 * ```
114 * new Matcher({
115 * values: 'init install update upgrade',
116 * threshold: 4
117 * }).list('udpdate');
118 * // [ { value: 'update', distance: 2 }, { value: 'upgrade', distance: 4 } ]
119 * ```
120 *
121 * The results are sorted by their distance (similarity). The distance of `0` means
122 * a strict match.
123 *
124 * You can increase `threshold` for more relaxed search, or decrease it to shorten the results.
125 *
126 * @param q {String} — search string
127 * @returns {Array} — an array of objects `{ value: String, distance: Number }`
128 */
129Matcher.prototype.list = function(q) {
130 var m = this
131 q = q.trim()
132 if (!m.caseSensitive) q = q.toLowerCase()
133 var matches = this.values.reduce(function(results, word) {
134 var d = m.distance(q, m.caseSensitive ? word : word.toLowerCase())
135 if (d > m.threshold) return results
136 return results.concat({
137 value: word,
138 distance: d,
139 })
140 }, [])
141 return sortBy(matches, 'distance')
142}
143
144/**
145 * Returns a single closest match (or `null` if no values from the dictionary
146 * are similar to `q`).
147 *
148 * @param q {String} — search string
149 * @returns {String} — closest match or `null`
150 */
151Matcher.prototype.get = function(q) {
152 var closest = this.list(q)[0]
153 return closest ? closest.value : null
154}
155
156
157export default Matcher