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>
117 lines
4.5 KiB
C#
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);
|
|
}
|
|
}
|