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>
124 lines
4.7 KiB
C#
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;
|
|
}
|
|
}
|