using System.Globalization; using System.Text; namespace MaBibli.Shared.Textes; /// /// Mise à plat d'un texte pour la recherche et le regroupement : minuscules, sans accents, /// sans ponctuation, espaces réduits. /// /// /// Pourquoi une forme stockée plutôt qu'une comparaison à la volée ? SQLite ne sait pas /// comparer sans tenir compte des accents : sa collation NOCASE ne couvre que l'ASCII, et /// il n'existe pas de unaccent() intégré. Faire le travail en C# à l'écriture, dans une /// colonne dédiée, est la seule façon d'obtenir « Emile » → « Émile » sans charger tout /// le catalogue en mémoire à chaque recherche. /// /// Conséquence à ne pas oublier : toute écriture doit recalculer la forme normalisée, et le /// terme cherché doit passer par la même fonction que la valeur stockée. /// /// public static class NormalisationTexte { /// /// Forme comparable d'un texte quelconque : « Émile ZOLA » → « emile zola ». /// /// /// La ponctuation devient une espace plutôt que de disparaître : « P.F. Hamilton » doit /// donner trois mots (p f hamilton) et non un seul (pf hamilton), sans quoi /// le rapprochement par initiales serait impossible. /// /// Les ligatures œ et æ sont décomposées en oe et ae. /// ⚠️ Ce n'est pas ce que fait la décomposition NFD, qui sépare les accents mais laisse les /// ligatures intactes : sans ce traitement explicite, L'Œuvre de Zola — que la BnF /// écrit avec la ligature — resterait introuvable à qui tape « oeuvre » au clavier. /// /// /// Le choix s'est porté sur une table explicite plutôt que sur NFKD, qui aurait fait bien /// davantage : « ² » → « 2 », « Ⅻ » → « XII », les espaces insécables, les formes de /// présentation. Une normalisation de recherche doit rester prévisible ; on ne défait ici que /// ce dont on a constaté le besoin. /// /// public static string Normaliser(string? brut) { if (string.IsNullOrWhiteSpace(brut)) { return string.Empty; } // NFD sépare la lettre de son accent ; on ne garde ensuite que la lettre. var decompose = brut.Normalize(NormalizationForm.FormD); var sortie = new StringBuilder(decompose.Length); var espaceEnAttente = false; foreach (var c in decompose) { if (CharUnicodeInfo.GetUnicodeCategory(c) == UnicodeCategory.NonSpacingMark) { continue; // Accent détaché par la décomposition. } if (Ligature(c) is { } lettres) { if (espaceEnAttente && sortie.Length > 0) { sortie.Append(' '); } espaceEnAttente = false; sortie.Append(lettres); continue; } if (char.IsLetterOrDigit(c)) { if (espaceEnAttente && sortie.Length > 0) { sortie.Append(' '); } espaceEnAttente = false; sortie.Append(char.ToLowerInvariant(c)); continue; } // Tout le reste (espaces, points, virgules, tirets…) est un séparateur de mots. espaceEnAttente = true; } return sortie.ToString().Normalize(NormalizationForm.FormC); } /// /// Développement d'une ligature, ou null si le caractère n'en est pas une. /// /// /// Volontairement limité à œ et æ, seules ligatures courantes du français. /// Les ligatures typographiques de présentation (, ) ne sont pas traitées : /// elles n'apparaissent pas dans les catalogues, et rien n'a montré le besoin. /// private static string? Ligature(char c) => c switch { 'œ' or 'Œ' => "oe", 'æ' or 'Æ' => "ae", _ => null, }; /// Mots de la forme normalisée, sans mot vide. public static string[] Mots(string? brut) { var normalise = Normaliser(brut); return normalise.Length == 0 ? [] : normalise.Split(' ', StringSplitOptions.RemoveEmptyEntries); } }