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énomPré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 hamiltonf 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]); }