1 | import Levenshtein from 'levenshtein'
|
2 | import 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 | */
|
36 | var 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 | */
|
63 | Matcher.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 | */
|
73 | Matcher.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 | */
|
83 | Matcher.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 | */
|
94 | Matcher.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 | */
|
106 | Matcher.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 | */
|
129 | Matcher.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 | */
|
151 | Matcher.prototype.get = function(q) {
|
152 | var closest = this.list(q)[0]
|
153 | return closest ? closest.value : null
|
154 | }
|
155 |
|
156 |
|
157 | export default Matcher
|