stash/ui/v2.5/src/utils/query.ts
WithoutPants 9981574e82
Add gallery select filter and fix image gallery filtering (#4535)
* Accept gallery ids in findGalleries
* Add gallery select component
* Add and fix image gallery filter
* Show gallery path as alias
2024-02-09 16:42:07 +11:00

345 lines
7.8 KiB
TypeScript

interface ISortable {
id: string;
}
// sortByRelevance is a function that sorts an array of objects by relevance to a query string.
// It uses the following priorities:
// 1. Exact matches
// 2. Starts with
// 3. Word matches
// 4. Word starts with
// 5. Includes
// If aliases are provided, they are also checked in the same order, but with lower priority than
// the name of the object.
export function sortByRelevance<T extends ISortable>(
query: string,
value: T[],
getName: (o: T) => string,
getAliases?: (o: T) => string[] | undefined
) {
if (!query) {
return value;
}
query = query.toLowerCase();
interface ICacheEntry {
aliases?: string[];
aliasMatch?: boolean;
aliasStartsWith?: boolean;
wordIndex?: number;
wordStartsWithIndex?: number;
aliasWordIndex?: number;
aliasWordStartsWithIndex?: number;
aliasIncludesIndex?: number;
}
const cache: Record<string, ICacheEntry> = {};
function setCache(tag: T, partial: Partial<ICacheEntry>) {
cache[tag.id] = {
...cache[tag.id],
...partial,
};
}
function getObjectAliases(o: T) {
const cached = cache[o.id]?.aliases;
if (cached !== undefined) {
return cached;
}
if (!getAliases) {
return [];
}
const aliases = getAliases(o)?.map((a) => a.toLowerCase()) ?? [];
setCache(o, { aliases });
return aliases;
}
function aliasMatches(o: T) {
const cached = cache[o.id]?.aliasMatch;
if (cached !== undefined) {
return cached;
}
const aliases = getObjectAliases(o);
const aliasMatch = aliases.some((a) => a === query);
setCache(o, { aliasMatch });
return aliasMatch;
}
function aliasStartsWith(o: T) {
const cached = cache[o.id]?.aliasStartsWith;
if (cached !== undefined) {
return cached;
}
const aliases = getObjectAliases(o);
const startsWith = aliases.some((a) => a.startsWith(query));
setCache(o, { aliasStartsWith: startsWith });
return startsWith;
}
function getWords(o: T) {
return getName(o).toLowerCase().split(" ");
}
function getAliasWords(tag: T) {
const aliases = getObjectAliases(tag);
return aliases.map((a) => a.split(" ")).flat();
}
function getWordIndex(o: T) {
const cached = cache[o.id]?.wordIndex;
if (cached !== undefined) {
return cached;
}
const words = getWords(o);
const wordIndex = words.findIndex((w) => w === query);
setCache(o, { wordIndex });
return wordIndex;
}
function getAliasWordIndex(o: T) {
const cached = cache[o.id]?.aliasWordIndex;
if (cached !== undefined) {
return cached;
}
const aliasWords = getAliasWords(o);
const aliasWordIndex = aliasWords.findIndex((w) => w === query);
setCache(o, { aliasWordIndex });
return aliasWordIndex;
}
function getWordStartsWithIndex(o: T) {
const cached = cache[o.id]?.wordStartsWithIndex;
if (cached !== undefined) {
return cached;
}
const words = getWords(o);
const wordStartsWithIndex = words.findIndex((w) => w.startsWith(query));
setCache(o, { wordStartsWithIndex });
return wordStartsWithIndex;
}
function getAliasWordStartsWithIndex(o: T) {
const cached = cache[o.id]?.aliasWordStartsWithIndex;
if (cached !== undefined) {
return cached;
}
const aliasWords = getAliasWords(o);
const aliasWordStartsWithIndex = aliasWords.findIndex((w) =>
w.startsWith(query)
);
setCache(o, { aliasWordStartsWithIndex });
return aliasWordStartsWithIndex;
}
function getAliasIncludesIndex(o: T) {
const cached = cache[o.id]?.aliasIncludesIndex;
if (cached !== undefined) {
return cached;
}
const aliases = getObjectAliases(o);
const aliasIncludesIndex = aliases.findIndex((a) => a.includes(query));
setCache(o, { aliasIncludesIndex });
return aliasIncludesIndex;
}
function compare(a: T, b: T) {
const aName = getName(a).toLowerCase();
const bName = getName(b).toLowerCase();
const aAlias = aliasMatches(a);
const bAlias = aliasMatches(b);
// exact matches first
if (aName === query && bName !== query) {
return -1;
}
if (aName !== query && bName === query) {
return 1;
}
if (aAlias && !bAlias) {
return -1;
}
if (!aAlias && bAlias) {
return 1;
}
// then starts with
if (aName.startsWith(query) && !bName.startsWith(query)) {
return -1;
}
if (!aName.startsWith(query) && bName.startsWith(query)) {
return 1;
}
const aAliasStartsWith = aliasStartsWith(a);
const bAliasStartsWith = aliasStartsWith(b);
if (aAliasStartsWith && !bAliasStartsWith) {
return -1;
}
if (!aAliasStartsWith && bAliasStartsWith) {
return 1;
}
// only check words if the query is a single word
if (!query.includes(" ")) {
// word matches
{
const aWord = getWordIndex(a);
const bWord = getWordIndex(b);
if (aWord !== -1 && bWord === -1) {
return -1;
}
if (aWord === -1 && bWord !== -1) {
return 1;
}
if (aWord !== -1 && bWord !== -1) {
if (aWord === bWord) {
return aName.localeCompare(bName);
}
return aWord - bWord;
}
const aAliasWord = getAliasWordIndex(a);
const bAliasWord = getAliasWordIndex(b);
if (aAliasWord !== -1 && bAliasWord === -1) {
return -1;
}
if (aAliasWord === -1 && bAliasWord !== -1) {
return 1;
}
if (aAliasWord !== -1 && bAliasWord !== -1) {
if (aAliasWord === bAliasWord) {
return aName.localeCompare(bName);
}
return aAliasWord - bAliasWord;
}
}
// then start of word
{
const aWord = getWordStartsWithIndex(a);
const bWord = getWordStartsWithIndex(b);
if (aWord !== -1 && bWord === -1) {
return -1;
}
if (aWord === -1 && bWord !== -1) {
return 1;
}
if (aWord !== -1 && bWord !== -1) {
if (aWord === bWord) {
return aName.localeCompare(bName);
}
return aWord - bWord;
}
const aAliasWord = getAliasWordStartsWithIndex(a);
const bAliasWord = getAliasWordStartsWithIndex(b);
if (aAliasWord !== -1 && bAliasWord === -1) {
return -1;
}
if (aAliasWord === -1 && bAliasWord !== -1) {
return 1;
}
if (aAliasWord !== -1 && bAliasWord !== -1) {
if (aAliasWord === bAliasWord) {
return aName.localeCompare(bName);
}
return aAliasWord - bAliasWord;
}
}
}
// then contains
// performance of this is presumably fast enough to not require caching
const aNameIncludeIndex = aName.indexOf(query);
const bNameIncludeIndex = bName.indexOf(query);
if (aNameIncludeIndex !== -1 && bNameIncludeIndex === -1) {
return -1;
}
if (aNameIncludeIndex === -1 && bNameIncludeIndex !== -1) {
return 1;
}
if (aNameIncludeIndex !== -1 && bNameIncludeIndex !== -1) {
if (aNameIncludeIndex === bNameIncludeIndex) {
return aName.localeCompare(bName);
}
return aNameIncludeIndex - bNameIncludeIndex;
}
const aAliasIncludes = getAliasIncludesIndex(a);
const bAliasIncludes = getAliasIncludesIndex(b);
if (aAliasIncludes !== -1 && bAliasIncludes === -1) {
return -1;
}
if (aAliasIncludes === -1 && bAliasIncludes !== -1) {
return 1;
}
if (aAliasIncludes !== -1 && bAliasIncludes !== -1) {
if (aAliasIncludes === bAliasIncludes) {
return aName.localeCompare(bName);
}
return aAliasIncludes - bAliasIncludes;
}
return aName.localeCompare(bName);
}
return value.slice().sort(compare);
}