Aligne les thèmes sur le point-virgule des auteurs
Le champ des auteurs se saisit au point-virgule depuis toujours ; celui des thèmes se saisissait à la virgule. Deux champs voisins du même formulaire ne doivent pas se saisir de deux façons. La virgule reste acceptée en repli : c'était l'habitude, et un thème n'en contient jamais — contrairement à un titre d'article à la une, qui arrive au lot suivant et pour lequel elle couperait le titre en deux. D'où le paramètre de `ListeSaisie` plutôt qu'une règle unique. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -70,9 +70,14 @@
|
||||
</label>
|
||||
|
||||
<label class="champ">
|
||||
<span class="champ-libelle">Thèmes <span class="champ-aide">(séparés par des virgules)</span></span>
|
||||
@* Point-virgule, comme le champ des auteurs juste au-dessus : trois champs voisins du
|
||||
même formulaire ne doivent pas se saisir de trois façons. La virgule reste acceptée —
|
||||
un thème n'en contient jamais, contrairement à un titre d'article à la une. *@
|
||||
<span class="champ-libelle">
|
||||
Thèmes <span class="champ-aide">(séparés par des points-virgules)</span>
|
||||
</span>
|
||||
<input class="champ-saisie" type="text" @bind="Saisie.ThemesTexte" @bind:event="oninput"
|
||||
placeholder="Fantasy, aventure, space opera" autocomplete="off" />
|
||||
placeholder="Fantasy ; aventure ; space opera" autocomplete="off" />
|
||||
</label>
|
||||
|
||||
<label class="champ">
|
||||
|
||||
@@ -31,7 +31,7 @@ public record EnregistrementLivre
|
||||
/// </summary>
|
||||
public List<AuteurSaisi> Auteurs { get; set; } = [];
|
||||
|
||||
/// <summary>Thèmes saisis manuellement, séparés par des virgules dans l'interface.</summary>
|
||||
/// <summary>Thèmes saisis manuellement, séparés par des points-virgules dans l'interface.</summary>
|
||||
public List<string> Themes { get; set; } = [];
|
||||
|
||||
public string? Editeur { get; set; }
|
||||
@@ -73,7 +73,7 @@ public record EnregistrementLivre
|
||||
[JsonIgnore]
|
||||
public string? ThemesTexte
|
||||
{
|
||||
get => Themes.Count == 0 ? null : string.Join(", ", Themes);
|
||||
get => Themes.Count == 0 ? null : ListeSaisie.Joindre(Themes);
|
||||
set => Themes = DecouperThemes(value);
|
||||
}
|
||||
|
||||
@@ -110,19 +110,13 @@ public record EnregistrementLivre
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public static List<string> DecouperThemes(string? saisie)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(saisie))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var vus = new HashSet<string>(StringComparer.Ordinal);
|
||||
return saisie
|
||||
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Where(theme => vus.Add(NormalisationTexte.Normaliser(theme)))
|
||||
.ToList();
|
||||
}
|
||||
/// <summary>
|
||||
/// Découpe la ligne des thèmes. Séparateur : le <b>point-virgule</b>, comme celui des
|
||||
/// auteurs juste au-dessus dans le même formulaire — la virgule reste acceptée, un thème
|
||||
/// n'en contenant jamais.
|
||||
/// </summary>
|
||||
public static List<string> DecouperThemes(string? saisie) =>
|
||||
ListeSaisie.Decouper(saisie, virguleAussi: true);
|
||||
}
|
||||
|
||||
/// <summary>Un auteur tel qu'il est saisi : son nom, et son rôle sur ce livre.</summary>
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
namespace MaBibli.Shared.Textes;
|
||||
|
||||
/// <summary>
|
||||
/// Découpage d'un champ texte qui porte <b>plusieurs valeurs</b> : thèmes d'un livre, articles
|
||||
/// à la une d'un numéro de revue.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <b>Le séparateur est le point-virgule</b>, comme celui des auteurs
|
||||
/// (<c>EnregistrementLivre.SeparateurAuteurs</c>) : trois champs voisins du même formulaire ne
|
||||
/// doivent pas se saisir de trois façons.
|
||||
/// <para>
|
||||
/// ⚠️ <b>La virgule n'est acceptée que là où elle ne peut rien couper de travers.</b> Un thème
|
||||
/// (« dark fantasy ») n'en contient jamais, et l'accepter évite de désapprendre une habitude ;
|
||||
/// un <b>titre d'article</b> en contient souvent (« Ukraine, deux ans après »), et la virgule y
|
||||
/// couperait le titre en deux. D'où le paramètre plutôt qu'une règle unique.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public static class ListeSaisie
|
||||
{
|
||||
public const char Separateur = ';';
|
||||
|
||||
/// <summary>Recompose la ligne de saisie à partir des valeurs enregistrées.</summary>
|
||||
public static string Joindre(IEnumerable<string> valeurs) =>
|
||||
string.Join($"{Separateur} ", valeurs);
|
||||
|
||||
/// <summary>
|
||||
/// Découpe la saisie, élague, écarte les vides et les <b>doublons au sens de la
|
||||
/// normalisation</b> — « Fantasy » et « fantasy » sont la même étiquette, et le premier
|
||||
/// libellé tapé est celui qui reste.
|
||||
/// </summary>
|
||||
public static List<string> Decouper(string? saisie, bool virguleAussi)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(saisie))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
char[] separateurs = virguleAussi ? [Separateur, ','] : [Separateur];
|
||||
var vus = new HashSet<string>(StringComparer.Ordinal);
|
||||
|
||||
return saisie
|
||||
.Split(separateurs, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Where(valeur => vus.Add(NormalisationTexte.Normaliser(valeur)))
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using MaBibli.Shared.Dtos;
|
||||
using MaBibli.Shared.Textes;
|
||||
|
||||
namespace MaBibli.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Découpage des champs qui portent plusieurs valeurs : thèmes d'un livre, articles à la une
|
||||
/// d'un numéro (lot O2).
|
||||
/// </summary>
|
||||
public class ListeSaisieTests
|
||||
{
|
||||
/// <summary>
|
||||
/// ⚠️ Le cas qui a tranché le séparateur : un titre d'article contient souvent une virgule,
|
||||
/// et elle le couperait en deux.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Un_titre_darticle_garde_ses_virgules()
|
||||
{
|
||||
var articles = ListeSaisie.Decouper(
|
||||
"Ukraine, deux ans après ; Le retour du train de nuit", virguleAussi: false);
|
||||
|
||||
Assert.Equal(["Ukraine, deux ans après", "Le retour du train de nuit"], articles);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Les thèmes acceptent la virgule en repli : elle était le séparateur jusqu'au 2026-08-21,
|
||||
/// et un thème n'en contient jamais.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Un_theme_se_saisit_aux_deux_separateurs()
|
||||
{
|
||||
Assert.Equal(
|
||||
["Fantasy", "aventure", "space opera"],
|
||||
EnregistrementLivre.DecouperThemes("Fantasy ; aventure, space opera"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Le_premier_libelle_tape_est_celui_qui_reste()
|
||||
{
|
||||
// « Fantasy » et « fantasy » sont la même étiquette au sens de la normalisation.
|
||||
Assert.Equal(["Fantasy"], EnregistrementLivre.DecouperThemes("Fantasy ; fantasy ; FANTASY"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Les_entrees_vides_disparaissent()
|
||||
{
|
||||
Assert.Empty(ListeSaisie.Decouper(" ; ; ", virguleAussi: false));
|
||||
Assert.Empty(ListeSaisie.Decouper(null, virguleAussi: true));
|
||||
}
|
||||
|
||||
/// <summary>La ligne recomposée se redécoupe à l'identique : c'est ce que fait le formulaire.</summary>
|
||||
[Fact]
|
||||
public void Joindre_puis_decouper_ne_perd_rien()
|
||||
{
|
||||
List<string> articles = ["Ukraine, deux ans après", "Le retour du train de nuit"];
|
||||
|
||||
Assert.Equal(articles, ListeSaisie.Decouper(ListeSaisie.Joindre(articles), virguleAussi: false));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user