Files
mabibli/MaBibli.Shared/Textes/NormalisationTexte.cs
T
Mathieu LimonierandClaude Opus 5 6a6d745af4 MaBibli 1.0.0
Gestion de bibliothèque personnelle auto-hébergée : catalogue, prêts,
scan de code-barres, consultation hors-ligne.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 22:36:16 +02:00

117 lines
4.5 KiB
C#

using System.Globalization;
using System.Text;
namespace MaBibli.Shared.Textes;
/// <summary>
/// Mise à plat d'un texte pour la recherche et le regroupement : minuscules, sans accents,
/// sans ponctuation, espaces réduits.
/// </summary>
/// <remarks>
/// <b>Pourquoi une forme stockée plutôt qu'une comparaison à la volée ?</b> SQLite ne sait pas
/// comparer sans tenir compte des accents : sa collation <c>NOCASE</c> ne couvre que l'ASCII, et
/// il n'existe pas de <c>unaccent()</c> intégré. Faire le travail en C# à l'écriture, dans une
/// colonne dédiée, est la seule façon d'obtenir « Emile » → « Émile » <b>sans</b> charger tout
/// le catalogue en mémoire à chaque recherche.
/// <para>
/// Conséquence à ne pas oublier : toute écriture doit recalculer la forme normalisée, et le
/// terme cherché doit passer par la <b>même</b> fonction que la valeur stockée.
/// </para>
/// </remarks>
public static class NormalisationTexte
{
/// <summary>
/// Forme comparable d'un texte quelconque : « Émile ZOLA » → « emile zola ».
/// </summary>
/// <remarks>
/// La ponctuation devient une espace plutôt que de disparaître : « P.F. Hamilton » doit
/// donner trois mots (<c>p f hamilton</c>) et non un seul (<c>pf hamilton</c>), sans quoi
/// le rapprochement par initiales serait impossible.
/// <para>
/// Les <b>ligatures</b> <c>œ</c> et <c>æ</c> sont décomposées en <c>oe</c> et <c>ae</c>.
/// ⚠️ 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, <i>L'Œuvre</i> de Zola — que la BnF
/// écrit avec la ligature — resterait introuvable à qui tape « oeuvre » au clavier.
/// </para>
/// <para>
/// 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.
/// </para>
/// </remarks>
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);
}
/// <summary>
/// Développement d'une ligature, ou <c>null</c> si le caractère n'en est pas une.
/// </summary>
/// <remarks>
/// Volontairement limité à <c>œ</c> et <c>æ</c>, seules ligatures courantes du français.
/// Les ligatures typographiques de présentation (<c>fi</c>, <c>fl</c>) ne sont pas traitées :
/// elles n'apparaissent pas dans les catalogues, et rien n'a montré le besoin.
/// </remarks>
private static string? Ligature(char c) => c switch
{
'œ' or 'Œ' => "oe",
'æ' or 'Æ' => "ae",
_ => null,
};
/// <summary>Mots de la forme normalisée, sans mot vide.</summary>
public static string[] Mots(string? brut)
{
var normalise = Normaliser(brut);
return normalise.Length == 0
? []
: normalise.Split(' ', StringSplitOptions.RemoveEmptyEntries);
}
}