diff --git a/MaBibli.Api/Services/Isbn/NettoyageIsbd.cs b/MaBibli.Api/Services/Isbn/NettoyageIsbd.cs
index 2382b14..fa6deab 100644
--- a/MaBibli.Api/Services/Isbn/NettoyageIsbd.cs
+++ b/MaBibli.Api/Services/Isbn/NettoyageIsbd.cs
@@ -1,3 +1,4 @@
+using System.Globalization;
using System.Text.RegularExpressions;
namespace MaBibli.Api.Services.Isbn;
@@ -43,6 +44,71 @@ public static partial class NettoyageIsbd
return s.Length == 0 ? null : s;
}
+ ///
+ /// Retire du titre le nom d'auteur qui y est collé en fin de chaîne.
+ ///
+ ///
+ /// Cas réel mesuré, ISBN 9782749942636 :
+ /// dc:title vaut « Le plus grand défi … (Nouvelle éd. augmentée) Aurélien Barrau »
+ /// alors que dc:creator vaut « Barrau, Aurélien (1973-....) ». Le nom est recollé
+ /// sans le séparateur ISBD « / », donc ne peut rien y faire : la
+ /// notice est sale à la source.
+ ///
+ /// L'heuristique est volontairement stricte — comparaison en fin de chaîne uniquement,
+ /// et seulement contre un auteur déjà nettoyé. Un titre qui contient légitimement un nom de
+ /// personne ailleurs (« Lettre à Émile Zola ») n'est pas touché, et un titre qui est
+ /// le nom de l'auteur (biographie, recueil) est laissé intact plutôt que réduit à rien.
+ ///
+ ///
+ public static string? RetirerAuteurEnFin(string? titre, IEnumerable auteurs)
+ {
+ var s = titre?.Trim();
+ if (string.IsNullOrEmpty(s))
+ {
+ return null;
+ }
+
+ foreach (var auteur in auteurs)
+ {
+ var nom = auteur?.Trim();
+ if (string.IsNullOrEmpty(nom) || nom.Length >= s.Length)
+ {
+ // Un nom aussi long que le titre ne laisserait rien : c'est une biographie
+ // ou un recueil, pas une notice sale.
+ continue;
+ }
+
+ var debut = s.Length - nom.Length;
+
+ // IgnoreNonSpace ignore les accents sans changer les longueurs, contrairement à une
+ // normalisation NFD : les indices calculés restent valables sur la chaîne d'origine.
+ var identique = string.Compare(
+ s, debut, nom, 0, nom.Length,
+ CultureInfo.InvariantCulture,
+ CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace) == 0;
+
+ if (!identique)
+ {
+ continue;
+ }
+
+ // Le nom doit commencer sur une frontière de mot, sinon « …Barrau » couperait aussi
+ // dans un mot qui se termine par les mêmes lettres.
+ if (char.IsLetterOrDigit(s[debut - 1]))
+ {
+ continue;
+ }
+
+ var reste = s[..debut].TrimEnd(' ', ',', ';', '.', '-', '/', ':');
+ if (reste.Length > 0)
+ {
+ return reste;
+ }
+ }
+
+ return s;
+ }
+
///
/// Auteur : retirer les dates entre parenthèses, le rôle qui suit le point,
/// puis inverser « Nom, Prénom » en « Prénom Nom ».
diff --git a/MaBibli.Tests/NettoyageIsbdTests.cs b/MaBibli.Tests/NettoyageIsbdTests.cs
index 6ce560f..493af12 100644
--- a/MaBibli.Tests/NettoyageIsbdTests.cs
+++ b/MaBibli.Tests/NettoyageIsbdTests.cs
@@ -22,6 +22,52 @@ public class NettoyageIsbdTests
public void Titre_laisse_passer_ce_qui_est_deja_propre(string brut, string attendu)
=> Assert.Equal(attendu, NettoyageIsbd.Titre(brut));
+ ///
+ /// Cas réel mesuré sur l'ISBN 9782749942636 : la BnF recolle le nom de l'auteur en
+ /// fin de titre sans le séparateur ISBD, ce que ne
+ /// peut donc pas voir.
+ ///
+ [Fact]
+ public void Titre_retire_le_nom_de_lauteur_colle_en_fin()
+ {
+ const string brut = "Le plus grand défi de l'histoire de l'humanité : face à la "
+ + "catastrophe écologique et sociale (Nouvelle éd. augmentée) Aurélien Barrau";
+
+ var titre = NettoyageIsbd.RetirerAuteurEnFin(brut, ["Aurélien Barrau"]);
+
+ Assert.Equal(
+ "Le plus grand défi de l'histoire de l'humanité : face à la catastrophe écologique "
+ + "et sociale (Nouvelle éd. augmentée)",
+ titre);
+ }
+
+ [Theory]
+ // Contre-exemple central : le nom est bien là, mais pas en fin de chaîne. On n'y touche pas.
+ [InlineData("Lettre à Émile Zola", "Guy de Maupassant")]
+ [InlineData("Émile Zola et son temps", "Émile Zola")]
+ // Le titre EST le nom de l'auteur (biographie, recueil) : le retirer ne laisserait rien.
+ [InlineData("Aurélien Barrau", "Aurélien Barrau")]
+ // Fin de mot fortuite : « Zola » ne doit pas être découpé dans « Gorgonzola ».
+ [InlineData("Éloge du gorgonzola", "Zola")]
+ // Auteur inconnu du titre.
+ [InlineData("Germinal", "Émile Zola")]
+ public void Titre_ne_mutile_pas_un_nom_legitime(string titre, string auteur)
+ => Assert.Equal(titre, NettoyageIsbd.RetirerAuteurEnFin(titre, [auteur]));
+
+ [Fact]
+ public void Titre_tolere_une_difference_daccent_ou_de_casse_sur_le_nom()
+ => Assert.Equal(
+ "Le grand défi",
+ NettoyageIsbd.RetirerAuteurEnFin("Le grand défi AURELIEN BARRAU", ["Aurélien Barrau"]));
+
+ [Fact]
+ public void Titre_sans_auteur_connu_reste_intact()
+ {
+ Assert.Equal("Germinal", NettoyageIsbd.RetirerAuteurEnFin("Germinal", []));
+ Assert.Equal("Germinal", NettoyageIsbd.RetirerAuteurEnFin("Germinal", [null, " "]));
+ Assert.Null(NettoyageIsbd.RetirerAuteurEnFin(null, ["Émile Zola"]));
+ }
+
[Theory]
[InlineData("Zola, Émile (1840-1902). Auteur du texte", "Émile Zola")]
[InlineData("Saint-Exupéry, Antoine de (1900-1944). Auteur du texte", "Antoine de Saint-Exupéry")]