File size: 2,640 Bytes
dbb1bf9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
export interface TokenizedTitle {
  words: Set<string>;
  ordered: string[];
}

const INFLECTION_SUFFIXES = new Set(['s', 'es', 'ian', 'ians', 'ean', 'eans', 'an', 'ans', 'n', 'ns', 'i', 'is', 'ish', 'ese']);
const MIN_SUFFIX_KEYWORD_LEN = 4;

export function tokenizeForMatch(title: string): TokenizedTitle {
  const lower = title.toLowerCase();
  const words = new Set<string>();
  const ordered: string[] = [];
  for (const raw of lower.split(/\s+/)) {
    const cleaned = raw.replace(/^[^a-z0-9]+|[^a-z0-9]+$/g, '');
    if (!cleaned) continue;
    words.add(cleaned);
    ordered.push(cleaned);
    for (const part of cleaned.split(/[^a-z0-9]+/)) {
      if (part) words.add(part);
    }
  }
  return { words, ordered };
}

function hasSuffix(word: string, keyword: string): boolean {
  if (word.length <= keyword.length) return false;
  if (word.startsWith(keyword)) {
    const suffix = word.slice(keyword.length);
    if (INFLECTION_SUFFIXES.has(suffix)) return true;
  }
  if (keyword.endsWith('e')) {
    const stem = keyword.slice(0, -1);
    if (word.length > stem.length && word.startsWith(stem)) {
      const suffix = word.slice(stem.length);
      if (INFLECTION_SUFFIXES.has(suffix)) return true;
    }
  }
  return false;
}

function wordMatches(token: string, kwPart: string): boolean {
  if (token === kwPart) return true;
  if (kwPart.length >= MIN_SUFFIX_KEYWORD_LEN) return hasSuffix(token, kwPart);
  return false;
}

function matchSingleWord(words: Set<string>, keyword: string): boolean {
  if (words.has(keyword)) return true;
  if (keyword.length < MIN_SUFFIX_KEYWORD_LEN) return false;
  for (const word of words) {
    if (hasSuffix(word, keyword)) return true;
  }
  return false;
}

export function matchKeyword(tokens: TokenizedTitle, keyword: string): boolean {
  const parts = keyword.toLowerCase().split(/\s+/).filter((w): w is string => w.length > 0);
  if (parts.length === 0) return false;
  if (parts.length === 1) return matchSingleWord(tokens.words, parts[0]!);
  const { ordered } = tokens;
  for (let i = 0; i <= ordered.length - parts.length; i++) {
    let match = true;
    for (let j = 0; j < parts.length; j++) {
      if (!wordMatches(ordered[i + j]!, parts[j]!)) { match = false; break; }
    }
    if (match) return true;
  }
  return false;
}

export function matchesAnyKeyword(tokens: TokenizedTitle, keywords: string[]): boolean {
  for (const kw of keywords) {
    if (matchKeyword(tokens, kw)) return true;
  }
  return false;
}

export function findMatchingKeywords(tokens: TokenizedTitle, keywords: string[]): string[] {
  return keywords.filter(kw => matchKeyword(tokens, kw));
}