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)
{
// Quand le rôle suit une initiale (« Cormen, Thomas H. Auteur du texte »), le point
// qui sert de séparateur appartient aussi au nom : on le conserve.
s = EstPointDInitiale(s, pointRole) ? s[..(pointRole + 1)] : 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;
}
///
/// Titre d'un périodique : couper la mention de responsabilité puis retirer le
/// qualificatif entre parenthèses.
///
///
/// Mesuré sur l'ISSN 2466-6718 le 2026-08-18 : la BnF donne
/// « Médor (Bruxelles) ». Le parenthésé n'est pas une ville d'édition mais le
/// qualificatif de désambiguïsation propre aux notices de revues — deux périodiques
/// peuvent porter le même titre. Il se retire comme la ville d'un éditeur, et pour la même
/// raison : ce qu'on montre à l'utilisateur doit ressembler à ce qui est écrit sur la
/// couverture du magazine qu'il tient en main.
///
public static string? TitrePeriodique(string? brut) => Editeur(Titre(brut));
///
/// 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('.'))
{
if (EstPointDInitiale(s, s.Length - 1))
{
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. Le point d'une initiale
/// (« Cormen, Thomas H. ») n'en est pas un — le couper amputerait le prénom.
///
/// Mais protéger l'initiale ne doit pas protéger le rôle qui la suit : « Cormen, Thomas H.
/// Auteur du texte » renvoyait auparavant « Thomas H. Auteur du texte Cormen ». Ce qui
/// tranche : dans « Nom, Prénom », la seule chose qui puisse suivre une initiale et faire
/// encore partie du nom est une autre initiale (« Tolkien, J. R. R. »). Tout le
/// reste est le rôle.
///
///
private static int IndexDuPointDeRole(string s)
{
for (var i = 0; i < s.Length - 1; i++)
{
if (s[i] != '.' || s[i + 1] != ' ')
{
continue;
}
if (EstPointDInitiale(s, i) && CommenceParUneInitiale(s, i + 2))
{
continue;
}
return i;
}
return -1;
}
///
/// Le point en clôt-il une initiale (majuscule isolée) ?
///
private static bool EstPointDInitiale(string s, int i)
=> i >= 1
&& char.IsUpper(s[i - 1])
&& (i == 1 || s[i - 2] == ' ' || s[i - 2] == '-' || s[i - 2] == '\'');
///
/// Le texte à partir de commence-t-il par une initiale
/// (une majuscule suivie d'un point ou de la fin de chaîne) ?
///
private static bool CommenceParUneInitiale(string s, int debut)
=> debut < s.Length
&& char.IsUpper(s[debut])
&& (debut + 1 == s.Length || s[debut + 1] == '.');
}