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>
This commit is contained in:
@@ -25,15 +25,131 @@ public sealed class ServiceRenormalisation(MaBibliDbContext db, ILogger<ServiceR
|
||||
{
|
||||
var titres = await RenormaliserTitresAsync(ct);
|
||||
var (auteursCorriges, auteursFusionnes) = await RenormaliserAuteursAsync(ct);
|
||||
var autres = await RenormaliserLesAutresTablesAsync(ct);
|
||||
|
||||
if (titres + auteursCorriges + auteursFusionnes > 0)
|
||||
if (titres + auteursCorriges + auteursFusionnes + autres > 0)
|
||||
{
|
||||
logger.LogInformation(
|
||||
"Renormalisation : {Titres} titre(s), {Auteurs} auteur(s) corrigés, {Fusions} fusionné(s).",
|
||||
titres, auteursCorriges, auteursFusionnes);
|
||||
"Renormalisation : {Titres} titre(s), {Auteurs} auteur(s) corrigés, "
|
||||
+ "{Fusions} fusionné(s), {Autres} autre(s) ligne(s).",
|
||||
titres, auteursCorriges, auteursFusionnes, autres);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remet à jour les colonnes normalisées des tables apparues après ce service.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Envies, séries, revues et numéros calculent leurs formes <b>à l'écriture</b> : rien ne
|
||||
/// les rattraperait si la règle de normalisation changeait — et elle a changé le 2026-08-19,
|
||||
/// avec le développement des ligatures <c>œ</c> et <c>æ</c>. Sans ce passage, une envie
|
||||
/// enregistrée avant serait restée introuvable par sa propre clé.
|
||||
/// <para>
|
||||
/// ⚠️ Trois de ces colonnes portent une <b>unicité</b>. Une règle plus large peut donc
|
||||
/// rendre identiques deux lignes qui ne l'étaient pas (« L'Œuvre » et « L'oeuvre »), et
|
||||
/// l'écriture échouerait <b>au démarrage</b> — l'application ne se lancerait plus. La
|
||||
/// collision est donc détectée avant d'écrire : la ligne la plus ancienne prend la nouvelle
|
||||
/// clé, les suivantes <b>gardent l'ancienne</b> et sont journalisées. Mal normalisée est
|
||||
/// infiniment préférable à supprimée, ou à un serveur qui ne démarre pas.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
private async Task<int> RenormaliserLesAutresTablesAsync(CancellationToken ct)
|
||||
{
|
||||
var corriges = 0;
|
||||
|
||||
var souhaits = await db.LivresSouhaites.OrderBy(s => s.Id).ToListAsync(ct);
|
||||
corriges += Renormaliser(
|
||||
souhaits,
|
||||
s => Cle(s.Utilisateur, s.TitreNormalise, s.AuteurNormalise),
|
||||
s => Cle(s.Utilisateur, CleOeuvre.Cle(s.Titre), RapprochementAuteurs.Cle(s.Auteur)),
|
||||
s => s.RecalculerFormes(),
|
||||
s => $"l'envie « {s.Titre} »");
|
||||
|
||||
var series = await db.Series.OrderBy(s => s.Id).ToListAsync(ct);
|
||||
corriges += Renormaliser(
|
||||
series,
|
||||
s => s.TitreNormalise,
|
||||
s => NormalisationTexte.Normaliser(s.Titre),
|
||||
s => s.RecalculerFormes(),
|
||||
s => $"la série « {s.Titre} »");
|
||||
|
||||
var revues = await db.Revues.OrderBy(r => r.Id).ToListAsync(ct);
|
||||
corriges += Renormaliser(
|
||||
revues,
|
||||
r => r.TitreNormalise,
|
||||
r => NormalisationTexte.Normaliser(r.Titre),
|
||||
r => r.RecalculerFormes(),
|
||||
r => $"la revue « {r.Titre} »");
|
||||
|
||||
var numeros = await db.NumerosRevue.OrderBy(n => n.Id).ToListAsync(ct);
|
||||
corriges += Renormaliser(
|
||||
numeros,
|
||||
n => Cle(n.RevueId.ToString(), n.NumeroNormalise),
|
||||
n => Cle(n.RevueId.ToString(), NormalisationTexte.Normaliser(n.Numero)),
|
||||
n => n.RecalculerFormes(),
|
||||
n => $"le numéro {n.Numero}");
|
||||
|
||||
if (corriges > 0)
|
||||
{
|
||||
await db.SaveChangesAsync(ct);
|
||||
}
|
||||
|
||||
return corriges;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recalcule les formes d'un ensemble de lignes en refusant toute collision d'unicité.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Le jeu des clés occupées part de l'état <b>actuel</b> de la base, et chaque ligne libère
|
||||
/// la sienne avant de réserver la nouvelle. Sans cette libération, une ligne inchangée
|
||||
/// bloquerait sa propre mise à jour ; sans le jeu initial, une ligne traitée tôt prendrait
|
||||
/// la clé d'une ligne non encore traitée, et l'écriture échouerait.
|
||||
/// </remarks>
|
||||
private int Renormaliser<T>(
|
||||
IReadOnlyList<T> lignes,
|
||||
Func<T, string> cleActuelle,
|
||||
Func<T, string> cleVoulue,
|
||||
Action<T> appliquer,
|
||||
Func<T, string> nommer)
|
||||
{
|
||||
var occupees = new HashSet<string>(lignes.Select(cleActuelle), StringComparer.Ordinal);
|
||||
var corriges = 0;
|
||||
|
||||
foreach (var ligne in lignes)
|
||||
{
|
||||
var actuelle = cleActuelle(ligne);
|
||||
var voulue = cleVoulue(ligne);
|
||||
|
||||
if (actuelle == voulue)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
occupees.Remove(actuelle);
|
||||
|
||||
if (!occupees.Add(voulue))
|
||||
{
|
||||
// Deux lignes que la nouvelle règle rend identiques. On garde les deux, en
|
||||
// laissant la seconde sur son ancienne clé : c'est visible, réparable à la main,
|
||||
// et surtout ça ne coûte pas le démarrage du serveur.
|
||||
occupees.Add(actuelle);
|
||||
logger.LogWarning(
|
||||
"Renormalisation : {Ligne} garde son ancienne forme, la nouvelle est déjà prise.",
|
||||
nommer(ligne));
|
||||
continue;
|
||||
}
|
||||
|
||||
appliquer(ligne);
|
||||
corriges++;
|
||||
}
|
||||
|
||||
return corriges;
|
||||
}
|
||||
|
||||
/// <summary>Clé composite comparable, les composants ne pouvant pas contenir de NUL.</summary>
|
||||
private static string Cle(params string?[] composants) => string.Join('\0', composants);
|
||||
|
||||
private async Task<int> RenormaliserTitresAsync(CancellationToken ct)
|
||||
{
|
||||
var livres = await db.Livres.ToListAsync(ct);
|
||||
|
||||
Reference in New Issue
Block a user