namespace MaBibli.Shared.Textes;
/// Verdict du rapprochement de deux noms d'auteur.
public enum NiveauRapprochement
{
/// Rien de commun : ce sont deux auteurs distincts.
Aucun = 0,
///
/// Variante sûre : même nom à la casse, aux accents, à l'ordre ou à l'abréviation près.
/// Peut être fusionné automatiquement.
///
Certain = 1,
///
/// Rapprochement plausible mais ambigu : un nom est contenu dans l'autre
/// (« Hamilton » vers « Peter F. Hamilton »). À proposer, jamais à appliquer seul.
///
Suggere = 2,
}
///
/// Règles de rapprochement des noms d'auteurs.
///
///
/// Décision actée dans CLAUDE.md : le regroupement automatique se limite aux variantes
/// sûres — casse, accents, ordre Nom, Prénom ↔ Prénom Nom, initiales abrégées
/// quand le reste concorde. Tout le reste est proposé à l'utilisateur.
///
/// La raison est asymétrique : ne pas fusionner laisse deux fiches à réunir d'un clic, alors
/// qu'une fusion erronée mélange les livres de deux personnes et se défait mal. Dans le doute,
/// on ne fusionne pas.
///
///
public static class RapprochementAuteurs
{
///
/// Clé de regroupement : mots normalisés triés, ce qui rend l'ordre indifférent.
///
///
/// « Zola, Émile » et « Émile Zola » donnent tous deux emile zola. C'est cette clé qui
/// porte l'unicité en base : deux auteurs ne peuvent pas coexister avec la même.
///
/// Elle ne couvre pas les initiales (p f hamilton ≠ f hamilton peter) :
/// ce cas relève de , qui aligne les mots au lieu de les hacher.
///
///
public static string Cle(string? nom)
{
var mots = NormalisationTexte.Mots(nom);
Array.Sort(mots, StringComparer.Ordinal);
return string.Join(' ', mots);
}
///
/// Compare deux noms d'auteur et dit s'ils désignent sûrement, peut-être, ou sûrement pas
/// la même personne.
///
public static NiveauRapprochement Comparer(string? a, string? b)
{
var motsA = NormalisationTexte.Mots(a);
var motsB = NormalisationTexte.Mots(b);
if (motsA.Length == 0 || motsB.Length == 0)
{
return NiveauRapprochement.Aucun;
}
if (motsA.Length == motsB.Length)
{
// Même nombre de mots : soit tout s'aligne (variante sûre), soit ce sont deux personnes.
// On exige au moins un mot entier commun : « P. H. » et « Peter Hamilton » s'alignent
// par les seules initiales, ce qui ne concorde sur rien de vérifiable.
return Aligne(motsA, motsB, out var pleinPartage) && pleinPartage
? NiveauRapprochement.Certain
: NiveauRapprochement.Aucun;
}
// Nombres de mots différents : au mieux l'un est un fragment de l'autre
// (« Hamilton » ⊂ « Peter F. Hamilton »). Jamais sûr — deux personnes partagent
// couramment un patronyme.
var (court, longue) = motsA.Length < motsB.Length ? (motsA, motsB) : (motsB, motsA);
return Aligne(court, longue, out var motPleinPartage) && motPleinPartage
? NiveauRapprochement.Suggere
: NiveauRapprochement.Aucun;
}
/// Raccourci : les deux noms sont-ils une variante sûre l'un de l'autre ?
public static bool SontLeMemeAuteur(string? a, string? b) =>
Comparer(a, b) == NiveauRapprochement.Certain;
///
/// Deux jeux de noms ont-ils au moins un auteur en commun ?
///
///
/// Sert partout où l'on rapproche deux fiches par leur titre : le titre seul ne suffit
/// jamais — deux recueils « Nouvelles » sans rapport se confondraient — mais un auteur
/// commun tranche.
///
/// Convention : deux jeux tous deux vides concordent. Le titre est alors la
/// seule information disponible des deux côtés, et exiger un auteur reviendrait à ne jamais
/// rien rapprocher. Un jeu vide face à un jeu renseigné, en revanche, ne concorde pas :
/// l'information manque d'un seul côté, ce qui n'est pas une raison de conclure.
///
///
public static bool PartagentUnAuteur(IEnumerable a, IEnumerable b)
{
var gauche = a.Where(n => !string.IsNullOrWhiteSpace(n)).ToList();
var droite = b.Where(n => !string.IsNullOrWhiteSpace(n)).ToList();
if (gauche.Count == 0 && droite.Count == 0)
{
return true;
}
return gauche.Any(g => droite.Any(d => SontLeMemeAuteur(g, d)));
}
///
/// Cherche à placer chaque mot de sur un mot distinct de
/// , par égalité ou par initiale.
///
///
/// Vrai si au moins un couple aligné est une égalité de mots entiers. Sans cette
/// garantie, « P. » se rapprocherait de n'importe quel prénom commençant par P — une
/// suggestion sans valeur qui n'apprendrait rien à l'utilisateur.
///
///
/// Recherche exhaustive avec retour arrière. Les noms de personnes tiennent en quelques mots,
/// le coût est négligeable ; une correspondance gloutonne, elle, échouerait sur
/// « P. Peter » vs « Peter Paul » en consommant le mauvais mot en premier.
///
private static bool Aligne(string[] aPlacer, string[] cible, out bool motPleinPartage)
{
var pris = new bool[cible.Length];
motPleinPartage = false;
return Placer(aPlacer, cible, pris, 0, ref motPleinPartage);
}
private static bool Placer(
string[] aPlacer, string[] cible, bool[] pris, int index, ref bool motPleinPartage)
{
if (index == aPlacer.Length)
{
return true;
}
var mot = aPlacer[index];
for (var i = 0; i < cible.Length; i++)
{
if (pris[i])
{
continue;
}
var autre = cible[i];
var egalite = mot == autre;
var initiale = !egalite && EstInitialeDe(mot, autre);
if (!egalite && !initiale)
{
continue;
}
pris[i] = true;
var pleinAvant = motPleinPartage;
motPleinPartage = pleinAvant || (egalite && mot.Length > 1);
if (Placer(aPlacer, cible, pris, index + 1, ref motPleinPartage))
{
return true;
}
pris[i] = false;
motPleinPartage = pleinAvant;
}
return false;
}
/// « p » est l'initiale de « peter ». Une lettre seule d'un côté, le mot de l'autre.
private static bool EstInitialeDe(string a, string b) =>
(a.Length == 1 && b.Length > 1 && b[0] == a[0])
|| (b.Length == 1 && a.Length > 1 && a[0] == b[0]);
}