using System.Text; namespace MaBibli.Shared.Isbn; /// /// Normalisation, validation et conversion des ISBN. /// /// /// Piège bloquant documenté dans CLAUDE.md : la BnF indexe l'ISBN tel qu'imprimé /// sur le livre. Les ouvrages parus avant 2007 ne portent qu'un ISBN-10 et sont donc /// introuvables par leur ISBN-13, alors qu'un scanner de code-barres lit toujours un EAN-13. /// La conversion 13 → 10 n'est pas une optimisation : sans elle, tout le fonds ancien est perdu. /// public static class IsbnUtils { /// /// Retire tirets, espaces et points ; met le x final en majuscule. /// Renvoie null si l'entrée est vide. /// public static string? Normaliser(string? brut) { if (string.IsNullOrWhiteSpace(brut)) { return null; } var sb = new StringBuilder(brut.Length); foreach (var c in brut) { if (char.IsDigit(c)) { sb.Append(c); } else if (c is 'x' or 'X') { sb.Append('X'); } // Tout le reste (tirets, espaces, points, insécables…) est ignoré. } return sb.Length == 0 ? null : sb.ToString(); } /// Vrai si (déjà normalisé) est un ISBN-10 valide, clé comprise. public static bool EstIsbn10Valide(string? isbn) { if (isbn is not { Length: 10 }) { return false; } var somme = 0; for (var i = 0; i < 10; i++) { var c = isbn[i]; int valeur; if (char.IsDigit(c)) { valeur = c - '0'; } else if (c == 'X' && i == 9) { // Le X n'est autorisé qu'en position de clé. valeur = 10; } else { return false; } somme += valeur * (10 - i); } return somme % 11 == 0; } /// Vrai si (déjà normalisé) est un ISBN-13 / EAN-13 valide, clé comprise. public static bool EstIsbn13Valide(string? isbn) { if (isbn is not { Length: 13 }) { return false; } var somme = 0; for (var i = 0; i < 13; i++) { if (!char.IsDigit(isbn[i])) { return false; } somme += (isbn[i] - '0') * (i % 2 == 0 ? 1 : 3); } return somme % 10 == 0; } /// Vrai si l'ISBN normalisé est valide sous l'une ou l'autre forme. public static bool EstValide(string? isbn) => EstIsbn10Valide(isbn) || EstIsbn13Valide(isbn); /// /// Convertit un ISBN-13 en ISBN-10. /// /// /// Uniquement pour le préfixe 978 : les ISBN-13 en 979 n'ont aucun équivalent /// ISBN-10 et ne doivent pas être « convertis ». Le calcul suit CLAUDE.md : retirer 978, /// garder les 9 chiffres, recalculer la clé (somme pondérée 10→2, modulo 11, X si 10). /// /// true et l'ISBN-10 en sortie, ou false si la conversion est impossible. public static bool TryConvertirEnIsbn10(string? isbn13, out string? isbn10) { isbn10 = null; if (!EstIsbn13Valide(isbn13) || !isbn13!.StartsWith("978", StringComparison.Ordinal)) { return false; } // Les 9 chiffres significatifs : on retire le préfixe 978 et la clé EAN-13. var corps = isbn13.Substring(3, 9); var somme = 0; for (var i = 0; i < 9; i++) { somme += (corps[i] - '0') * (10 - i); } var cle = (11 - (somme % 11)) % 11; isbn10 = corps + (cle == 10 ? "X" : cle.ToString()); return true; } /// /// URL de la couverture chez OpenLibrary, à utiliser quelle que soit la source /// des métadonnées : le Dublin Core de la BnF n'en fournit aucune. /// /// /// Le ?default=false est indispensable : sans lui OpenLibrary renvoie une image /// placeholder au lieu d'un 404, et on afficherait des couvertures vides sans le savoir. /// public static string UrlCouverture(string isbn) => $"https://covers.openlibrary.org/b/isbn/{isbn}-L.jpg?default=false"; }