Ajouter la liste d'envies personnelle, son export et la bibliographie par auteur

La liste d'envies vit dans une table séparée (LivreSouhaite) plutôt que dans un
statut de plus sur Livre : un livre souhaité n'est pas possédé, et le loger dans
Livres l'aurait fait entrer dans le catalogue, les compteurs et les prêts, au
prix d'un « et qui n'est pas souhaité » à répéter dans chaque lecture. La portée
est personnelle, comme le statut de lecture — mais ici Utilisateur est une vraie
frontière : toute lecture filtre dessus.

Bibliographie : SRU BnF interrogé par bib.author, vérifié le 2026-08-18. Deux
filtres mesurés sur des réponses réelles sont indispensables — le type de
document (l'index mêle livres audio, jeux et spectacles) et surtout l'auteur
réel de la notice, « all » rapprochant les mots sur l'ensemble des auteurs :
« Émile Zola » remonte sinon toute l'œuvre de sa fille Denise Le Blond-Zola.
Les rééditions sont regroupées par clé d'œuvre (183 notices Werber -> 51
œuvres). Le rapprochement avec l'étagère se fait par titre, pas par ISBN, qui
désigne une édition et non une œuvre ; ses limites sont dites à l'écran.

Export en deux formats, tous deux du texte sans dépendance : .txt groupé par
auteur pour la librairie, .csv à séparateur point-virgule et BOM pour le
tableur.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
mathieu
2026-08-18 13:59:06 +02:00
co-authored by Claude Opus 5
parent 3a5a3a7763
commit 8b8fe3be6e
27 changed files with 3939 additions and 0 deletions
+82
View File
@@ -0,0 +1,82 @@
using MaBibli.Shared.Textes;
namespace MaBibli.Tests;
/// <summary>
/// Clé d'œuvre : ce qu'elle réunit, et surtout <b>ce qu'elle rate</b>.
/// </summary>
/// <remarks>
/// Les cas d'échec sont testés au même titre que les réussites : ils documentent la limite du
/// rapprochement, que l'interface annonce à l'utilisateur. Si l'un d'eux se met à passer un
/// jour, c'est que la règle a changé — et le message affiché doit changer avec.
/// </remarks>
public class CleOeuvreTests
{
[Theory]
// Les trois formes rencontrées pour un même roman de Werber.
[InlineData("Les thanatonautes", "Les thanatonautes : roman")]
[InlineData("Les thanatonautes", "Les thanatonautes / Bernard Werber")]
[InlineData("Les thanatonautes : roman", "Les thanatonautes / Bernard Werber")]
// Casse et accents, comme partout ailleurs dans le projet.
[InlineData("Germinal", "GERMINAL")]
public void Reunit_les_variantes_d_un_meme_titre(string a, string b) =>
Assert.Equal(CleOeuvre.Cle(a), CleOeuvre.Cle(b));
[Theory]
[InlineData("Germinal", "La bête humaine")]
// Un tome n'est pas rapproché de l'œuvre entière : le point n'est pas coupé.
[InlineData("Troisième humanité", "Troisième humanité. Tome 1")]
public void Distingue_deux_oeuvres_differentes(string a, string b) =>
Assert.NotEqual(CleOeuvre.Cle(a), CleOeuvre.Cle(b));
/// <summary>
/// Limite assumée : deux œuvres distinctes partageant leur titre principal se confondent.
/// Le risque est borné, la comparaison n'ayant lieu qu'à auteur donné.
/// </summary>
[Fact]
public void Confond_deux_oeuvres_partageant_le_titre_principal() =>
Assert.Equal(
CleOeuvre.Cle("Nouvelles : premier recueil"),
CleOeuvre.Cle("Nouvelles : second recueil"));
/// <summary>
/// Limite <b>héritée</b> de <see cref="NormalisationTexte"/>, pas propre à la clé d'œuvre :
/// la ligature « œ » n'est pas ramenée à « oe ».
/// </summary>
/// <remarks>
/// <c>NormalisationTexte</c> décompose en NFD, qui sépare les accents mais laisse les
/// ligatures intactes — seul NFKD les défait. Conséquence concrète : <i>L'Œuvre</i> de Zola,
/// que la BnF écrit avec la ligature, n'est pas rapproché d'un « L'oeuvre » saisi au clavier.
/// <para>
/// Le défaut ne vient pas d'ici et ne se corrige pas ici : la même fonction alimente la
/// recherche du catalogue et la clé unique des auteurs, et en changer déplacerait ces deux
/// invariants. Consigné dans IDEES.md ; ce test existe pour que la limite soit visible plutôt
/// que découverte en usage.
/// </para>
/// </remarks>
[Fact]
public void Ne_reduit_pas_la_ligature_oe() =>
Assert.NotEqual(CleOeuvre.Cle("L'Œuvre"), CleOeuvre.Cle("L'oeuvre"));
/// <summary>
/// Limite assumée : un titre retraduit n'est pas rapproché du tout.
/// </summary>
[Fact]
public void Ne_rapproche_pas_un_titre_retraduit() =>
Assert.NotEqual(CleOeuvre.Cle("Le Seigneur des anneaux"), CleOeuvre.Cle("The Lord of the Rings"));
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public void Rend_une_cle_vide_pour_une_saisie_vide(string? titre) =>
Assert.Equal(string.Empty, CleOeuvre.Cle(titre));
/// <summary>
/// Un titre réduit à son seul sous-titre ne doit pas produire une clé vide : elle se
/// confondrait avec toutes les autres clés vides.
/// </summary>
[Fact]
public void Ne_vide_pas_un_titre_qui_commence_par_le_separateur() =>
Assert.NotEqual(string.Empty, CleOeuvre.Cle(": roman"));
}