views-morph
Version:
Views language morpher
157 lines (144 loc) • 4.05 kB
JavaScript
import Levenshtein from 'levenshtein'
import sortBy from 'lodash.sortby'
/**
* Creates a fuzzy matcher.
*
* Usage:
*
* ```
* var m = new Matcher({
* values: 'init install update upgrade',
* threshold: 4
* });
*
* m.list('udpate') // [ { value: 'update', distance: 2 }, { value: 'upgrade', distance: 4 } ]
* m.get('udpate') // { value: 'update', distance: 2 }
* ```
*
* You can also initialize with an array or with a string directly:
*
* ```
* new Matcher('init install update upgrade');
* new Matcher(['init', 'install', 'update', 'upgrade']);
* ```
*
* @param options {Object} may contain:
*
* * `threshold` (number, default `2`) — maximum search distance, increase
* for more relaxed search
*
* * `values` (array or string, default `[]`) — an array of words to match from;
* if a string is given, values can be separated with comma or whitespace
*
* * `caseSensitive` (boolean, default `false`) — ignore case when matching
*/
var Matcher = function(options) {
options = options || {}
if (typeof options == 'string' || Array.isArray(options))
options = { values: options }
this.values = []
this.threshold = options.threshold || 2
// Try to initialize via `options.values`
if (Array.isArray(options.values)) this.values = options.values
else if (typeof options.values == 'string')
this.values = options.values.split(/[, ]\s*/g)
this.caseSensitive = options.caseSensitive || false
}
/**
* Adds values to the dictionary.
*
* Usage:
*
* ```
* new Matcher().add('update', 'upgrade', 'delete').get('deete')
* ```
*
* @returns {Matcher} this for chaining
*/
Matcher.prototype.add = function() {
;[].push.apply(this.values, arguments)
return this
}
/**
* Chainable helper for settings `this.caseSensitive = false`.
*
* @returns {Matcher} this for chaining
*/
Matcher.prototype.ignoreCase = function() {
this.caseSensitive = false
return this
}
/**
* Chainable helper for settings `this.caseSensitive = true`.
*
* @returns {Matcher} this for chaining
*/
Matcher.prototype.matchCase = function() {
this.caseSensitive = true
return this
}
/**
* Chainable helper for settings `this.threshold`.
*
* @param num {Number} new threshold
* @returns {Matcher} this for chaining
*/
Matcher.prototype.setThreshold = function(num) {
this.threshold = num
return this
}
/**
* Calculate distance (how much difference) between two strings
*
* @param word1 {String} a string input
* @param word2 {String} the other string input
* @returns {Number} Levenshtein distance between word1 and word2
*/
Matcher.prototype.distance = function(word1, word2) {
return new Levenshtein(word1, word2).distance
}
/**
* Lists all results from dictionary which are similar to `q`.
*
* ```
* new Matcher({
* values: 'init install update upgrade',
* threshold: 4
* }).list('udpdate');
* // [ { value: 'update', distance: 2 }, { value: 'upgrade', distance: 4 } ]
* ```
*
* The results are sorted by their distance (similarity). The distance of `0` means
* a strict match.
*
* You can increase `threshold` for more relaxed search, or decrease it to shorten the results.
*
* @param q {String} — search string
* @returns {Array} — an array of objects `{ value: String, distance: Number }`
*/
Matcher.prototype.list = function(q) {
var m = this
q = q.trim()
if (!m.caseSensitive) q = q.toLowerCase()
var matches = this.values.reduce(function(results, word) {
var d = m.distance(q, m.caseSensitive ? word : word.toLowerCase())
if (d > m.threshold) return results
return results.concat({
value: word,
distance: d,
})
}, [])
return sortBy(matches, 'distance')
}
/**
* Returns a single closest match (or `null` if no values from the dictionary
* are similar to `q`).
*
* @param q {String} — search string
* @returns {String} — closest match or `null`
*/
Matcher.prototype.get = function(q) {
var closest = this.list(q)[0]
return closest ? closest.value : null
}
export default Matcher