Files
mabibli/MaBibli.Client/Services/GroupementCatalogue.cs
T
mathieuandClaude Opus 5 7d2f3dc5d9 Rassemble au catalogue les tomes qui racontent la même histoire
Les livres d'une même série se suivaient au hasard de l'alphabet, et rien
ne disait qu'ils allaient ensemble. Ils tiennent maintenant sous le nom de
leur série, à la place qu'occupait le premier d'entre eux — l'ordre général
ne bouge donc pas — et ce nom mène à la série, seul écran qui montre aussi
les tomes qui manquent.

Le regroupement ne cache ni ne duplique rien : une bascule le défait, et
un livre rattaché à deux séries ne paraît qu'une fois, toujours sous la
même. La règle est une fonction pure, avec ses tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 00:16:52 +02:00

124 lines
4.7 KiB
C#

using MaBibli.Shared.Dtos;
using MaBibli.Shared.Textes;
namespace MaBibli.Client.Services;
/// <summary>
/// Une entrée du catalogue : un livre seul, ou les tomes d'une même série sous son nom.
/// </summary>
public sealed record EntreeCatalogue
{
/// <summary>La série qui coiffe le bloc, ou <c>null</c> pour un livre seul.</summary>
public SerieDto? Serie { get; init; }
/// <summary>Les livres de l'entrée : un seul hors série, les tomes visibles sinon.</summary>
public required IReadOnlyList<LivreDto> Livres { get; init; }
public bool EstGroupe => Serie is not null;
}
/// <summary>
/// Regroupe les livres du catalogue par série.
/// </summary>
/// <remarks>
/// <b>Le bloc se place là où son premier tome serait tombé</b> dans l'ordre du catalogue
/// (choisi avec l'utilisateur le 2026-08-22) : l'ordre général reste celui qu'on connaît, et
/// l'on retrouve « La Légende de Drizzt » entre « Dracula » et « Dune ». À l'intérieur, les
/// tomes suivent l'<b>ordre de lecture</b>, seul ordre qui ait un sens pour une saga — c'est
/// même la raison d'être de <c>ElementSerie.Position</c>.
/// <para>
/// ⚠️ <b>Rien n'est jamais masqué ni déplacé hors de la liste.</b> Un livre filtré reste absent,
/// un livre visible reste visible : le regroupement ne fait que rassembler, et l'écran offre de
/// le défaire. C'est la même règle que le grisage de la bibliographie — on marque, on ne cache
/// pas.
/// </para>
/// <para>
/// ⚠️ Un livre peut appartenir à <b>plusieurs</b> séries (le modèle l'autorise, sans unicité sur
/// <c>LivreId</c> seul). Il n'apparaît pourtant qu'une fois : le dupliquer ferait mentir le
/// compteur du catalogue et donnerait deux cartes du même exemplaire. La série retenue est la
/// <b>première par ordre alphabétique</b> — un critère explicable, à défaut d'être le bon dans
/// tous les cas ; sa fiche livre, elle, les montre toutes.
/// </para>
/// </remarks>
public static class GroupementCatalogue
{
public static IReadOnlyList<EntreeCatalogue> Grouper(
IReadOnlyList<LivreDto> livres, IReadOnlyList<SerieDto>? series)
{
if (series is null || series.Count == 0)
{
return [.. livres.Select(l => new EntreeCatalogue { Livres = [l] })];
}
var place = PlaceDesLivres(series);
// Les tomes s'accumulent dans le brouillon du groupe, créé à la position de son PREMIER
// tome rencontré : c'est ce qui range le bloc là où l'ordre du catalogue l'attend.
var brouillons = new List<(SerieDto? Serie, List<(int Position, LivreDto Livre)> Livres)>();
var groupes = new Dictionary<int, List<(int Position, LivreDto Livre)>>();
foreach (var livre in livres)
{
if (!place.TryGetValue(livre.Id, out var appartenance))
{
brouillons.Add((null, [(0, livre)]));
continue;
}
if (!groupes.TryGetValue(appartenance.Serie.Id, out var tomes))
{
tomes = [];
groupes[appartenance.Serie.Id] = tomes;
brouillons.Add((appartenance.Serie, tomes));
}
tomes.Add((appartenance.Position, livre));
}
return
[
.. brouillons.Select(b => new EntreeCatalogue
{
Serie = b.Serie,
Livres =
[
.. b.Livres
.OrderBy(t => t.Position)
.ThenBy(t => t.Livre.Id)
.Select(t => t.Livre),
],
}),
];
}
/// <summary>
/// À quelle série — et à quelle place — appartient chaque livre rattaché.
/// </summary>
/// <remarks>
/// ⚠️ Les séries sont parcourues dans l'ordre alphabétique de leur titre normalisé, et le
/// <c>TryAdd</c> garde donc la <b>première</b> : sans cet ordre, la série retenue pour un
/// livre rattaché deux fois dépendrait de l'ordre où l'API rend les séries, c'est-à-dire de
/// rien de compréhensible.
/// </remarks>
private static Dictionary<int, (SerieDto Serie, int Position)> PlaceDesLivres(
IReadOnlyList<SerieDto> series)
{
var place = new Dictionary<int, (SerieDto, int)>();
foreach (var serie in series
.OrderBy(s => NormalisationTexte.Normaliser(s.Titre), StringComparer.Ordinal)
.ThenBy(s => s.Id))
{
foreach (var element in serie.Elements)
{
if (element.LivreId is { } livreId)
{
place.TryAdd(livreId, (serie, element.Position));
}
}
}
return place;
}
}