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. 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. v1[j] + cost, ); 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); }