mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-06-13 16:33:30 -07:00
website: swap ufuzzy for quickmatch
This commit is contained in:
@@ -11,3 +11,5 @@ nano.*
|
||||
worker.*
|
||||
*.mts
|
||||
*.cts
|
||||
*.rs
|
||||
*.md
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
LICENSE
|
||||
*.json
|
||||
webcomponent*
|
||||
README.md
|
||||
cli*
|
||||
extras/
|
||||
-215
@@ -1,215 +0,0 @@
|
||||
declare class uFuzzy {
|
||||
constructor(opts?: uFuzzy.Options);
|
||||
|
||||
/** search API composed of filter/info/sort, with a info/ranking threshold (1e3) and fast outOfOrder impl */
|
||||
search(
|
||||
haystack: string[],
|
||||
needle: string,
|
||||
/** limit how many terms will be permuted, default = 0; 5 will result in up to 5! (120) search iterations. be careful with this! */
|
||||
outOfOrder?: number,
|
||||
/** default = 1e3 */
|
||||
infoThresh?: number,
|
||||
preFiltered?: uFuzzy.HaystackIdxs | null,
|
||||
): uFuzzy.SearchResult;
|
||||
|
||||
/** initial haystack filter, can accept idxs from previous prefix/typeahead match as optimization */
|
||||
filter(
|
||||
haystack: string[],
|
||||
needle: string,
|
||||
idxs?: uFuzzy.HaystackIdxs,
|
||||
): uFuzzy.HaystackIdxs | null;
|
||||
|
||||
/** collects stats about pre-filtered matches, does additional filtering based on term boundary settings, finds highlight ranges */
|
||||
info(
|
||||
idxs: uFuzzy.HaystackIdxs,
|
||||
haystack: string[],
|
||||
needle: string,
|
||||
): uFuzzy.Info;
|
||||
|
||||
/** performs final result sorting via Array.sort(), relying on Info */
|
||||
sort(
|
||||
info: uFuzzy.Info,
|
||||
haystack: string[],
|
||||
needle: string,
|
||||
): uFuzzy.InfoIdxOrder;
|
||||
|
||||
/** utility for splitting needle into terms following defined interSplit/intraSplit opts. useful for out-of-order permutes */
|
||||
split(needle: string, keepCase?: boolean): uFuzzy.Terms;
|
||||
|
||||
/** util for creating out-of-order permutations of a needle terms array */
|
||||
static permute(arr: unknown[]): unknown[][];
|
||||
|
||||
/** util for replacing common diacritics/accents */
|
||||
static latinize<T extends string[] | string>(strings: T): T;
|
||||
|
||||
/** util for highlighting matched substr parts of a result */
|
||||
static highlight<TAccum = string, TMarkedPart = string>(
|
||||
match: string,
|
||||
ranges: number[],
|
||||
|
||||
mark?: (part: string, matched: boolean) => TMarkedPart,
|
||||
accum?: TAccum,
|
||||
append?: (accum: TAccum, part: TMarkedPart) => TAccum | undefined,
|
||||
): TAccum;
|
||||
}
|
||||
|
||||
export = uFuzzy;
|
||||
|
||||
declare namespace uFuzzy {
|
||||
/** needle's terms */
|
||||
export type Terms = string[];
|
||||
|
||||
/** subset of idxs of a haystack array */
|
||||
export type HaystackIdxs = number[];
|
||||
|
||||
/** sorted order in which info facets should be iterated */
|
||||
export type InfoIdxOrder = number[];
|
||||
|
||||
export type AbortedResult = [null, null, null];
|
||||
|
||||
export type FilteredResult = [uFuzzy.HaystackIdxs, null, null];
|
||||
|
||||
export type RankedResult = [
|
||||
uFuzzy.HaystackIdxs,
|
||||
uFuzzy.Info,
|
||||
uFuzzy.InfoIdxOrder,
|
||||
];
|
||||
|
||||
export type SearchResult = FilteredResult | RankedResult | AbortedResult;
|
||||
|
||||
/** partial RegExp */
|
||||
type PartialRegExp = string;
|
||||
|
||||
/** what should be considered acceptable term bounds */
|
||||
export const enum BoundMode {
|
||||
/** will match 'man' substr anywhere. e.g. tasmania */
|
||||
Any = 0,
|
||||
/** will match 'man' at whitespace, punct, case-change, and alpha-num boundaries. e.g. mantis, SuperMan, fooManBar, 0007man */
|
||||
Loose = 1,
|
||||
/** will match 'man' at whitespace, punct boundaries only. e.g. mega man, walk_man, man-made, foo.man.bar */
|
||||
Strict = 2,
|
||||
}
|
||||
|
||||
export const enum IntraMode {
|
||||
/** allows any number of extra char insertions within a term, but all term chars must be present for a match */
|
||||
MultiInsert = 0,
|
||||
/** allows for a single-char substitution, transposition, insertion, or deletion within terms (excluding first and last chars) */
|
||||
SingleError = 1,
|
||||
}
|
||||
|
||||
export type IntraSliceIdxs = [from: number, to: number];
|
||||
|
||||
type CompareFn = (a: string, b: string) => number;
|
||||
|
||||
export interface Options {
|
||||
// whether regexps use a /u unicode flag
|
||||
unicode?: boolean; // false
|
||||
|
||||
/** @deprecated renamed to opts.alpha */
|
||||
letters?: PartialRegExp | null; // a-z
|
||||
|
||||
// regexp character class [] of chars which should be treated as letters (case insensitive)
|
||||
alpha?: PartialRegExp | null; // a-z
|
||||
|
||||
/** term segmentation & punct/whitespace merging */
|
||||
interSplit?: PartialRegExp; // '[^A-Za-z\\d']+'
|
||||
intraSplit?: PartialRegExp | null; // '[a-z][A-Z]'
|
||||
|
||||
/** inter bounds that will be used to increase lft2/rgt2 info counters */
|
||||
interBound?: PartialRegExp | null; // '[^A-Za-z\\d]'
|
||||
/** intra bounds that will be used to increase lft1/rgt1 info counters */
|
||||
intraBound?: PartialRegExp | null; // '[A-Za-z][0-9]|[0-9][A-Za-z]|[a-z][A-Z]'
|
||||
|
||||
/** inter-term modes, during .info() can discard matches when bounds conditions are not met */
|
||||
interLft?: BoundMode; // 0
|
||||
interRgt?: BoundMode; // 0
|
||||
|
||||
/** allowance between terms */
|
||||
interChars?: PartialRegExp; // '.'
|
||||
interIns?: number; // Infinity
|
||||
|
||||
/** allowance between chars within terms */
|
||||
intraChars?: PartialRegExp; // '[a-z\\d]'
|
||||
intraIns?: number; // 0
|
||||
|
||||
/** contractions detection */
|
||||
intraContr?: PartialRegExp; // "'[a-z]{1,2}\\b"
|
||||
|
||||
/** error tolerance mode within terms. will clamp intraIns to 1 when set to SingleError */
|
||||
intraMode?: IntraMode; // 0
|
||||
|
||||
/** which part of each term should tolerate errors (when intraMode: 1) */
|
||||
intraSlice?: IntraSliceIdxs; // [1, Infinity]
|
||||
|
||||
/** max substitutions (when intraMode: 1) */
|
||||
intraSub?: 0 | 1; // 0
|
||||
/** max transpositions (when intraMode: 1) */
|
||||
intraTrn?: 0 | 1; // 0
|
||||
/** max omissions/deletions (when intraMode: 1) */
|
||||
intraDel?: 0 | 1; // 0
|
||||
|
||||
/** can dynamically adjust error tolerance rules per term in needle (when intraMode: 1) */
|
||||
intraRules?: (term: string) => {
|
||||
intraSlice?: IntraSliceIdxs;
|
||||
intraIns: 0 | 1;
|
||||
intraSub: 0 | 1;
|
||||
intraTrn: 0 | 1;
|
||||
intraDel: 0 | 1;
|
||||
};
|
||||
|
||||
/** post-filters matches during .info() based on cmp of term in needle vs partial match */
|
||||
intraFilt?: (term: string, match: string, index: number) => boolean; // should this also accept WIP info?
|
||||
|
||||
/** default: toLocaleUpperCase() */
|
||||
toUpper?: (str: string) => string;
|
||||
|
||||
/** default: toLocaleLowerCase() */
|
||||
toLower?: (str: string) => string;
|
||||
|
||||
/** final sorting cmp when all other match metrics are equal */
|
||||
compare?: CompareFn;
|
||||
|
||||
sort?: (
|
||||
info: Info,
|
||||
haystack: string[],
|
||||
needle: string,
|
||||
compare?: CompareFn,
|
||||
) => InfoIdxOrder;
|
||||
}
|
||||
|
||||
export interface Info {
|
||||
/** matched idxs from haystack */
|
||||
idx: HaystackIdxs;
|
||||
|
||||
/** match offsets */
|
||||
start: number[];
|
||||
|
||||
/** number of left BoundMode.Strict term boundaries found */
|
||||
interLft2: number[];
|
||||
/** number of right BoundMode.Strict term boundaries found */
|
||||
interRgt2: number[];
|
||||
/** number of left BoundMode.Loose term boundaries found */
|
||||
interLft1: number[];
|
||||
/** number of right BoundMode.Loose term boundaries found */
|
||||
interRgt1: number[];
|
||||
|
||||
/** total number of extra chars matched within all terms. higher = matched terms have more fuzz in them */
|
||||
intraIns: number[];
|
||||
/** total number of chars found in between matched terms. higher = terms are more sparse, have more fuzz in between them */
|
||||
interIns: number[];
|
||||
|
||||
/** total number of matched contiguous chars (substrs but not necessarily full terms) */
|
||||
chars: number[];
|
||||
|
||||
/** number of exactly-matched terms (intra = 0) where both lft and rgt landed on a BoundMode.Loose or BoundMode.Strict boundary */
|
||||
terms: number[];
|
||||
|
||||
/** number of needle terms with case-sensitive partial matches */
|
||||
cases: number[];
|
||||
|
||||
/** offset ranges within match for highlighting: [startIdx0, endIdx0, startIdx1, endIdx1,...] */
|
||||
ranges: number[][];
|
||||
}
|
||||
}
|
||||
|
||||
export as namespace uFuzzy;
|
||||
-1047
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,427 @@
|
||||
const DEFAULT_SEPARATORS = "_- ";
|
||||
const DEFAULT_TRIGRAM_BUDGET = 6;
|
||||
const DEFAULT_LIMIT = 100;
|
||||
|
||||
/**
|
||||
* Configuration for QuickMatch.
|
||||
*/
|
||||
export class QuickMatchConfig {
|
||||
/** @type {string} Characters used to split items into words */
|
||||
separators = DEFAULT_SEPARATORS;
|
||||
|
||||
/** @type {number} Maximum number of results to return */
|
||||
limit = DEFAULT_LIMIT;
|
||||
|
||||
/** @type {number} Number of trigram lookups for fuzzy matching (0-20) */
|
||||
trigramBudget = DEFAULT_TRIGRAM_BUDGET;
|
||||
|
||||
/**
|
||||
* Set maximum number of results.
|
||||
* @param {number} n
|
||||
*/
|
||||
withLimit(n) {
|
||||
this.limit = Math.max(1, n);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set trigram budget for fuzzy matching.
|
||||
* Higher values find more typos but cost more.
|
||||
* @param {number} n - Budget (0-20, default: 6)
|
||||
*/
|
||||
withTrigramBudget(n) {
|
||||
this.trigramBudget = Math.max(0, Math.min(20, n));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set word separator characters.
|
||||
* @param {string} s - Separator characters (default: '_- ')
|
||||
*/
|
||||
withSeparators(s) {
|
||||
this.separators = s;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fast fuzzy string matcher using word and trigram indexing.
|
||||
*/
|
||||
export class QuickMatch {
|
||||
/**
|
||||
* Create a new matcher.
|
||||
* @param {string[]} items - Items to index (should be lowercase)
|
||||
* @param {QuickMatchConfig} [config] - Optional configuration
|
||||
*/
|
||||
constructor(items, config = new QuickMatchConfig()) {
|
||||
this.config = config;
|
||||
this.items = items;
|
||||
/** @type {Map<string, number[]>} */
|
||||
this.wordIndex = new Map();
|
||||
/** @type {Map<string, number[]>} */
|
||||
this.trigramIndex = new Map();
|
||||
|
||||
let maxWordLength = 0;
|
||||
let maxQueryLength = 0;
|
||||
let maxWordCount = 0;
|
||||
|
||||
const { separators } = config;
|
||||
|
||||
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
|
||||
const item = items[itemIndex];
|
||||
|
||||
if (item.length > maxQueryLength) {
|
||||
maxQueryLength = item.length;
|
||||
}
|
||||
|
||||
let wordCount = 0;
|
||||
let wordStart = 0;
|
||||
|
||||
for (let i = 0; i <= item.length; i++) {
|
||||
const isEndOfWord = i === item.length || separators.includes(item[i]);
|
||||
|
||||
if (isEndOfWord && i > wordStart) {
|
||||
wordCount++;
|
||||
const word = item.slice(wordStart, i);
|
||||
|
||||
if (word.length > maxWordLength) {
|
||||
maxWordLength = word.length;
|
||||
}
|
||||
|
||||
addToIndex(this.wordIndex, word, itemIndex);
|
||||
addTrigramsToIndex(this.trigramIndex, word, itemIndex);
|
||||
|
||||
wordStart = i + 1;
|
||||
} else if (isEndOfWord) {
|
||||
wordStart = i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (wordCount > maxWordCount) {
|
||||
maxWordCount = wordCount;
|
||||
}
|
||||
}
|
||||
|
||||
this.maxWordLength = maxWordLength + 4;
|
||||
this.maxQueryLength = maxQueryLength + 6;
|
||||
this.maxWordCount = maxWordCount + 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find matching items. Returns items sorted by relevance.
|
||||
* @param {string} query - Search query
|
||||
*/
|
||||
matches(query) {
|
||||
return this.matchesWith(query, this.config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find matching items with custom config. Returns items sorted by relevance.
|
||||
* @param {string} query - Search query
|
||||
* @param {QuickMatchConfig} config - Configuration to use
|
||||
*/
|
||||
matchesWith(query, config) {
|
||||
const { limit, trigramBudget, separators } = config;
|
||||
|
||||
const normalizedQuery = normalizeQuery(query);
|
||||
|
||||
if (!normalizedQuery || normalizedQuery.length > this.maxQueryLength) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const queryWords = parseWords(
|
||||
normalizedQuery,
|
||||
separators,
|
||||
this.maxWordLength,
|
||||
);
|
||||
|
||||
if (!queryWords.length || queryWords.length > this.maxWordCount) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const knownWords = [];
|
||||
const unknownWords = [];
|
||||
|
||||
for (const word of queryWords) {
|
||||
const matchingItems = this.wordIndex.get(word);
|
||||
|
||||
if (matchingItems) {
|
||||
knownWords.push(matchingItems);
|
||||
} else if (word.length >= 3 && unknownWords.length < trigramBudget) {
|
||||
unknownWords.push(word);
|
||||
}
|
||||
}
|
||||
|
||||
const exactMatches = intersectAll(knownWords);
|
||||
const hasExactMatches = exactMatches.length > 0;
|
||||
const needsFuzzyMatching = unknownWords.length > 0 && trigramBudget > 0;
|
||||
|
||||
if (!needsFuzzyMatching) {
|
||||
if (!hasExactMatches) return [];
|
||||
return this.sortedByLength(exactMatches, limit);
|
||||
}
|
||||
|
||||
const scores = new Map();
|
||||
|
||||
if (hasExactMatches) {
|
||||
for (const index of exactMatches) {
|
||||
scores.set(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
const minItemLength = Math.max(0, normalizedQuery.length - 3);
|
||||
|
||||
const hitCount = this.scoreByTrigrams({
|
||||
unknownWords,
|
||||
budget: trigramBudget,
|
||||
scores,
|
||||
hasExactMatches,
|
||||
minItemLength,
|
||||
});
|
||||
|
||||
const minScoreToInclude = Math.max(1, Math.ceil(hitCount / 2));
|
||||
|
||||
return this.rankedResults(scores, minScoreToInclude, limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {{unknownWords: string[], budget: number, scores: Map<number, number>, hasExactMatches: boolean, minItemLength: number}} args
|
||||
*/
|
||||
scoreByTrigrams({
|
||||
unknownWords,
|
||||
budget,
|
||||
scores,
|
||||
hasExactMatches,
|
||||
minItemLength,
|
||||
}) {
|
||||
const visitedTrigrams = new Set();
|
||||
let budgetRemaining = budget;
|
||||
let hitCount = 0;
|
||||
|
||||
outer: for (let round = 0; round < budget; round++) {
|
||||
for (const word of unknownWords) {
|
||||
if (budgetRemaining <= 0) break outer;
|
||||
|
||||
const position = pickTrigramPosition(word.length, round);
|
||||
if (position < 0) continue;
|
||||
|
||||
const trigram =
|
||||
word[position] + word[position + 1] + word[position + 2];
|
||||
|
||||
if (visitedTrigrams.has(trigram)) continue;
|
||||
visitedTrigrams.add(trigram);
|
||||
|
||||
budgetRemaining--;
|
||||
|
||||
const matchingItems = this.trigramIndex.get(trigram);
|
||||
if (!matchingItems) continue;
|
||||
|
||||
hitCount++;
|
||||
|
||||
for (const itemIndex of matchingItems) {
|
||||
if (hasExactMatches) {
|
||||
const currentScore = scores.get(itemIndex);
|
||||
if (currentScore !== undefined) {
|
||||
scores.set(itemIndex, currentScore + 1);
|
||||
}
|
||||
} else if (this.items[itemIndex].length >= minItemLength) {
|
||||
scores.set(itemIndex, (scores.get(itemIndex) || 0) + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return hitCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {number[]} indices
|
||||
* @param {number} limit
|
||||
*/
|
||||
sortedByLength(indices, limit) {
|
||||
const { items } = this;
|
||||
indices.sort((a, b) => items[a].length - items[b].length);
|
||||
if (indices.length > limit) indices.length = limit;
|
||||
return indices.map((i) => items[i]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {Map<number, number>} scores
|
||||
* @param {number} minScore
|
||||
* @param {number} limit
|
||||
*/
|
||||
rankedResults(scores, minScore, limit) {
|
||||
const { items } = this;
|
||||
const results = [];
|
||||
|
||||
for (const [index, score] of scores) {
|
||||
if (score >= minScore) {
|
||||
results.push({ index, score });
|
||||
}
|
||||
}
|
||||
|
||||
results.sort((a, b) => {
|
||||
if (b.score !== a.score) return b.score - a.score;
|
||||
return items[a.index].length - items[b.index].length;
|
||||
});
|
||||
|
||||
if (results.length > limit) results.length = limit;
|
||||
|
||||
return results.map((r) => items[r.index]);
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {string} query */
|
||||
function normalizeQuery(query) {
|
||||
let result = "";
|
||||
let start = 0;
|
||||
let end = query.length;
|
||||
|
||||
while (start < end && query.charCodeAt(start) <= 32) start++;
|
||||
while (end > start && query.charCodeAt(end - 1) <= 32) end--;
|
||||
|
||||
for (let i = start; i < end; i++) {
|
||||
const code = query.charCodeAt(i);
|
||||
if (code >= 128) continue;
|
||||
result +=
|
||||
code >= 65 && code <= 90 ? String.fromCharCode(code + 32) : query[i];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
* @param {string} separators
|
||||
* @param {number} maxLength
|
||||
*/
|
||||
function parseWords(text, separators, maxLength) {
|
||||
/** @type {string[]} */
|
||||
const words = [];
|
||||
let start = 0;
|
||||
|
||||
for (let i = 0; i <= text.length; i++) {
|
||||
const isEnd = i === text.length || separators.includes(text[i]);
|
||||
|
||||
if (isEnd && i > start) {
|
||||
const word = text.slice(start, i);
|
||||
if (word.length <= maxLength && !words.includes(word)) {
|
||||
words.push(word);
|
||||
}
|
||||
start = i + 1;
|
||||
} else if (isEnd) {
|
||||
start = i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
return words;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Map<string, number[]>} index
|
||||
* @param {string} key
|
||||
* @param {number} value
|
||||
*/
|
||||
function addToIndex(index, key, value) {
|
||||
const existing = index.get(key);
|
||||
if (existing) {
|
||||
existing.push(value);
|
||||
} else {
|
||||
index.set(key, [value]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Map<string, number[]>} index
|
||||
* @param {string} word
|
||||
* @param {number} itemIndex
|
||||
*/
|
||||
function addTrigramsToIndex(index, word, itemIndex) {
|
||||
if (word.length < 3) return;
|
||||
|
||||
for (let i = 0; i <= word.length - 3; i++) {
|
||||
const trigram = word[i] + word[i + 1] + word[i + 2];
|
||||
const existing = index.get(trigram);
|
||||
|
||||
if (!existing) {
|
||||
index.set(trigram, [itemIndex]);
|
||||
} else if (existing[existing.length - 1] !== itemIndex) {
|
||||
existing.push(itemIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {number[][]} arrays */
|
||||
function intersectAll(arrays) {
|
||||
if (!arrays.length) return [];
|
||||
|
||||
let smallestIndex = 0;
|
||||
for (let i = 1; i < arrays.length; i++) {
|
||||
if (arrays[i].length < arrays[smallestIndex].length) {
|
||||
smallestIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
const result = arrays[smallestIndex].slice();
|
||||
|
||||
for (let i = 0; i < arrays.length && result.length > 0; i++) {
|
||||
if (i === smallestIndex) continue;
|
||||
|
||||
let writeIndex = 0;
|
||||
for (let j = 0; j < result.length; j++) {
|
||||
if (binarySearch(arrays[i], result[j])) {
|
||||
result[writeIndex++] = result[j];
|
||||
}
|
||||
}
|
||||
result.length = writeIndex;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number[]} sortedArray
|
||||
* @param {number} value
|
||||
*/
|
||||
function binarySearch(sortedArray, value) {
|
||||
let low = 0;
|
||||
let high = sortedArray.length - 1;
|
||||
|
||||
while (low <= high) {
|
||||
const mid = (low + high) >> 1;
|
||||
const midValue = sortedArray[mid];
|
||||
|
||||
if (midValue === value) return true;
|
||||
if (midValue < value) low = mid + 1;
|
||||
else high = mid - 1;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} wordLength
|
||||
* @param {number} round
|
||||
*/
|
||||
function pickTrigramPosition(wordLength, round) {
|
||||
const maxPosition = wordLength - 3;
|
||||
if (maxPosition < 0) return -1;
|
||||
|
||||
if (round === 0) return 0;
|
||||
if (round === 1 && maxPosition > 0) return maxPosition;
|
||||
if (round === 2 && maxPosition > 1) return maxPosition >> 1;
|
||||
if (maxPosition <= 2) return -1;
|
||||
|
||||
const middle = maxPosition >> 1;
|
||||
const offset = (round - 2) >> 1;
|
||||
const position = round & 1 ? Math.max(0, middle - offset) : middle + offset;
|
||||
|
||||
if (position === 0 || position >= maxPosition || position === middle) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return position;
|
||||
}
|
||||
+1
-1
@@ -405,6 +405,6 @@ main() {
|
||||
# Run the main function with all arguments
|
||||
# main "$@"
|
||||
|
||||
main "@leeoniya/ufuzzy"
|
||||
main "quickmatch-js"
|
||||
main "lean-qr"
|
||||
main "lightweight-charts"
|
||||
|
||||
Reference in New Issue
Block a user