using System.Globalization; using System.Text.RegularExpressions; namespace MaBibli.Api.Services.Isbn; /// /// Nettoyage de la ponctuation ISBD des champs Dublin Core de la BnF. /// /// /// Les champs BnF ne sont pas exploitables bruts. Règles validées le 2026-08-17 (CLAUDE.md) : /// /// dc:title : Germinal / Émile Zola ; préface…Germinal /// dc:creator : Zola, Émile (1840-1902). Auteur du texteÉmile Zola /// dc:publisher : le Livre de poche (Paris)le Livre de poche /// /// Les valeurs déjà propres doivent traverser ces règles inchangées. /// public static partial class NettoyageIsbd { [GeneratedRegex(@"\s*\([^)]*\)")] private static partial Regex Parentheses(); [GeneratedRegex(@"\s*\([^)]*\)\s*$")] private static partial Regex ParenthesesFinales(); /// /// Titre : couper à la première mention de responsabilité (« / »). /// public static string? Titre(string? brut) { var s = brut?.Trim(); if (string.IsNullOrEmpty(s)) { return null; } var i = s.IndexOf(" / ", StringComparison.Ordinal); if (i >= 0) { s = s[..i]; } s = s.Trim(); 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 ». /// public static string? Auteur(string? brut) { var s = brut?.Trim(); if (string.IsNullOrEmpty(s)) { return null; } // 1. Dates de vie et autres qualificatifs entre parenthèses : (1840-1902), (1932-....) s = Parentheses().Replace(s, string.Empty).Trim(); // 2. Rôle après le point : « . Auteur du texte », « . Préfacier ». var pointRole = IndexDuPointDeRole(s); if (pointRole >= 0) { s = s[..pointRole]; } s = RetirerPonctuationFinale(s); // 3. Inversion « Nom, Prénom » → « Prénom Nom » (sur la première virgule seulement). var virgule = s.IndexOf(", ", StringComparison.Ordinal); if (virgule > 0) { var nom = s[..virgule].Trim(); var prenom = s[(virgule + 2)..].Trim(); if (nom.Length > 0 && prenom.Length > 0) { s = $"{prenom} {nom}"; } } s = s.Trim(); return s.Length == 0 ? null : s; } /// /// Éditeur : retirer la ville entre parenthèses en fin de chaîne. /// public static string? Editeur(string? brut) { var s = brut?.Trim(); if (string.IsNullOrEmpty(s)) { return null; } s = ParenthesesFinales().Replace(s, string.Empty).Trim(); return s.Length == 0 ? null : s; } /// /// Retire les virgules et espaces de fin, et le point final sauf s'il appartient /// à une initiale (« Cormen, Thomas H. » doit garder son point). /// private static string RetirerPonctuationFinale(string s) { s = s.TrimEnd(' ', ','); while (s.EndsWith('.')) { var i = s.Length - 1; var estInitiale = i >= 1 && char.IsUpper(s[i - 1]) && (i == 1 || s[i - 2] == ' ' || s[i - 2] == '-' || s[i - 2] == '\''); if (estInitiale) { break; } s = s[..^1].TrimEnd(' ', ','); } return s; } /// /// Position du point qui introduit le rôle, ou -1. /// /// /// Un point suivi d'une espace sépare le rôle du nom. On ignore les points d'initiales /// (« Cormen, Thomas H. Auteur ») : un point précédé d'une majuscule isolée n'est pas /// un séparateur de rôle. /// private static int IndexDuPointDeRole(string s) { for (var i = 0; i < s.Length - 1; i++) { if (s[i] != '.' || s[i + 1] != ' ') { continue; } var estInitiale = i >= 1 && char.IsUpper(s[i - 1]) && (i == 1 || s[i - 2] == ' ' || s[i - 2] == '-' || s[i - 2] == '\''); if (!estInitiale) { return i; } } return -1; } }