Files
mabibli/MaBibli.Shared/Textes/NormalisationTexte.cs
T
mathieuandClaude Opus 5 b97cf33494 Ramener les ligatures « œ » et « æ » à « oe » et « ae »
L'Œuvre de Zola, que la BnF écrit avec la ligature, était introuvable à
qui tape « oeuvre » : NFD sépare les accents mais laisse les ligatures.

Table explicite plutôt que NFKD, qui aurait aussi transformé les exposants,
les chiffres romains et les espaces insécables. Une normalisation de
recherche doit rester prévisible.

Sept colonnes normalisées en dépendent, dont cinq portent une unicité. Le
rattrapage au démarrage est donc étendu aux envies, séries, revues et
numéros — qui ne calculaient leurs formes qu'à l'écriture — et refuse toute
collision AVANT d'écrire : sinon l'exception tomberait au démarrage et le
serveur ne se lancerait plus.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 22:25:59 +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);
}
}