Skip to main content

game_manager_lib/utils/
text.rs

1//! Utilitários para manipulação de Strings em nomes de jogos, séries e tags.
2
3// === CONSTANTS ===
4
5const EDITION_SUFFIXES: &[&str] = &[
6    "Collector's Edition",
7    "Collectors Edition",
8    "Complete Edition",
9    "Game of the Year Edition",
10    "Definitive Edition",
11    "Special Edition",
12    "Deluxe Edition",
13    "Premium Edition",
14    "Enhanced Edition",
15    "GOTY Edition",
16    "GOTY",
17    "Ultimate Edition",
18];
19
20/// Remove símbolos de marca registrada, preservando capitalização e conteúdo.
21/// Uso: limpar nome antes de persistir (import), exibição.
22pub fn strip_trademark_symbols(name: &str) -> String {
23    name.chars()
24        .filter(|c| !matches!(c, '™' | '®' | '©'))
25        .collect::<String>()
26        .split_whitespace()
27        .collect::<Vec<_>>()
28        .join(" ")
29}
30
31/// Normaliza para comparação/matching: minúsculo, sem símbolos, sem pontuação de separador.
32/// Uso: cache keys, dedup, series matching, comparação com resultado de busca externa.
33pub fn normalize_for_matching(name: &str) -> String {
34    strip_trademark_symbols(name)
35        .to_lowercase()
36        .replace(':', "")
37        .trim()
38        .to_string()
39}
40
41/// Verifica se `needle` aparece em `haystack` com fronteira de palavra nos dois lados
42/// (sem letra/dígito colado antes ou depois). Evita falsos positivos como "demo"
43/// dentro de "demon's" ou "trial" dentro de "trials".
44pub fn contains_word_boundary(haystack: &str, needle: &str) -> bool {
45    let h = haystack.as_bytes();
46    let n = needle.as_bytes();
47    if n.is_empty() || n.len() > h.len() {
48        return false;
49    }
50    for start in 0..=(h.len() - n.len()) {
51        if &h[start..start + n.len()] == n {
52            let before_ok = start == 0 || !(h[start - 1] as char).is_alphanumeric();
53            let end = start + n.len();
54            let after_ok = end == h.len() || !(h[end] as char).is_alphanumeric();
55            if before_ok && after_ok {
56                return true;
57            }
58        }
59    }
60    false
61}
62
63/// Heurística por palavra-chave para identificar nomes que provavelmente
64/// não são o jogo-base (DLC, edição especial, trilha sonora, demo, pré-venda).
65/// Não é definitivo — é um filtro barato para descartar candidatos óbvios
66/// antes de aplicar critérios mais confiáveis (match exato, dados estruturados).
67pub fn is_likely_non_base_game(name: &str) -> bool {
68    let lower = name.to_lowercase();
69    if name.trim().len() <= 2 {
70        return true;
71    }
72
73    // Palavras curtas com risco comprovado de colisão (demon's, trials) — exigem fronteira de palavra nos dois lados.
74    let boundary_keywords = ["demo", "trial"];
75    if boundary_keywords
76        .iter()
77        .any(|kw| contains_word_boundary(&lower, kw))
78    {
79        return true;
80    }
81
82    // Demais keywords: substring simples é seguro, pois não há risco de colisão com nomes de jogos conhecidos.
83    let substring_keywords = [
84        "season pass",
85        "dlc",
86        "add-on",
87        "addon",
88        "expansion",
89        "pre-order",
90        "preorder",
91        "pre order",
92        "starter pack",
93        "pack",
94        "soundtrack",
95        "artbook",
96        "art book",
97        "playtest",
98        "goodie",
99    ];
100    substring_keywords.iter().any(|kw| lower.contains(kw))
101}
102
103/// Remove sufixo de edição conhecido do final do nome, se houver.
104/// Usado como segunda tentativa de match quando o nome completo (com edição) não bate.
105pub fn strip_edition_suffix(name: &str) -> String {
106    let trimmed = name.trim();
107    for suffix in EDITION_SUFFIXES {
108        if let Some(rest) = trimmed.to_lowercase().strip_suffix(&suffix.to_lowercase()) {
109            return trimmed[..rest.len()]
110                .trim_end_matches(['-', ':', ',', ' '])
111                .trim()
112                .to_string();
113        }
114    }
115    trimmed.to_string()
116}