blob: 89e4666dd6c624f1e1bc1fe31ce7181d039d42f9 [file] [log] [blame]
// SPDX-FileCopyrightText: 2014 John Hawthorn
//
// SPDX-License-Identifier: MIT
window.fzy = (function() {
var SCORE_MIN = -Infinity;
var SCORE_MAX = Infinity;
var SCORE_GAP_LEADING = -0.005
var SCORE_GAP_TRAILING = -0.005
var SCORE_GAP_INNER = -0.01
var SCORE_MATCH_CONSECUTIVE = 1.0
var SCORE_MATCH_SLASH = 0.9
var SCORE_MATCH_WORD = 0.8
var SCORE_MATCH_CAPITAL = 0.7
var SCORE_MATCH_DOT = 0.6
function islower(s) {
return s.toLowerCase() === s;
}
function isupper(s) {
return s.toUpperCase() === s;
}
function precompute_bonus(haystack) {
/* Which positions are beginning of words */
var m = haystack.length;
var match_bonus = new Array(m);
var last_ch = '/';
for (var i = 0; i < m; i++) {
var ch = haystack[i];
if (last_ch === '/') {
match_bonus[i] = SCORE_MATCH_SLASH;
} else if (last_ch === '-' || last_ch === '_' || last_ch === ' ') {
match_bonus[i] = SCORE_MATCH_WORD;
} else if (last_ch === '.') {
match_bonus[i] = SCORE_MATCH_DOT;
} else if (islower(last_ch) && isupper(ch)) {
match_bonus[i] = SCORE_MATCH_CAPITAL;
} else {
match_bonus[i] = 0;
}
last_ch = ch;
}
return match_bonus;
}
function compute(needle, haystack, D, M) {
var n = needle.length;
var m = haystack.length;
var match_bonus = precompute_bonus(haystack, match_bonus);
/*
* D[][] Stores the best score for this position ending with a match.
* M[][] Stores the best possible score at this position.
*/
for (var i = 0; i < n; i++) {
D[i] = new Array(m);
M[i] = new Array(m);
var prev_score = SCORE_MIN;
var gap_score = i === n - 1 ? SCORE_GAP_TRAILING : SCORE_GAP_INNER;
for (var j = 0; j < m; j++) {
if (needle[i] === haystack[j]) {
var score = SCORE_MIN;
if (!i) {
score = (j * SCORE_GAP_LEADING) + match_bonus[j];
} else if (j) { /* i > 0 && j > 0*/
score = Math.max(
M[i - 1][j - 1] + match_bonus[j],
/* consecutive match, doesn't stack with match_bonus */
D[i - 1][j - 1] + SCORE_MATCH_CONSECUTIVE);
}
D[i][j] = score;
M[i][j] = prev_score = Math.max(score, prev_score + gap_score);
} else {
D[i][j] = SCORE_MIN;
M[i][j] = prev_score = prev_score + gap_score;
}
}
}
}
function score(needle, haystack) {
var n = needle.length;
var m = haystack.length;
if (!n || !m)
return SCORE_MIN;
if (n === m) {
/* Since this method can only be called with a haystack which
* matches needle. If the lengths of the strings are equal the
* strings themselves must also be equal (ignoring case).
*/
return SCORE_MAX;
}
if (m > 1024) {
/*
* Unreasonably large candidate: return no score
* If it is a valid match it will still be returned, it will
* just be ranked below any reasonably sized candidates
*/
return SCORE_MIN;
}
var D = new Array(n);
var M = new Array(n);
compute(needle, haystack, D, M)
return M[n - 1][m - 1];
}
function positions(needle, haystack) {
var n = needle.length;
var m = haystack.length;
var positions = new Array(n);
if (!n || !m)
return positions;
if (n === m) {
for (var i = 0; i < n; i++)
positions[i] = i;
return positions;
}
if (m > 1024) {
return positions;
}
var D = new Array(n);
var M = new Array(n);
compute(needle, haystack, D, M)
/* backtrack to find the positions of optimal matching */
var match_required = false;
for (var i = n - 1, j = m - 1; i >= 0; i--) {
for (; j >= 0; j--) {
/*
* There may be multiple paths which result in
* the optimal weight.
*
* For simplicity, we will pick the first one
* we encounter, the latest in the candidate
* string.
*/
if (D[i][j] !== SCORE_MIN &&
(match_required || D[i][j] === M[i][j])) {
/* If this score was determined using
* SCORE_MATCH_CONSECUTIVE, the
* previous character MUST be a match
*/
match_required =
i && j &&
M[i][j] === D[i - 1][j - 1] + SCORE_MATCH_CONSECUTIVE;
positions[i] = j--;
break;
}
}
}
return positions;
}
function hasMatch(needle, haystack) {
var l = needle.length
for (var i = 0, j = 0; i < l; i += 1) {
j = haystack.indexOf(needle[i], j) + 1
if (j === 0) return false
}
return true
}
function filter(needle, items, selector) {
const isCaseSensitive = needle.toLowerCase() !== needle;
const actualNeedle = isCaseSensitive ? needle : needle.toLowerCase();
const filteredItems = items.reduce((results, item) => {
const haystack = isCaseSensitive ? selector(item) : selector(item).toLowerCase();
if (hasMatch(actualNeedle, haystack))
results.push({ item, score: score(needle, haystack) })
return results;
}, [])
filteredItems.sort((a, b) => b.score - a.score)
return filteredItems;
}
return {
/* constants */
SCORE_MIN,
SCORE_MAX,
SCORE_GAP_LEADING,
SCORE_GAP_TRAILING,
SCORE_GAP_INNER,
SCORE_MATCH_CONSECUTIVE,
SCORE_MATCH_SLASH,
SCORE_MATCH_WORD,
SCORE_MATCH_CAPITAL,
SCORE_MATCH_DOT,
/* functions */
score,
positions,
hasMatch,
filter,
}
})();