philomena/assets/js/query/literal.ts

115 lines
2.8 KiB
TypeScript
Raw Normal View History

import { FieldMatcher } from './types';
function extractValues(v: string, name: string) {
return name === 'tags' ? v.split(', ') : [v];
}
function makeExactMatcher(term: string): FieldMatcher {
return (v, name) => {
const values = extractValues(v, name);
for (const val of values) {
if (val.toLowerCase() === term.toLowerCase()) {
return true;
}
}
return false;
};
}
function makeWildcardMatcher(term: string): FieldMatcher {
// Transforms wildcard match into regular expression.
// A custom NFA with caching may be more sophisticated but not
// likely to be faster.
2024-07-04 02:27:59 +02:00
const regexpForm = term
.replace(/([.+^$[\]\\(){}|-])/g, '\\$1')
.replace(/([^\\]|[^\\](?:\\\\)+)\*/g, '$1.*')
.replace(/^(?:\\\\)*\*/g, '.*')
.replace(/([^\\]|[^\\](?:\\\\)+)\?/g, '$1.?')
.replace(/^(?:\\\\)*\?/g, '.?');
const wildcard = new RegExp(`^${regexpForm}$`, 'i');
return (v, name) => {
const values = extractValues(v, name);
for (const val of values) {
if (wildcard.test(val)) {
return true;
}
}
return false;
};
}
function fuzzyMatch(term: string, targetStr: string, fuzz: number): boolean {
const targetDistance = fuzz < 1.0 ? targetStr.length * (1.0 - fuzz) : fuzz;
const targetStrLower = targetStr.toLowerCase();
// Work vectors, representing the last three populated
// rows of the dynamic programming matrix of the iterative
// optimal string alignment calculation.
let v0: number[] = [];
let v1: number[] = [];
let v2: number[] = [];
let temp: number[];
for (let i = 0; i <= targetStrLower.length; i += 1) {
v1.push(i);
}
for (let i = 0; i < term.length; i += 1) {
v2[0] = i;
for (let j = 0; j < targetStrLower.length; j += 1) {
const cost = term[i] === targetStrLower[j] ? 0 : 1;
v2[j + 1] = Math.min(
// Deletion.
v1[j + 1] + 1,
// Insertion.
v2[j] + 1,
// Substitution or No Change.
2024-07-04 02:27:59 +02:00
v1[j] + cost,
);
2024-07-04 02:27:59 +02:00
if (i > 1 && j > 1 && term[i] === targetStrLower[j - 1] && targetStrLower[i - 1] === targetStrLower[j]) {
v2[j + 1] = Math.min(v2[j], v0[j - 1] + cost);
}
}
// Rotate dem vec pointers bra.
temp = v0;
v0 = v1;
v1 = v2;
v2 = temp;
}
return v1[targetStrLower.length] <= targetDistance;
}
function makeFuzzyMatcher(term: string, fuzz: number): FieldMatcher {
return (v, name) => {
const values = extractValues(v, name);
for (const val of values) {
if (fuzzyMatch(term, val, fuzz)) {
return true;
}
}
return false;
};
}
export function makeLiteralMatcher(term: string, fuzz: number, wildcardable: boolean): FieldMatcher {
if (fuzz === 0 && !wildcardable) {
return makeExactMatcher(term);
}
if (!wildcardable) {
return makeFuzzyMatcher(term, fuzz);
}
return makeWildcardMatcher(term);
}