Gestion de bibliothèque personnelle auto-hébergée : catalogue, prêts, scan de code-barres, consultation hors-ligne. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
145 lines
4.5 KiB
C#
145 lines
4.5 KiB
C#
using System.Text;
|
|
|
|
namespace MaBibli.Shared.Isbn;
|
|
|
|
/// <summary>
|
|
/// Normalisation, validation et conversion des ISBN.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <b>Piège bloquant documenté dans CLAUDE.md :</b> la BnF indexe l'ISBN <i>tel qu'imprimé</i>
|
|
/// 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.
|
|
/// </remarks>
|
|
public static class IsbnUtils
|
|
{
|
|
/// <summary>
|
|
/// Retire tirets, espaces et points ; met le <c>x</c> final en majuscule.
|
|
/// Renvoie <c>null</c> si l'entrée est vide.
|
|
/// </summary>
|
|
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();
|
|
}
|
|
|
|
/// <summary>Vrai si <paramref name="isbn"/> (déjà normalisé) est un ISBN-10 valide, clé comprise.</summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>Vrai si <paramref name="isbn"/> (déjà normalisé) est un ISBN-13 / EAN-13 valide, clé comprise.</summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>Vrai si l'ISBN normalisé est valide sous l'une ou l'autre forme.</summary>
|
|
public static bool EstValide(string? isbn) => EstIsbn10Valide(isbn) || EstIsbn13Valide(isbn);
|
|
|
|
/// <summary>
|
|
/// Convertit un ISBN-13 en ISBN-10.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Uniquement pour le préfixe <c>978</c> : les ISBN-13 en <c>979</c> n'ont aucun équivalent
|
|
/// ISBN-10 et ne doivent pas être « convertis ». Le calcul suit CLAUDE.md : retirer <c>978</c>,
|
|
/// garder les 9 chiffres, recalculer la clé (somme pondérée 10→2, modulo 11, <c>X</c> si 10).
|
|
/// </remarks>
|
|
/// <returns><c>true</c> et l'ISBN-10 en sortie, ou <c>false</c> si la conversion est impossible.</returns>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// URL de la couverture chez OpenLibrary, à utiliser <b>quelle que soit la source</b>
|
|
/// des métadonnées : le Dublin Core de la BnF n'en fournit aucune.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Le <c>?default=false</c> est indispensable : sans lui OpenLibrary renvoie une image
|
|
/// placeholder au lieu d'un 404, et on afficherait des couvertures vides sans le savoir.
|
|
/// </remarks>
|
|
public static string UrlCouverture(string isbn) =>
|
|
$"https://covers.openlibrary.org/b/isbn/{isbn}-L.jpg?default=false";
|
|
}
|